mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-19 14:59:21 +00:00
Compare commits
872 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0772f09e98 | ||
|
|
e916673454 | ||
|
|
ac163d5130 | ||
|
|
d656b67570 | ||
|
|
3c66b65ac6 | ||
|
|
c227933b3b | ||
|
|
5cf9cb8e50 | ||
|
|
693cd6120f | ||
|
|
6efd800481 | ||
|
|
a723a7cba3 | ||
|
|
b1a5eb5f44 | ||
|
|
8232282d13 | ||
|
|
404d359f89 | ||
|
|
62e84d92fc | ||
|
|
9b2246eac2 | ||
|
|
e6d3935246 | ||
|
|
5c4111b6a7 | ||
|
|
51a700600a | ||
|
|
23c3ee08e9 | ||
|
|
c1d08f9c23 | ||
|
|
4cabdbe952 | ||
|
|
abdebd960b | ||
|
|
a7aea51a45 | ||
|
|
c8a4376208 | ||
|
|
315cd4ba6b | ||
|
|
6607ac5a6e | ||
|
|
562939498a | ||
|
|
e686d43ca8 | ||
|
|
e9ac400215 | ||
|
|
234870537a | ||
|
|
b5f392a20f | ||
|
|
9ea8eb4101 | ||
|
|
dc7cde1c33 | ||
|
|
4e75b5ca1d | ||
|
|
fe0bdccf40 | ||
|
|
c615ab592b | ||
|
|
16d4a8fd9b | ||
|
|
96f34092f6 | ||
|
|
a31c3c48f7 | ||
|
|
0e96374a03 | ||
|
|
24545563fa | ||
|
|
a9ea4d34e5 | ||
|
|
11453393d4 | ||
|
|
1d850a9ddb | ||
|
|
5b90b3d006 | ||
|
|
d85ec65384 | ||
|
|
9d681db030 | ||
|
|
c5c2dde0bf | ||
|
|
807bffe9aa | ||
|
|
a1956c6822 | ||
|
|
321dccb0b5 | ||
|
|
1015c50802 | ||
|
|
c5e759390b | ||
|
|
883695b0b0 | ||
|
|
879aef471a | ||
|
|
64c81a9a5a | ||
|
|
b61a9a7bc3 | ||
|
|
3f725eb40c | ||
|
|
8f1e4a5198 | ||
|
|
3875447430 | ||
|
|
dce84f285d | ||
|
|
1314683eea | ||
|
|
6b1f89aab0 | ||
|
|
27e958a474 | ||
|
|
39ebd0dc4f | ||
|
|
2ada0497ec | ||
|
|
e9f2856040 | ||
|
|
6050c584c0 | ||
|
|
7ebf91bfae | ||
|
|
2d7799628a | ||
|
|
1627de732b | ||
|
|
fea882c642 | ||
|
|
8b749357f7 | ||
|
|
e4ff7e6d6c | ||
|
|
396630821f | ||
|
|
70f65d126b | ||
|
|
e8925a0d79 | ||
|
|
2a3cf9be7d | ||
|
|
971d3f5167 | ||
|
|
99da65a4d8 | ||
|
|
25744b4ef7 | ||
|
|
8f2d1c7298 | ||
|
|
a9aa93a682 | ||
|
|
ef340840ab | ||
|
|
cf08015961 | ||
|
|
cb09e0cfb2 | ||
|
|
554fac89c0 | ||
|
|
215bed9908 | ||
|
|
935aafa74a | ||
|
|
c51ba85f38 | ||
|
|
6a67d1bbe0 | ||
|
|
20bc76a768 | ||
|
|
6928f97dff | ||
|
|
06742fb848 | ||
|
|
759983b000 | ||
|
|
5556e3b6ce | ||
|
|
bcffcb1f64 | ||
|
|
fa77e6e418 | ||
|
|
c8aa29c82f | ||
|
|
51cf982359 | ||
|
|
f6d860335f | ||
|
|
5cea4665a1 | ||
|
|
34b183a35e | ||
|
|
61298c24bb | ||
|
|
af9d32895e | ||
|
|
52fa67432c | ||
|
|
404f18d81f | ||
|
|
9ee8ea1c84 | ||
|
|
64f845e293 | ||
|
|
30d1acc67e | ||
|
|
8d047d8892 | ||
|
|
cb49ab8d73 | ||
|
|
436e85dada | ||
|
|
223333882d | ||
|
|
06d8dda7dd | ||
|
|
805ffb19c2 | ||
|
|
cd3e4ef063 | ||
|
|
557b738511 | ||
|
|
8ee6b7a199 | ||
|
|
dc880bc37f | ||
|
|
c419587933 | ||
|
|
afb4fdd5d5 | ||
|
|
bf8474e02d | ||
|
|
937ea9bc8e | ||
|
|
ef7b3c4f32 | ||
|
|
370712c5b2 | ||
|
|
ae657acee3 | ||
|
|
34dc6a537d | ||
|
|
e99ba66271 | ||
|
|
0111f25865 | ||
|
|
02c8b899dc | ||
|
|
b725015318 | ||
|
|
11550e0ba3 | ||
|
|
33e20a0c0b | ||
|
|
0ae13d730d | ||
|
|
f5ad5c079f | ||
|
|
305f90f697 | ||
|
|
e56041eae2 | ||
|
|
904c65ba39 | ||
|
|
fbb8ec3048 | ||
|
|
2a59a0b3f5 | ||
|
|
b108d38598 | ||
|
|
8b7e891ab6 | ||
|
|
be6bebe361 | ||
|
|
1ff4cab88d | ||
|
|
e3c40a231b | ||
|
|
c8fd3f88fa | ||
|
|
f9ec1f3ebb | ||
|
|
033af3126c | ||
|
|
a15739fbb9 | ||
|
|
f4b9124636 | ||
|
|
fecda83fcb | ||
|
|
07228c3661 | ||
|
|
a0fb2e90b3 | ||
|
|
ae69c1c060 | ||
|
|
4992f4492c | ||
|
|
58bd3acdc3 | ||
|
|
af51c035a3 | ||
|
|
85ae2b5fb2 | ||
|
|
7ff10b33a0 | ||
|
|
431c9e13c9 | ||
|
|
c8579fb987 | ||
|
|
2f4eb46456 | ||
|
|
2d547507d5 | ||
|
|
8e4678468e | ||
|
|
c2a32cabc5 | ||
|
|
07f06aac68 | ||
|
|
7ae1001b1b | ||
|
|
e04c19acf5 | ||
|
|
1820a810f4 | ||
|
|
cedd12a048 | ||
|
|
7967d00208 | ||
|
|
00a68be2ad | ||
|
|
da5d991d2a | ||
|
|
c2f6d240ad | ||
|
|
4cde24c536 | ||
|
|
029617bc45 | ||
|
|
0627fb20e4 | ||
|
|
bc1f983684 | ||
|
|
31eb747c55 | ||
|
|
e8facf52ce | ||
|
|
5ee4bdd644 | ||
|
|
92c371e201 | ||
|
|
d27278454a | ||
|
|
16780c149c | ||
|
|
0445ba63bc | ||
|
|
b6aefd6f57 | ||
|
|
f803af0225 | ||
|
|
2675f4a377 | ||
|
|
03341c981f | ||
|
|
38cc654167 | ||
|
|
b347a8d060 | ||
|
|
8733b86b45 | ||
|
|
eda8e72633 | ||
|
|
c942a015d1 | ||
|
|
c00265f1f9 | ||
|
|
e303984dcf | ||
|
|
b449b5dee6 | ||
|
|
48f8c0c967 | ||
|
|
f88c4c3e2c | ||
|
|
e6273fa9a0 | ||
|
|
1ba5585d0d | ||
|
|
73547371ae | ||
|
|
fca6825394 | ||
|
|
53af114f44 | ||
|
|
311cc85c92 | ||
|
|
099a258cf8 | ||
|
|
480f59a166 | ||
|
|
45c2f5350f | ||
|
|
ed88d77852 | ||
|
|
e7fc9f015e | ||
|
|
cabde9e8d8 | ||
|
|
4df7d2def4 | ||
|
|
59ed77c169 | ||
|
|
53da3bd40c | ||
|
|
abdf8223fc | ||
|
|
a5efeecad3 | ||
|
|
4d26b24d13 | ||
|
|
29b3a60c64 | ||
|
|
da894f57c4 | ||
|
|
1bd703641b | ||
|
|
1b1d4c8e8d | ||
|
|
842ec0e28d | ||
|
|
2d5648408e | ||
|
|
41b3240741 | ||
|
|
bc03e8f3e3 | ||
|
|
86799f4c08 | ||
|
|
a3e004cf71 | ||
|
|
a7aeb1de21 | ||
|
|
1a703102c3 | ||
|
|
368c68f42b | ||
|
|
e191713bdc | ||
|
|
1604338498 | ||
|
|
c223797bf4 | ||
|
|
9b10adc8e4 | ||
|
|
5935f1ab5f | ||
|
|
3455b10a20 | ||
|
|
34e08b712d | ||
|
|
605d314a58 | ||
|
|
daaf5147a7 | ||
|
|
50eb9a7543 | ||
|
|
7d8ef7b6b3 | ||
|
|
347d2d0a8f | ||
|
|
bd560fcc99 | ||
|
|
b68aedc99a | ||
|
|
47373d8974 | ||
|
|
a57b8df994 | ||
|
|
17235637a5 | ||
|
|
cd5abd5a65 | ||
|
|
5c6631cdcf | ||
|
|
621afcd5c6 | ||
|
|
112a974a6c | ||
|
|
f9d46b4aff | ||
|
|
dfd828bca1 | ||
|
|
2df9fd897a | ||
|
|
ff2531539e | ||
|
|
52ec138273 | ||
|
|
0f7a866652 | ||
|
|
eeb52ac41e | ||
|
|
4c7ec9ec21 | ||
|
|
2f9064c4c6 | ||
|
|
5c234cdb1d | ||
|
|
7d8a74381b | ||
|
|
04a7793585 | ||
|
|
ea068529f6 | ||
|
|
fead3df0d8 | ||
|
|
0c5a42a344 | ||
|
|
75a2378ea8 | ||
|
|
662fc96ad5 | ||
|
|
f456443da0 | ||
|
|
07da1c04ce | ||
|
|
845abf370a | ||
|
|
83b5209a75 | ||
|
|
8e9850dd19 | ||
|
|
1d24209b68 | ||
|
|
da70a54d78 | ||
|
|
fc7330213a | ||
|
|
d8272ba52d | ||
|
|
315f0eeb2f | ||
|
|
8e83617ac4 | ||
|
|
212bb0beb8 | ||
|
|
c16231b12b | ||
|
|
93f7294859 | ||
|
|
32ac57e2a4 | ||
|
|
c949c976d6 | ||
|
|
ab736829f2 | ||
|
|
4433926ce7 | ||
|
|
f819fd4d5e | ||
|
|
e7659255fe | ||
|
|
0dee2e8319 | ||
|
|
7b35c47315 | ||
|
|
5056a794d8 | ||
|
|
5e6068431a | ||
|
|
8d69508689 | ||
|
|
efb6d8a7de | ||
|
|
79d9778378 | ||
|
|
6a6695e447 | ||
|
|
8862e6cd70 | ||
|
|
0b29de9efc | ||
|
|
962cdfce0b | ||
|
|
0c527202e5 | ||
|
|
d4e33aa9d2 | ||
|
|
2fcd58fc18 | ||
|
|
3d10b2324f | ||
|
|
31419f3b97 | ||
|
|
8105ac27fc | ||
|
|
44f251a948 | ||
|
|
463e707d27 | ||
|
|
2d85910744 | ||
|
|
268b279fdf | ||
|
|
a8ca3314d8 | ||
|
|
2bdd3dae37 | ||
|
|
e29564c4ad | ||
|
|
6b8bb23af9 | ||
|
|
91bbe05851 | ||
|
|
8ed6869aad | ||
|
|
7efdf0d329 | ||
|
|
49c32e3f98 | ||
|
|
f0574527b9 | ||
|
|
ad510a8fca | ||
|
|
303b287705 | ||
|
|
549508b9c1 | ||
|
|
6c8a577701 | ||
|
|
862177bec7 | ||
|
|
dbed4d83a2 | ||
|
|
aa2090d97a | ||
|
|
b168ede7c5 | ||
|
|
0e706d36c4 | ||
|
|
69d68de5c0 | ||
|
|
3d5395e5ae | ||
|
|
332c71f041 | ||
|
|
b9fbcd72dd | ||
|
|
68897e6265 | ||
|
|
04606a7c9a | ||
|
|
6286bbe2ad | ||
|
|
24ba209f8f | ||
|
|
05d588f681 | ||
|
|
9aa3606f54 | ||
|
|
fc05e4b17a | ||
|
|
7b2b47de83 | ||
|
|
be0b4e3397 | ||
|
|
dd1ba594de | ||
|
|
89368778f3 | ||
|
|
e3fb1762a1 | ||
|
|
516c243df8 | ||
|
|
b7aa75fcd5 | ||
|
|
549f013e0f | ||
|
|
14c56af465 | ||
|
|
bee01dd15a | ||
|
|
e97551e67e | ||
|
|
97023e8425 | ||
|
|
4fa7106a46 | ||
|
|
e0b81e4c76 | ||
|
|
c4adec3082 | ||
|
|
107238360c | ||
|
|
6141adbdb9 | ||
|
|
eaa2ed74a6 | ||
|
|
44c652c452 | ||
|
|
78cf6bff63 | ||
|
|
8ad4158ec0 | ||
|
|
405e6d7162 | ||
|
|
cff1c8f982 | ||
|
|
f43ca2f043 | ||
|
|
3114ab1a62 | ||
|
|
2da9749b0c | ||
|
|
d4d510e100 | ||
|
|
550ea26097 | ||
|
|
2b1e72a42e | ||
|
|
75f8bee706 | ||
|
|
a3b50a0bf5 | ||
|
|
e9ab0909ce | ||
|
|
1e91b4b4ce | ||
|
|
043b51d812 | ||
|
|
fa5cea2aa3 | ||
|
|
e35dcf3aad | ||
|
|
68ec79add6 | ||
|
|
70235963ba | ||
|
|
245fbec80c | ||
|
|
4d1a465c03 | ||
|
|
07dc0b8c12 | ||
|
|
525f15e65b | ||
|
|
017d26673a | ||
|
|
087c787ec2 | ||
|
|
7218390f65 | ||
|
|
bd312c48ea | ||
|
|
e4cc792f54 | ||
|
|
7941745ed0 | ||
|
|
21f125c48a | ||
|
|
18b8b958fe | ||
|
|
35c23c7970 | ||
|
|
4bb38f67d3 | ||
|
|
98cf42065b | ||
|
|
328d4476aa | ||
|
|
8293d767da | ||
|
|
7b8fb92737 | ||
|
|
0f1cc2d24f | ||
|
|
676ba7a10a | ||
|
|
82dc765f66 | ||
|
|
16cfbc9410 | ||
|
|
2b4c7e6c3d | ||
|
|
a03d540938 | ||
|
|
6bcb3e7908 | ||
|
|
facbda65a8 | ||
|
|
2f5ddf6164 | ||
|
|
4dfbc0e32b | ||
|
|
07d0376dd6 | ||
|
|
d23ea087c1 | ||
|
|
ea77f2d782 | ||
|
|
ef3dc7e602 | ||
|
|
3fe6245ddf | ||
|
|
a35e83a424 | ||
|
|
be5d1128de | ||
|
|
8463e1bb7a | ||
|
|
5456b232f0 | ||
|
|
b0e585841c | ||
|
|
cd298af5ae | ||
|
|
e19477aada | ||
|
|
7f75b9a917 | ||
|
|
819421492b | ||
|
|
8f13a3955c | ||
|
|
73de0ff392 | ||
|
|
93edb0de4c | ||
|
|
bb5ae0b741 | ||
|
|
ccd7601a0e | ||
|
|
50d15e9580 | ||
|
|
aa1e8b8e95 | ||
|
|
3ff01f7bb3 | ||
|
|
93ab25bf44 | ||
|
|
4c0addfd67 | ||
|
|
b77cf2524d | ||
|
|
501ea3f609 | ||
|
|
a93af95fd2 | ||
|
|
39767c5cd0 | ||
|
|
48f7037898 | ||
|
|
67dddf0cfa | ||
|
|
eeb5e57c1e | ||
|
|
5a6ea2cffe | ||
|
|
07ddd34f8e | ||
|
|
a0b86c298f | ||
|
|
6fbef032bc | ||
|
|
318aacd432 | ||
|
|
ba1b2ff62e | ||
|
|
67ef4888da | ||
|
|
a5d3015c6e | ||
|
|
131a364f53 | ||
|
|
19636d9447 | ||
|
|
f803a0b57b | ||
|
|
28afffe95e | ||
|
|
c7e100a78a | ||
|
|
12e223618d | ||
|
|
f6773f4e68 | ||
|
|
b9434a56e8 | ||
|
|
325fac41bf | ||
|
|
87377bffc1 | ||
|
|
c6fb7fb10f | ||
|
|
c2ae9bd20a | ||
|
|
e580c4f2bd | ||
|
|
dabd9bc57d | ||
|
|
fa7f6f0d6e | ||
|
|
267cf1ff35 | ||
|
|
08944ffd35 | ||
|
|
c75114b858 | ||
|
|
350a8b864d | ||
|
|
df0bd7a7cb | ||
|
|
9fc9e8caf4 | ||
|
|
2e3db2fffa | ||
|
|
530b3ef24d | ||
|
|
a9bd4dd2f0 | ||
|
|
85706d97c5 | ||
|
|
f7e3104e6b | ||
|
|
f56d7f6113 | ||
|
|
685431a40d | ||
|
|
9da3ff5281 | ||
|
|
24453d0e1a | ||
|
|
280e90e297 | ||
|
|
31920d056d | ||
|
|
6641e11fd9 | ||
|
|
66bbde0b5d | ||
|
|
7dde6423e4 | ||
|
|
0eac12e3f8 | ||
|
|
5df58e4ef9 | ||
|
|
4a6583d8bd | ||
|
|
059ee6f660 | ||
|
|
414246f243 | ||
|
|
a3e269c00b | ||
|
|
bf9f29ffe9 | ||
|
|
29bd2e1319 | ||
|
|
2475a9b3c6 | ||
|
|
2a52cc9086 | ||
|
|
237abf9698 | ||
|
|
adbc264532 | ||
|
|
f9cfec7d03 | ||
|
|
29805bfe04 | ||
|
|
8d2e5a6a52 | ||
|
|
d4f61bd646 | ||
|
|
55173be04b | ||
|
|
e7078c9542 | ||
|
|
2aa33b2f2c | ||
|
|
972fee1bc7 | ||
|
|
efe34977b5 | ||
|
|
02ed9687ee | ||
|
|
08889a1739 | ||
|
|
9cf6d1eab6 | ||
|
|
33c2eb2237 | ||
|
|
299586aa86 | ||
|
|
05c988f2c7 | ||
|
|
8e0ad76968 | ||
|
|
4db162e78f | ||
|
|
272bc1278a | ||
|
|
35fdcf7106 | ||
|
|
48e087a3d7 | ||
|
|
ca000197e4 | ||
|
|
a6bab88cee | ||
|
|
f0c64df439 | ||
|
|
ac5ab56717 | ||
|
|
c2025350ba | ||
|
|
4c2bb5c6df | ||
|
|
fb069296e1 | ||
|
|
22eedc8522 | ||
|
|
664c8c3352 | ||
|
|
e7e6f9cfa7 | ||
|
|
5ec0f1b098 | ||
|
|
8fad075357 | ||
|
|
83e27cca83 | ||
|
|
729230a646 | ||
|
|
029407613f | ||
|
|
3d6ff7c30b | ||
|
|
5833ce955f | ||
|
|
b3a5091828 | ||
|
|
693209ff00 | ||
|
|
5d1bd7c3a9 | ||
|
|
7b8c75dbeb | ||
|
|
b7a30bd6e9 | ||
|
|
2daf5fedef | ||
|
|
5fd40915d0 | ||
|
|
c49700e9ec | ||
|
|
0f35331441 | ||
|
|
0153e14ce5 | ||
|
|
419434bdca | ||
|
|
f1be4d2d88 | ||
|
|
c973dc5f05 | ||
|
|
b530941687 | ||
|
|
c09fde4c36 | ||
|
|
8d3beab145 | ||
|
|
cdf666bde6 | ||
|
|
350528e394 | ||
|
|
a1d3e5fddc | ||
|
|
e096e58c45 | ||
|
|
75eafe57e2 | ||
|
|
0005392cd3 | ||
|
|
93913ca4ef | ||
|
|
aa09639e55 | ||
|
|
b205643fdd | ||
|
|
6fc827dedf | ||
|
|
b8aa1d9701 | ||
|
|
2db3bb313f | ||
|
|
4d23377a18 | ||
|
|
51116cbdfb | ||
|
|
6831ce094d | ||
|
|
289227e763 | ||
|
|
3b8a9e4ed1 | ||
|
|
7db3ed678f | ||
|
|
6d9e68a65b | ||
|
|
2c33f3b0b4 | ||
|
|
c002d86fc0 | ||
|
|
a384de8e42 | ||
|
|
cae17f6e68 | ||
|
|
03ca335293 | ||
|
|
c2b9e6c932 | ||
|
|
518853c921 | ||
|
|
fe9dda31bd | ||
|
|
0fd29bca57 | ||
|
|
37a671dae9 | ||
|
|
c406f40898 | ||
|
|
7561762c25 | ||
|
|
d245245083 | ||
|
|
2b55e129b3 | ||
|
|
0caec74436 | ||
|
|
55e443cd49 | ||
|
|
b63e1bd283 | ||
|
|
575122e6dd | ||
|
|
856cbb9caa | ||
|
|
7034a97d3a | ||
|
|
635edb930f | ||
|
|
1d3e4b7a20 | ||
|
|
6f9abb63cc | ||
|
|
563b9e146e | ||
|
|
7c9ac37eb7 | ||
|
|
5d3de4115f | ||
|
|
105f65c93a | ||
|
|
9018ad02e1 | ||
|
|
9db9a3e721 | ||
|
|
9120c062de | ||
|
|
bd83c8935d | ||
|
|
62095364ce | ||
|
|
304df9984c | ||
|
|
b73de81519 | ||
|
|
12e882fe22 | ||
|
|
4ed8b94d55 | ||
|
|
854bf37186 | ||
|
|
8bab2a8123 | ||
|
|
97ad608d56 | ||
|
|
f7419f7277 | ||
|
|
f0497f61f0 | ||
|
|
f6aab54d4d | ||
|
|
c306d700ce | ||
|
|
bbcb091daf | ||
|
|
eb9e2491e5 | ||
|
|
0250ad80c8 | ||
|
|
516b163e07 | ||
|
|
c123b15708 | ||
|
|
29f8d8bf50 | ||
|
|
4f65cf49ef | ||
|
|
27f9e24dfb | ||
|
|
2cbe39f158 | ||
|
|
5840f61571 | ||
|
|
6bd12dc000 | ||
|
|
1afbd3753a | ||
|
|
38a063420b | ||
|
|
0fae3484a1 | ||
|
|
7bb4bff485 | ||
|
|
c5f5cd702e | ||
|
|
3ced2cc0af | ||
|
|
73e1840574 | ||
|
|
d1ca56af1f | ||
|
|
55211e30c5 | ||
|
|
2c1a38f2c9 | ||
|
|
0c7e052d44 | ||
|
|
54cc11fce0 | ||
|
|
3645fa01ec | ||
|
|
e20a0da845 | ||
|
|
7ed7d0a29e | ||
|
|
e7e7995e4e | ||
|
|
af57cfcf70 | ||
|
|
f1ac9b5e04 | ||
|
|
a9b1070725 | ||
|
|
5a000add98 | ||
|
|
aec2836d1e | ||
|
|
84e2ea3904 | ||
|
|
4f669170c2 | ||
|
|
8ea8c7b7ba | ||
|
|
acab4ae58a | ||
|
|
ea822b0f4b | ||
|
|
24a9974ff7 | ||
|
|
222a9871e0 | ||
|
|
0ce9d2ce74 | ||
|
|
cde500a123 | ||
|
|
f15790030f | ||
|
|
ce3350c6fa | ||
|
|
4288742052 | ||
|
|
8205b9c169 | ||
|
|
1d7f6a8d8b | ||
|
|
3425c97245 | ||
|
|
2d1d8f1ab6 | ||
|
|
f322d31b03 | ||
|
|
368e665ac3 | ||
|
|
922eb2f19a | ||
|
|
23e0123004 | ||
|
|
e98bac844a | ||
|
|
d6d9a55f72 | ||
|
|
e3070a65ab | ||
|
|
a8b4696604 | ||
|
|
7f5ca6206b | ||
|
|
ad885a9a15 | ||
|
|
fe89e7b770 | ||
|
|
17ea62ed0b | ||
|
|
94717637ba | ||
|
|
f6663f0c09 | ||
|
|
33588c2345 | ||
|
|
c2c5a424fb | ||
|
|
ce41053e87 | ||
|
|
5817de83e5 | ||
|
|
30dd48ce88 | ||
|
|
cf7912a89e | ||
|
|
9900b44858 | ||
|
|
9019181208 | ||
|
|
99483387f1 | ||
|
|
42177f2582 | ||
|
|
e4e0743205 | ||
|
|
105927a57f | ||
|
|
71706bf9ae | ||
|
|
101e0c11d7 | ||
|
|
70323fb2e2 | ||
|
|
9dc5577175 | ||
|
|
117d5edea2 | ||
|
|
3f0f518037 | ||
|
|
2017ecc20f | ||
|
|
ded89ad343 | ||
|
|
c4b194f8d6 | ||
|
|
d682e6e6c6 | ||
|
|
6fef9c4eae | ||
|
|
16eee05065 | ||
|
|
b664e2586d | ||
|
|
d594c9c257 | ||
|
|
532a65eca8 | ||
|
|
5af8ae0da5 | ||
|
|
c0709f82bd | ||
|
|
945b65e1bc | ||
|
|
efcb36d345 | ||
|
|
30cd89c578 | ||
|
|
bb3753b8e4 | ||
|
|
d5569ad3ed | ||
|
|
d8386fd2a2 | ||
|
|
3bfea58f35 | ||
|
|
51cf7beeb8 | ||
|
|
df247b41f4 | ||
|
|
bac760165e | ||
|
|
dd39482ed1 | ||
|
|
c05f165278 | ||
|
|
c888759d33 | ||
|
|
089ea5b63a | ||
|
|
9020e9d906 | ||
|
|
7584ea0070 | ||
|
|
9c243c2893 | ||
|
|
8ba90fadc4 | ||
|
|
480796fbee | ||
|
|
9b186ec97a | ||
|
|
d828631dea | ||
|
|
5b8de7967b | ||
|
|
4fddbc2b32 | ||
|
|
43cd367ca3 | ||
|
|
19ca504bab | ||
|
|
394b8ff1d1 | ||
|
|
4577da3336 | ||
|
|
c3b339b3dd | ||
|
|
2b91154ca2 | ||
|
|
2351be31e3 | ||
|
|
624543b93d | ||
|
|
c13f4b2a05 | ||
|
|
00b8d335d8 | ||
|
|
89d8b79ae3 | ||
|
|
acd97a1cb0 | ||
|
|
8c0f7a295f | ||
|
|
7536bfaaf5 | ||
|
|
cbf5d9c24e | ||
|
|
b9e474d946 | ||
|
|
3d232f97fb | ||
|
|
6d649d0889 | ||
|
|
0754837820 | ||
|
|
8f44dbb296 | ||
|
|
049cd8ecca | ||
|
|
2ed7705903 | ||
|
|
6bc6749e38 | ||
|
|
97a02b7a05 | ||
|
|
abc41a0aca | ||
|
|
31a14b2de7 | ||
|
|
59a0002c16 | ||
|
|
61020e04d9 | ||
|
|
ff1ace8342 | ||
|
|
847790c514 | ||
|
|
390a77b407 | ||
|
|
04ab9d14f8 | ||
|
|
3a32d481c4 | ||
|
|
8edbc029f8 | ||
|
|
258356e45d | ||
|
|
e1628b6448 | ||
|
|
fa32e97564 | ||
|
|
8080d57645 | ||
|
|
0a2f7b6cd4 | ||
|
|
26a2366876 | ||
|
|
2690ab3144 | ||
|
|
18b96944e9 | ||
|
|
3282f5739c | ||
|
|
50e66be0c0 | ||
|
|
593c88c8ba | ||
|
|
2f700b2d93 | ||
|
|
d1c95030f0 | ||
|
|
1ed2414241 | ||
|
|
8847483ff8 | ||
|
|
31552f85e4 | ||
|
|
125f142cf5 | ||
|
|
dc3882e47e | ||
|
|
dafbcad10e | ||
|
|
d1085ca7bd | ||
|
|
4663842946 | ||
|
|
e4262abfd7 | ||
|
|
fc6a760848 | ||
|
|
e03a580870 | ||
|
|
112e5fe0bd | ||
|
|
ae4f005a9c | ||
|
|
d1432ebb31 | ||
|
|
460cb54a7d | ||
|
|
ebf5b77f00 | ||
|
|
33904d82d0 | ||
|
|
10b3eb5a43 | ||
|
|
7ae0709895 | ||
|
|
9d6da91a5e | ||
|
|
268c351a95 | ||
|
|
8760fcea1f | ||
|
|
60a3bba165 | ||
|
|
082b3e6c42 | ||
|
|
221baa7b04 | ||
|
|
8b7b32906e | ||
|
|
261b9cc00c | ||
|
|
654e1f0855 | ||
|
|
fe14b4db35 | ||
|
|
c94ed4926f | ||
|
|
b0a37ddb04 | ||
|
|
e78e61b084 | ||
|
|
f849336435 | ||
|
|
f51e05061b | ||
|
|
b0b39e684c | ||
|
|
66af3065e3 | ||
|
|
12bac4011c | ||
|
|
02c64a54c2 | ||
|
|
9534a8ed0c | ||
|
|
8552598c6e | ||
|
|
41399b39b1 | ||
|
|
17086e51a9 | ||
|
|
4e479029a3 | ||
|
|
51faa6acb2 | ||
|
|
60a435aa82 | ||
|
|
e34fa07eb9 | ||
|
|
0520cc6ad0 | ||
|
|
c931910fc0 | ||
|
|
2bee9561d7 | ||
|
|
feb6280fd2 | ||
|
|
8f396273c7 | ||
|
|
5d584581f0 | ||
|
|
5fe9085483 | ||
|
|
c2e0c605f8 | ||
|
|
038150cff7 | ||
|
|
e19a82ef82 | ||
|
|
2aba652793 | ||
|
|
12d69e96de | ||
|
|
143bbfc4f8 | ||
|
|
5ce68458aa | ||
|
|
4494f4ee27 | ||
|
|
e550ad9156 | ||
|
|
68ed7248a1 | ||
|
|
2254c36bb4 | ||
|
|
b5789c04ec | ||
|
|
b35408c429 | ||
|
|
ac3f179321 | ||
|
|
3323418b74 | ||
|
|
22b7648b77 | ||
|
|
b5f447b20a | ||
|
|
086c235323 | ||
|
|
fbf9f7f2b1 | ||
|
|
28c4cfb55f | ||
|
|
b0363d10ed | ||
|
|
723c79b3a9 | ||
|
|
ff1c8cddb5 | ||
|
|
54553069bf | ||
|
|
bc9d31abc8 | ||
|
|
e5e848126a | ||
|
|
b0e0b9c435 | ||
|
|
6bfbeec74d | ||
|
|
745c420fed | ||
|
|
e5422d66f0 | ||
|
|
2e7ed6d9f5 | ||
|
|
d1e9f858fe | ||
|
|
0fce3c44ab | ||
|
|
3d29d45d8a | ||
|
|
725cc2b24b | ||
|
|
94e7fe61cc | ||
|
|
96c6a878c4 | ||
|
|
f650db4438 | ||
|
|
b7709f2d6c | ||
|
|
1c9d74f925 | ||
|
|
fd08a3e7d0 | ||
|
|
1829526aa7 | ||
|
|
f2d4bec138 | ||
|
|
948887c90f | ||
|
|
8e5b794b6d | ||
|
|
649b7c0857 | ||
|
|
75a9050c39 | ||
|
|
7d38b9941e | ||
|
|
d78dd42b72 | ||
|
|
38c261871a | ||
|
|
9471e0cbff | ||
|
|
b3e4fbb3de |
22
.github/dependabot.yml
vendored
Normal file
22
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# Dependabot configuration file for GitHub
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# CI workflow action updates
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "ci"
|
||||
|
||||
# Gradle package updates
|
||||
- package-ecosystem: "gradle"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "deps"
|
||||
ignore:
|
||||
- dependency-name: 'org.spigotmc:spigot-api'
|
||||
- dependency-name: 'org.papermc:paper-api'
|
||||
4
.github/funding.yml
vendored
Normal file
4
.github/funding.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Funding metadata for GitHub
|
||||
|
||||
github: WiIIiam278
|
||||
custom: https://buymeacoff.ee/william278
|
||||
96
.github/workflows/ci.yml
vendored
Normal file
96
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
name: CI Tests & Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'master' ]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'workflows/**'
|
||||
- 'README.md'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Set up JDK 21 📦'
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
- name: 'Build with Gradle 🏗️'
|
||||
uses: gradle/gradle-build-action@v3
|
||||
with:
|
||||
arguments: build test publish
|
||||
env:
|
||||
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v6
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
- name: 'Fetch Version Name 📝'
|
||||
run: |
|
||||
echo "::set-output name=VERSION_NAME::$(${{github.workspace}}/gradlew properties --no-daemon --console=plain -q | grep "^version:" | awk '{printf $2}')"
|
||||
id: fetch-version
|
||||
- name: Get Version
|
||||
run: |
|
||||
echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV
|
||||
- name: 'Publish to William278.net 🚀'
|
||||
uses: WiIIiam278/bones-publish-action@v1
|
||||
with:
|
||||
api-key: ${{ secrets.BONES_API_KEY }}
|
||||
project: 'husksync'
|
||||
channel: 'alpha'
|
||||
version: ${{ env.version_name }}
|
||||
changelog: ${{ github.event.head_commit.message }}
|
||||
distro-names: |
|
||||
paper-1.21.1
|
||||
paper-1.21.4
|
||||
paper-1.21.5
|
||||
paper-1.21.8
|
||||
paper-1.21.10
|
||||
paper-1.21.11
|
||||
fabric-1.21.1
|
||||
fabric-1.21.4
|
||||
fabric-1.21.5
|
||||
fabric-1.21.8
|
||||
distro-groups: |
|
||||
paper
|
||||
paper
|
||||
paper
|
||||
paper
|
||||
paper
|
||||
paper
|
||||
fabric
|
||||
fabric
|
||||
fabric
|
||||
fabric
|
||||
distro-descriptions: |
|
||||
Paper 1.21.1
|
||||
Paper 1.21.4
|
||||
Paper 1.21.5
|
||||
Paper 1.21.8
|
||||
Paper 1.21.10
|
||||
Paper 1.21.11
|
||||
Fabric 1.21.1
|
||||
Fabric 1.21.4
|
||||
Fabric 1.21.5
|
||||
Fabric 1.21.8
|
||||
files: |
|
||||
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.1.jar
|
||||
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.4.jar
|
||||
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.5.jar
|
||||
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.8.jar
|
||||
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.10.jar
|
||||
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.11.jar
|
||||
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.1.jar
|
||||
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.4.jar
|
||||
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.5.jar
|
||||
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.8.jar
|
||||
30
.github/workflows/pr_tests.yml
vendored
Normal file
30
.github/workflows/pr_tests.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: PR Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ 'master' ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout for CI 🛎'
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Set up JDK 21 📦'
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
- name: 'Build with Gradle 🏗️'
|
||||
uses: gradle/gradle-build-action@v3
|
||||
with:
|
||||
arguments: test
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v6
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
81
.github/workflows/release.yml
vendored
Normal file
81
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
name: Release Tests & Publish
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ 'published' ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Set up JDK 21 📦'
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
- name: 'Build with Gradle 🏗️'
|
||||
uses: gradle/gradle-build-action@v3
|
||||
with:
|
||||
arguments: build test publish
|
||||
env:
|
||||
RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
RELEASES_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v6
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
- name: 'Publish to William278.net 🚀'
|
||||
uses: WiIIiam278/bones-publish-action@v1
|
||||
with:
|
||||
api-key: ${{ secrets.BONES_API_KEY }}
|
||||
project: 'husksync'
|
||||
channel: 'release'
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
changelog: ${{ github.event.release.body }}
|
||||
distro-names: |
|
||||
paper-1.21.1
|
||||
paper-1.21.4
|
||||
paper-1.21.5
|
||||
paper-1.21.8
|
||||
paper-1.21.10
|
||||
fabric-1.21.1
|
||||
fabric-1.21.4
|
||||
fabric-1.21.5
|
||||
fabric-1.21.8
|
||||
distro-groups: |
|
||||
paper
|
||||
paper
|
||||
paper
|
||||
paper
|
||||
paper
|
||||
fabric
|
||||
fabric
|
||||
fabric
|
||||
fabric
|
||||
distro-descriptions: |
|
||||
Paper 1.21.1
|
||||
Paper 1.21.4
|
||||
Paper 1.21.5
|
||||
Paper 1.21.8
|
||||
Paper 1.21.10
|
||||
Fabric 1.21.1
|
||||
Fabric 1.21.4
|
||||
Fabric 1.21.5
|
||||
Fabric 1.21.8
|
||||
files: |
|
||||
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.1.jar
|
||||
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.4.jar
|
||||
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.5.jar
|
||||
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.8.jar
|
||||
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.10.jar
|
||||
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.1.jar
|
||||
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.4.jar
|
||||
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.5.jar
|
||||
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.8.jar
|
||||
25
.github/workflows/update_docs.yml
vendored
Normal file
25
.github/workflows/update_docs.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Update Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'master' ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'workflows/**'
|
||||
tags-ignore:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-docs:
|
||||
name: 'Update Docs'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Push Docs to Github Wiki 📄️'
|
||||
uses: Andrew-Chen-Wang/github-wiki-action@v5
|
||||
with:
|
||||
path: 'docs'
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -118,3 +118,7 @@ run/
|
||||
!gradle-wrapper.jar
|
||||
/build-output-final/
|
||||
/target/
|
||||
|
||||
# Don't include generated test suite files
|
||||
/test/servers/
|
||||
/test/HuskSync
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
JV=$(java -version 2>&1 >/dev/null | head -1)
|
||||
echo "$JV" | sed -E 's/^.*version "([^".]*)\.[^"]*".*$/\1/'
|
||||
|
||||
if [ "$JV" != 16 ]; then
|
||||
case "$1" in
|
||||
install)
|
||||
echo "installing sdkman..."
|
||||
curl -s "https://get.sdkman.io" | bash
|
||||
source ~/.sdkman/bin/sdkman-init.sh
|
||||
sdk install java 16.0.1-open
|
||||
;;
|
||||
use)
|
||||
echo "must source ~/.sdkman/bin/sdkman-init.sh"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "mvn-installing mpdbdataconverter..."
|
||||
curl "-L" "-O" "https://github.com/WiIIiam278/MPDBDataConverter/releases/download/1.0/mpdbdataconverter-1.0.jar"
|
||||
mvn "install:install-file" "-Dfile=mpdbdataconverter-1.0.jar" "-DgroupId=net.william278" "-DartifactId=mpdbdataconverter" "-Dversion=1.0" "-Dpackaging=jar" "-DgeneratePom=true" "-e"
|
||||
16
HEADER
Normal file
16
HEADER
Normal file
@@ -0,0 +1,16 @@
|
||||
This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
|
||||
Copyright (c) William278 <will27528@gmail.com>
|
||||
Copyright (c) contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
217
LICENSE
217
LICENSE
@@ -1,21 +1,202 @@
|
||||
Copyright © William278 2022. All rights reserved
|
||||
|
||||
LICENSE
|
||||
This source code is provided as reference to licensed individuals that have purchased the HuskSync
|
||||
plugin once from any of the official sources it is provided. The availability of this code does
|
||||
not grant you the rights to modify, re-distribute, compile or redistribute this source code or
|
||||
"plugin" outside this intended purpose. This license does not cover libraries developed by third
|
||||
parties that are utilised in the plugin.
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
CONTRIBUTOR AGREEMENT
|
||||
By contributing code to this repository, contributors agree that they forfeit their contributions
|
||||
to the copyright holder and only the copyright holder.
|
||||
In exchange for contributing, the copyright holder may give, at their discretion, permission to use
|
||||
the plugin in commercial contexts
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
DEFINITIONS
|
||||
"plugin"; the jar file compiled from this source code
|
||||
"source code"; the java source code and gradle configurations provided in this repository, however
|
||||
excludes libraries
|
||||
"copyright holder"; William278
|
||||
"contributor(s)"; person(s) who submit (contribute) code through a pull request to this repository
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to saveCause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must saveCause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
315
README.md
315
README.md
@@ -1,229 +1,112 @@
|
||||
[](https://github.com/WiIIiam278/HuskSync)
|
||||
# HuskSync
|
||||
[](https://discord.gg/tVYhJfyDWG)
|
||||
<!--suppress ALL -->
|
||||
<p align="center">
|
||||
<img src="images/banner.png" alt="HuskSync" />
|
||||
<a href="https://github.com/WiIIiam278/HuskSync/actions/workflows/ci.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/ci.yml?branch=master&logo=github"/>
|
||||
</a>
|
||||
<a href="https://repo.william278.net/#/releases/net/william278/husksync/">
|
||||
<img src="https://repo.william278.net/api/badge/latest/releases/net/william278/husksync/husksync-common?color=00fb9a&name=Maven&prefix=v" />
|
||||
</a>
|
||||
<a href="https://discord.gg/tVYhJfyDWG">
|
||||
<img src="https://img.shields.io/discord/818135932103557162.svg?label=&logo=discord&logoColor=fff&color=7389D8&labelColor=6A7EC2" />
|
||||
</a>
|
||||
<br/>
|
||||
<b>
|
||||
<a href="https://www.spigotmc.org/resources/husksync.97144/">Spigot</a>
|
||||
</b> —
|
||||
<b>
|
||||
<a href="https://william278.net/docs/husksync/setup">Setup</a>
|
||||
</b> —
|
||||
<b>
|
||||
<a href="https://william278.net/docs/husksync/">Docs</a>
|
||||
</b> —
|
||||
<b>
|
||||
<a href="https://github.com/WiIIiam278/HuskSync/issues">Issues</a>
|
||||
</b>
|
||||
</p>
|
||||
<br/>
|
||||
|
||||
**HuskSync** is a modern, cross-server player data synchronisation system that allows player data (inventories, health, hunger & status effects) to be synchronised across servers through the use of **Redis**.
|
||||
**HuskSync** is a modern, cross-server player data synchronization system that enables the comprehensive synchronization of your user's data across multiple proxied servers. It does this by making use of Redis and a MySQL/Mongo/PostgreSQL to optimally cache data while players change servers.
|
||||
|
||||
## Disclaimer
|
||||
This source code is provided as reference to licensed individuals that have purchased the HuskSync plugin once from any of the official sources it is provided. The availability of this code does not grant you the rights to re-distribute, compile or share this source code outside this intended purpose.
|
||||
## Features
|
||||
**⭐ Seamless synchronization** — Utilises optimised Redis caching when players change server to sync player data super quickly for a seamless experience.
|
||||
|
||||
Are you a developer? [Read below for information about code bounty licensing](#Contributing).
|
||||
**⭐ Complete player synchronization** — Sync inventories, Ender Chests, health, hunger, effects, advancements, statistics, locked maps & [more](https://william278.net/docs/husksync/sync-features)—no data left behind!
|
||||
|
||||
**⭐ Backup, restore & rotate** — Something gone wrong? Restore players back to a previous data state. Rotate and manage data snapshots in-game!
|
||||
|
||||
**⭐ Import existing data** — Import your MySQLPlayerDataBridge data—or from your existing world data! No server reset needed!
|
||||
|
||||
**⭐ Works great with Plan** — Stay in touch with your community through HuskSync analytics on your Plan web panel.
|
||||
|
||||
**⭐ Extensible API & open-source** — Need more? Extend the plugin with the Developer API. Or, submit a pull request through our code bounty system!
|
||||
|
||||
**Ready?** [It's syncing time!](https://william278.net/docs/husksync/setup)
|
||||
|
||||
## Compatibility
|
||||
HuskSync supports the following [compatible versions](https://william278.net/docs/husksync/compatibility) of Minecraft. Since v3.7, you must download the correct version of HuskSync for your server:
|
||||
|
||||
| Minecraft | Latest HuskSync | Java Version | Platforms | Support Status |
|
||||
|:---------------:|:---------------:|:------------:|:--------------|:------------------------------|
|
||||
| 1.21.10 | _latest_ | 21 | Paper | ✅ **Active Release** |
|
||||
| 1.21.7/8 | _latest_ | 21 | Paper, Fabric | ✅ **August 2026** |
|
||||
| 1.21.6 | 3.8.5 | 21 | Paper | 🗃️ Archived (July 2025) |
|
||||
| 1.21.5 | _latest_ | 21 | Paper | ✅ **February 2026** (Non-LTS) |
|
||||
| 1.21.4 | _latest_ | 21 | Paper, Fabric | ✅ **February 2026** (Non-LTS) |
|
||||
| 1.21.3 | 3.7.1 | 21 | Paper, Fabric | 🗃️ Archived (December 2024) |
|
||||
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **May 2026** (LTS) |
|
||||
| 1.20.6 | 3.6.8 | 17 | Paper | 🗃️ Archived (October 2024) |
|
||||
| 1.20.4 | 3.6.8 | 17 | Paper | 🗃️ Archived (July 2024) |
|
||||
| 1.20.1 | 3.8.7 | 17 | Paper, Fabric | 🗃️ Archived (November 2024) |
|
||||
| 1.17.1 - 1.19.4 | 3.6.8 | 17 | Paper | 🗃️ Archived |
|
||||
| 1.16.5 | 3.2.1 | 16 | Paper | 🗃️ Archived |
|
||||
|
||||
HuskSync is primarily developed against the latest release. Old Minecraft versions are allocated a support channel based on popularity, mod support, etc:
|
||||
|
||||
* Long Term Support (LTS) – Supported for up to 12-18 months
|
||||
* Non-Long Term Support (Non-LTS) – Supported for 3-6 months
|
||||
|
||||
Verify your purchase on Discord and [Download HuskSync](https://william278.net/project/husksync/download) for your server.
|
||||
|
||||
## Setup
|
||||
### Requirements
|
||||
* A BungeeCord or Velocity-based proxy server
|
||||
* A Spigot-based game server
|
||||
* A Redis server
|
||||
Requires a [MySQL/MariaDB/Mongo/PostgreSQL database](https://william278.net/docs/husksync/database), a [Redis (v5.0+) server]((https://william278.net/docs/husksync/redis)) and a network of [compatible Spigot or Fabric Minecraft servers](https://william278.net/docs/husksync/compatibility).
|
||||
|
||||
### Installation
|
||||
1. Install HuskSync in the `/plugins/` folder of both your Spigot and Proxy servers.
|
||||
2. Start your servers, then stop them again to allow the configuration files to generate.
|
||||
3. Navigate to the generated `config.yml` files on your Spigot server and Proxy (located in `/plugins/HuskSync/`) and fill in the credentials of your redis server.
|
||||
1. On the Proxy server, you can additionally configure a MySQL database to save player data in, as by default the plugin will create a SQLite database.
|
||||
3. By default, everything except player locations are synchronised. If you would like to change what gets synchronised, you can do this by editing the `config.yml` files of each Spigot server.
|
||||
4. Once you have finished setting everything up, make sure to restart all of your servers and proxy server. Then, log in and data should be synchronised!
|
||||
1. Place the plugin jar file in the `/plugins` or `/mods` directory of each Spigot/Fabric server. You do not need to install HuskSync as a proxy plugin.
|
||||
2. Start, then stop every server to let HuskSync generate the config file.
|
||||
3. Navigate to the HuskSync config file on each server and fill in both your database and Redis server credentials.
|
||||
4. Start every server again and synchronization will begin.
|
||||
|
||||
### Migration from MySQLPlayerDataBridge
|
||||
HuskSync supports the migration of player data from [MySQLPlayerDataBridge](https://www.spigotmc.org/resources/mysql-player-data-bridge.8117/). Please note that HuskSync is not compatible with MySQLPlayerInventoryBridge, as that has a different system for data handling.
|
||||
## Development
|
||||
To build HuskSync, simply run the following in the root of the repository (building requires Java 21). Builds will be output in `/target`:
|
||||
|
||||
To migrate from MySQLPLayerDataBridge, you need a Proxy server with HuskSync installed and one Spigot server with both HuskSync and MySQLPlayerDataBridge installed. To migrate:
|
||||
1. Make sure HuskSync is set up correctly on the Proxy and Spigot server, making sure that the two are able to communicate with Redis (it will display a handshake confirmation message in both consoles when communications have been established)
|
||||
2. Make sure your database is configured correctly on your Proxy server. For example, if you would like to change from SQLite to MySQL, you should do this now because the data from MySQLPlayerDataBridge will be moved into it.
|
||||
3. Make sure no players are online, then in the Proxy server's console run `husksync migrate`
|
||||
4. Follow the steps in the Migration wizard to ensure the connection credentials and details of the database containing your MySQLPlayerDataBridge are correct, changing settings with `husksync migrate setting <setting> <new value>` as necessary.
|
||||
5. Run `husksync migrate start` in the Proxy server's console to start the migration. This could take some time, depending on the amount of data that needs migrating and the speed of your database/server. When the migration is complete, it will display a "Migration complete" message.
|
||||
|
||||
### Troubleshooting
|
||||
#### Commands do not function
|
||||
Please check that the plugin is installed and enabled on both the proxy and bukkit server you are trying to execute the command from and that both plugins connected to Redis. (A connection handshake confirmation message is logged to console when communications are successfully established.)
|
||||
|
||||
#### Data not being synced on player join and SQL errors in proxy console
|
||||
This issue frequently occurs in users running Cracked (illegal) servers. I do not support piracy and so will be limited in my ability to help you.
|
||||
If you are running an offline server for a legitimate reason, however, make sure that in the `paper.yml` of your Bukkit servers `bungee-online-mode` is set to the correct value - and that both your Proxy (BungeeCord, Waterfall, etc.) server and Bukkit (Spigot, paper, etc.) servers are set up correctly to work with offline mode.
|
||||
|
||||
#### Data sometimes not syncing between servers
|
||||
There are two primary reasons this may happen:
|
||||
* On your proxy server, you are running _FlameCord_ or a similar fork of Waterfall. Due to the nature of these forks changing security parameters, they can block or interfere with Redis packets being sent to and from your server. FlameCord, XCord and other forks are not compatible with HuskSync. For security-conscious users, I recommend Velocity.
|
||||
* Your backend servers/proxy and Redis server have noticeably different amounts of latency between each other. This is particularly relevant for users running across multiple machines, where some backend servers / the proxy are installed with Redis and other backend servers are on a different machine. The solution to this is to have your BungeeCord and Redis alone on one machine, and your backend servers across the others - or have a separate machine with equal latency to the others that has Redis on. In the future, I may have a look at automatically correcting and accounting for differences in latency.
|
||||
|
||||
## How it works
|
||||

|
||||
HuskSync saves a player's data when they log out to a cache on your proxy server, and redistributes that data to players when they join another HuskSync-enabled server. Player data in the cache is then saved to a database (be it SQLite or MySQL) and this is loaded from when a player joins your network.
|
||||
|
||||
To facilitate the transfer of data between servers, HuskSync serializes player data and then makes use of Redis to communicate between the Proxy and Spigot servers.
|
||||
|
||||
### What is synchronised
|
||||
Everything except player locations are synchronised by default. You can enable or disable what data is loaded on a server by modifying these values in the `/plugins/HuskSync/config.yml` file on each Spigot server.
|
||||
* Player inventory
|
||||
* Player armour and off-hand
|
||||
* Player currently selected hotbar slot
|
||||
* Player ender chest
|
||||
* Player experience points & levels
|
||||
* Player health
|
||||
* Player max health
|
||||
* Player health scale
|
||||
* Player hunger
|
||||
* Player saturation
|
||||
* Player exhaustion
|
||||
* Player game mode
|
||||
* Player advancements
|
||||
* Player statistics (ESC → Statistics menu)
|
||||
* Player location
|
||||
* Player flight status
|
||||
|
||||
### Commands
|
||||
Commands are handled by the proxy server, rather than each spigot server. Some will only work on Spigot servers with HuskSync installed. Please remember that you will need a Proxy permission plugin (e.g. LuckPermsBungee) to set permissions for proxy commands.
|
||||
|
||||
| Command | Description | Permission |
|
||||
|---------------------------------------|--------------------------------------|--------------------------------|
|
||||
| `/husksync about` | View plugin information | _None_ |
|
||||
| `/husksync update` | Check if an update is available | `husksync.command.admin` |
|
||||
| `/husksync status` | View system status information | `husksync.command.admin` |
|
||||
| `/husksync reload` | Reload config & message files | `husksync.command.admin` |
|
||||
| `/husksync invsee <player> [cluster]` | View an offline player's inventory | `husksync.command.inventory` |
|
||||
| `/husksync echest <player> [cluster]` | View an offline player's ender chest | `husksync.command.ender_chest` |
|
||||
| `/husksync migrate [args] ` | Migrate data from MPDB | _Console-only_ |
|
||||
|
||||
### Frequently Asked Questions (FAQs)
|
||||
#### Is Redis required?
|
||||
Yes. Redis is both free, easy to install and multiplatform, though. Pterodactyl users can also run it in an egg with relatively low overheads.
|
||||
|
||||
#### What is Redis?
|
||||
Redis is server software that acts as an in-memory data store. Minecraft server software typically makes use of its function to send messages efficiently.
|
||||
|
||||
#### Is Economy / Vault synchronization supported?
|
||||
No.
|
||||
|
||||
Synchronising economy data like MySQLPlayerDataBridge does causes a number of issues and incompatibilities that mean that MySQLPlayerDataBridge has had to add integrations with a number of plugins just to make them work. This leads to poor compatibility and more bugs as plugins change their APIs and systems. In the case of HuskSync, this would require both plugin authors and myself to manually support each other, which would inevitably increase update times, lead to a bottomless pit of "add support for this plugin" requests and these integrations would then inevitably break when authors decide to update their plugins, requiring me to update manually.
|
||||
|
||||
I strongly recommend making use of economy plugins that provide built-in support for cross-server synchronisation instead, which do not have the same issues. I have personally used [XConomy](https://www.spigotmc.org/resources/xconomy.75669/) in the past and reccommend it.
|
||||
|
||||
#### Will this work on servers running multiple proxies?
|
||||
Short answer: Not right now, but improved support for this is planned in the future.
|
||||
|
||||
Long answer: This is a difficult question to unpack because of the wide variety of setups that involve multiple proxies, however currently the architecture of how messages are sent between servers assumes that one proxy will serve multiple Bukkit servers, so having multiple proxies will lead to data going out of sync, among other issues.
|
||||
|
||||
#### Does it work with Velocity?
|
||||
Yes! Servers running the Velocity proxy software are supported as of HuskSync 1.2+.
|
||||
|
||||
#### Is this faster than MySqlPlayerDataBridge (MPDB)?
|
||||
It's difficult to say, and will depend on your server.
|
||||
|
||||
MPDB stores data in a MySQL database (hence the name) and operates by querying a database for said data when a player joins a Bukkit server.
|
||||
HuskSync stores player data in a central cache on the Proxy server and servers request data from said cache; data is only queried from the database when a player joins the network, not when switching servers within it.
|
||||
|
||||
HuskSync should operate faster in theory, then, as it does not need to query large amounts of data from a database file as often. However, any performance enhancements you might see will heavily depend on the speed of your existing database and your server hardware.
|
||||
|
||||
#### Are modded items supported?
|
||||
Most likely not - and I cannot support it - but feel free to test it, as depending on the implementation of your modding API it may work just fine.
|
||||
|
||||
## Developers
|
||||
### API
|
||||
HuskSync has an API for Bukkit providing events that fire when synchronisation takes place as well as a method to access and deserialize player data on demand. There is no API for the proxy side currently.
|
||||
|
||||
HuskSync's API is available on [JitPack](https://jitpack.io/#WiIIiam278/HuskSync/Tag). You can view the [HuskSync JavaDocs here](https://javadoc.jitpack.io/com/github/WiIIiam278/HuskSync/latest/javadoc/index.html). You should only use stuff in the `husksync.bukkit.api` and `husksync.bukkit.data` packages (as well as the PlayerData class located in the `husksync` root package.
|
||||
|
||||
#### Including the API in your project
|
||||
With Maven, add the repository to your pom.xml:
|
||||
```xml
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>jitpack.io</id>
|
||||
<url>https://jitpack.io</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
```
|
||||
Then, add the dependency. Replace `version` with the latest version of HuskSync: [](https://jitpack.io/#WiIIiam278/HuskSync)
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>net.william278</groupId>
|
||||
<artifactId>HuskSync</artifactId>
|
||||
<version>version</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Or, with Gradle, add the dependency like so to your build.gradle:
|
||||
```
|
||||
allprojects {
|
||||
repositories {
|
||||
...
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
```
|
||||
Then add the dependency as follows. Replace `version` with the latest version of HuskSync: [](https://jitpack.io/#net.william278/HuskSync)
|
||||
```
|
||||
dependencies {
|
||||
compileOnly 'net.william278:HuskSync:version'
|
||||
}
|
||||
```
|
||||
|
||||
#### API Events
|
||||
* **SyncCompleteEvent** - Fires when a player's data has finished synchronising. Use #getData to get the PlayerData being set.
|
||||
* **SyncEvent** - Fires just before a player's data is synchronised. Can be cancelled. Use #getData to get the PlayerData being set, and #setData to set it.
|
||||
|
||||
#### Fetching player data on demand
|
||||
To fetch PlayerData from a UUID as you need it, create an instance of the HuskSyncAPI class and use the `#getPlayerData` method. Note that data returned in this method is only the data from the central cache. That is to say, if the player is online, the data returned in this way will not necessarily be the same as the player's actual current data.
|
||||
```java
|
||||
HuskSyncAPI huskSyncApi = HuskSyncAPI.getInstance();
|
||||
try {
|
||||
CompletableFuture<PlayerData> playerDataCompletableFuture = huskSyncApi.getPlayerData(playerUUID);
|
||||
// thenAccept blocks the thread until HuskSync has grabbed the data, so you may wish to run this asynchronously (e.g. Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {});.
|
||||
playerDataCompletableFuture.thenAccept(playerData -> {
|
||||
// You now have a PlayerData object which you can get serialized data from and deserialize with the DataSerializer static methods
|
||||
});
|
||||
} catch (IOException e) {
|
||||
Bukkit.getLogger().severe("An error occurred fetching player data!");
|
||||
}
|
||||
```
|
||||
|
||||
#### Getting ItemStacks and usable data from PlayerData
|
||||
Use the static methods provided in the [DataSerializer class](https://javadoc.jitpack.io/net.william278/HuskSync/latest/javadoc/net/william278/husksync/bukkit/data/DataSerializer.html). For instance, to get a player's inventory as an `ItemStack[]` from a `PlayerData` object.
|
||||
```java
|
||||
ItemStack[] inventoryItems = DataSerializer.serializeInventory(playerData.getSerializedInventory());
|
||||
ItemStack[] enderChestItems = DataSerializer.serializeInventory(playerData.getSerializedEnderChest());
|
||||
```
|
||||
|
||||
#### Updating PlayerData
|
||||
You can then update PlayerData back to the central cache using the `HuskSyncAPI#updatePlayerData(playerData)` method. For example:
|
||||
```java
|
||||
// Update a value in the player data object
|
||||
playerData.setHealth(20);
|
||||
try {
|
||||
// Update the player data to the cache
|
||||
huskSyncApi.updatePlayerData(playerData);
|
||||
} catch (IOException e) {
|
||||
Bukkit.getLogger().severe("An error occurred updating player data!");
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Contributing
|
||||
A code bounty program is in place for HuskSync, where developers making significant code contributions to HuskSync may be entitled to a discretionary license to use HuskSync in commercial contexts without having to purchase the resource, so please feel free to submit pull requests with improvements, fixes and features!
|
||||
|
||||
### Translation
|
||||
While the code bounty program is not available for translation contributors, they are still strongly appreciated in making the plugin more accessible. If you'd like to contribute translated message strings for your language, you can submit a Pull Request that creates a .yml file in `bungeecord/src/main/resources/languages` with the correct translations.
|
||||
|
||||
### Building
|
||||
You can build HuskSync yourself, though please read the license and buy yourself a copy as HuskSync is indeed a premium resource.
|
||||
|
||||
To build HuskSync, you'll need to get the [MPDBConverter](https://github.com/WiIIiam278/MPDBDataConverter) library, either by authenticating through GitHub packages or by downloading and running `mvn install-file` to publish it to your local maven repository.
|
||||
|
||||
Then, to build the plugin, run the following in the root of the repository:
|
||||
```
|
||||
```bash
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
## bStats
|
||||
This plugin uses bStats to provide me with metrics about its usage:
|
||||
* [View Bukkit metrics](https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140)
|
||||
* [View BungeeCord metrics](https://bstats.org/plugin/bungeecord/HuskSync%20-%20BungeeCord/13141)
|
||||
* [View Velocity metrics](https://bstats.org/plugin/velocity/HuskSync%20-%20Velocity/13489)
|
||||
HuskSync uses `essential-multi-version` (Fabric) and `preprocessor` (Bukkit) to target multiple versions of Minecraft in one codebase - [check here](https://github.com/WiIIiam278/PreProcessor?tab=readme-ov-file#code-example) for a preprocessor comment logic reference.
|
||||
|
||||
You can turn metric collection off by navigating to `~/plugins/bStats/config.yml` and editing the config to disable plugin metrics.
|
||||
### License
|
||||
HuskSync is licensed under the Apache 2.0 license.
|
||||
|
||||
## Support
|
||||
* Report bugs: [Click here](https://github.com/WiIIiam278/HuskSync/issues)
|
||||
* Discord support: Join the [HuskHelp Discord](https://discord.gg/tVYhJfyDWG)!
|
||||
* Proof of purchase is required for support.
|
||||
- [License](https://github.com/WiIIiam278/HuskSync/blob/master/LICENSE)
|
||||
|
||||
Contributions to the project are welcome—feel free to open a pull request with new features, improvements and/or fixes!
|
||||
|
||||
### Support
|
||||
Due to its complexity, official binaries and customer support for HuskSync is provided through a paid model. This means that support is only available to users who have purchased a license to the plugin from Spigot, Polymart, or BuiltByBit and have provided proof of purchase. Please join our Discord server if you have done so and need help!
|
||||
|
||||
### Translations
|
||||
Translations of the plugin locales are welcome to help make the plugin more accessible. Please submit a pull request with your translations as a `.yml` file.
|
||||
|
||||
- [Locales Directory](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/locales)
|
||||
- [English Locales](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/locales/en-gb.yml)
|
||||
|
||||
## Links
|
||||
- [Docs](https://william278.net/docs/husksync/) — Read the plugin documentation!
|
||||
- [Spigot](https://www.spigotmc.org/resources/husksync.97144/) — View the Spigot resource page (Also: [Polymart](https://polymart.org/resource/husksync.1634), [Craftaro](https://craftaro.com/marketplace/product/husksync.758), [BuiltByBit](https://builtbybit.com/resources/husksync.34956/))
|
||||
- [Issues](https://github.com/WiIIiam278/HuskSync/issues) — File a bug report or feature request
|
||||
- [Discord](https://discord.gg/tVYhJfyDWG) — Get help, ask questions (Purchase required)
|
||||
- [bStats](https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140) — View plugin metrics
|
||||
|
||||
---
|
||||
© [William278](https://william278.net/), 2025. Licensed under the Apache-2.0 License.
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
dependencies {
|
||||
compileOnly project(path: ':common')
|
||||
implementation project(path: ':bukkit')
|
||||
|
||||
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
|
||||
compileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.apache', 'net.william278.husksync.libraries'
|
||||
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
|
||||
}
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package net.william278.husksync.bukkit.api;
|
||||
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.listener.BukkitRedisListener;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* HuskSync's API. To access methods, use the {@link #getInstance()} entrypoint.
|
||||
*
|
||||
* @author William
|
||||
*/
|
||||
public class HuskSyncAPI {
|
||||
|
||||
private HuskSyncAPI() {
|
||||
}
|
||||
|
||||
private static HuskSyncAPI instance;
|
||||
|
||||
/**
|
||||
* The API entry point. Returns an instance of the {@link HuskSyncAPI}
|
||||
*
|
||||
* @return instance of the {@link HuskSyncAPI}
|
||||
*/
|
||||
public static HuskSyncAPI getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new HuskSyncAPI();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link CompletableFuture} that will fetch the {@link PlayerData} for a user given their {@link UUID},
|
||||
* which contains serialized synchronised data.
|
||||
* <p>
|
||||
* This can then be deserialized into ItemStacks and other usable values using the {@code DataSerializer} class.
|
||||
* <p>
|
||||
* If no data could be returned, such as if an invalid UUID is specified, the CompletableFuture will be cancelled.
|
||||
*
|
||||
* @param playerUUID The {@link UUID} of the player to get data for
|
||||
* @return a {@link CompletableFuture} with the user's {@link PlayerData} accessible on completion
|
||||
* @throws IOException If an exception occurs with serializing during processing of the request
|
||||
* @apiNote This only returns the latest saved and cached data of the user. This is <b>not</b> necessarily the current state of their inventory if they are online.
|
||||
*/
|
||||
public CompletableFuture<PlayerData> getPlayerData(UUID playerUUID) throws IOException {
|
||||
// Create the request to be completed
|
||||
final UUID requestUUID = UUID.randomUUID();
|
||||
BukkitRedisListener.apiRequests.put(requestUUID, new CompletableFuture<>());
|
||||
|
||||
// Remove the request from the map on completion
|
||||
BukkitRedisListener.apiRequests.get(requestUUID).whenComplete((playerData, throwable) -> BukkitRedisListener.apiRequests.remove(requestUUID));
|
||||
|
||||
// Request the data via the proxy
|
||||
new RedisMessage(RedisMessage.MessageType.API_DATA_REQUEST,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
playerUUID.toString(), requestUUID.toString()).send();
|
||||
|
||||
return BukkitRedisListener.apiRequests.get(requestUUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a player's {@link PlayerData} to the proxy cache and database.
|
||||
* <p>
|
||||
* If the player is online on the Proxy network, they will be updated and overwritten with this data.
|
||||
*
|
||||
* @param playerData The {@link PlayerData} (which contains the {@link UUID}) of the player data to update to the central cache and database
|
||||
* @throws IOException If an exception occurs with serializing during processing of the update
|
||||
*/
|
||||
public void updatePlayerData(PlayerData playerData) throws IOException {
|
||||
// Serialize and send the updated player data
|
||||
final String serializedPlayerData = RedisMessage.serialize(playerData);
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
serializedPlayerData, Boolean.toString(true)).send();
|
||||
}
|
||||
|
||||
}
|
||||
217
build.gradle
217
build.gradle
@@ -1,72 +1,233 @@
|
||||
import org.apache.tools.ant.filters.ReplaceTokens
|
||||
|
||||
plugins {
|
||||
id 'com.github.johnrengelman.shadow' version '7.1.0'
|
||||
id 'org.ajoberstar.grgit' version '4.1.1'
|
||||
id 'com.gradleup.shadow' version '9.2.2'
|
||||
id 'org.cadixdev.licenser' version '0.6.1' apply false
|
||||
id 'dev.architectury.loom' version '1.9-SNAPSHOT' apply false
|
||||
id 'gg.essential.multi-version.root' apply false
|
||||
id 'org.ajoberstar.grgit' version '5.3.2'
|
||||
id 'maven-publish'
|
||||
id 'java'
|
||||
}
|
||||
|
||||
group 'net.william278'
|
||||
version "$ext.plugin_version+${versionMetadata()}"
|
||||
version "$ext.plugin_version${versionMetadata()}"
|
||||
description "$ext.plugin_description"
|
||||
defaultTasks 'licenseFormat', 'build'
|
||||
|
||||
ext {
|
||||
set 'version', version.toString()
|
||||
set 'description', description.toString()
|
||||
|
||||
set 'jedis_version', jedis_version.toString()
|
||||
set 'mysql_driver_version', mysql_driver_version.toString()
|
||||
set 'mariadb_driver_version', mariadb_driver_version.toString()
|
||||
set 'postgres_driver_version', postgres_driver_version.toString()
|
||||
set 'mongodb_driver_version', mongodb_driver_version.toString()
|
||||
set 'snappy_version', snappy_version.toString()
|
||||
}
|
||||
|
||||
import org.apache.tools.ant.filters.ReplaceTokens
|
||||
publishing {
|
||||
repositories {
|
||||
if (System.getenv("RELEASES_MAVEN_USERNAME") != null) {
|
||||
maven {
|
||||
name = "william278-releases"
|
||||
url = "https://repo.william278.net/releases"
|
||||
credentials {
|
||||
username = System.getenv("RELEASES_MAVEN_USERNAME")
|
||||
password = System.getenv("RELEASES_MAVEN_PASSWORD")
|
||||
}
|
||||
authentication {
|
||||
basic(BasicAuthentication)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (System.getenv("SNAPSHOTS_MAVEN_USERNAME") != null) {
|
||||
maven {
|
||||
name = "william278-snapshots"
|
||||
url = "https://repo.william278.net/snapshots"
|
||||
credentials {
|
||||
username = System.getenv("SNAPSHOTS_MAVEN_USERNAME")
|
||||
password = System.getenv("SNAPSHOTS_MAVEN_PASSWORD")
|
||||
}
|
||||
authentication {
|
||||
basic(BasicAuthentication)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
apply plugin: 'com.github.johnrengelman.shadow'
|
||||
// Ignore parent projects (no jars)
|
||||
if (project.name == 'fabric' || project.name == 'bukkit') {
|
||||
return
|
||||
}
|
||||
|
||||
apply plugin: 'com.gradleup.shadow'
|
||||
apply plugin: 'org.cadixdev.licenser'
|
||||
apply plugin: 'java'
|
||||
|
||||
compileJava.options.encoding = 'UTF-8'
|
||||
compileJava.options.release.set 17
|
||||
javadoc.options.encoding = 'UTF-8'
|
||||
javadoc.options.addStringOption('Xdoclint:none', '-quiet')
|
||||
|
||||
compileJava.options.release.set 16
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven { url 'https://repo.william278.net/releases/' }
|
||||
maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
|
||||
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
|
||||
maven { url 'https://repo.velocitypowered.com/snapshots/' }
|
||||
maven { url 'https://repo.papermc.io/repository/maven-public/' }
|
||||
maven { url 'https://repo.codemc.io/repository/maven-public/' }
|
||||
maven { url 'https://repo.minebench.de/' }
|
||||
maven { url 'https://repo.codemc.org/repository/maven-public' }
|
||||
maven { url 'https://repo.alessiodp.com/releases/' }
|
||||
maven { url 'https://jitpack.io' }
|
||||
maven { url 'https://mvn-repo.arim.space/lesser-gpl3/' }
|
||||
maven { url 'https://libraries.minecraft.net/' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation('redis.clients:jedis:4.2.3') {
|
||||
//noinspection GroovyAssignabilityCheck
|
||||
exclude module: 'slf4j-api'
|
||||
}
|
||||
testImplementation(platform("org.junit:junit-bom:6.0.1"))
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
testCompileOnly 'org.jetbrains:annotations:26.0.2-1'
|
||||
}
|
||||
|
||||
license {
|
||||
header = rootProject.file('HEADER')
|
||||
include '**/*.java'
|
||||
newLine = true
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
processResources {
|
||||
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
|
||||
tokens: rootProject.ext.properties
|
||||
def tokenMap = rootProject.ext.properties
|
||||
tokenMap.merge("grgit", '', (s, s2) -> s)
|
||||
filesMatching(['**/*.json', '**/*.yml']) {
|
||||
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
|
||||
tokens: tokenMap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
version rootProject.version
|
||||
archivesBaseName = "${rootProject.name}-${project.name.capitalize()}"
|
||||
|
||||
if (['bukkit', 'api', 'bungeecord', 'velocity', 'plugin'].contains(project.name)) {
|
||||
shadowJar {
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
archiveClassifier.set('')
|
||||
}
|
||||
jar.dependsOn shadowJar
|
||||
clean.delete "$rootDir/target"
|
||||
// Ignore parent projects (no jars)
|
||||
if (['fabric', 'bukkit'].contains(project.name)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Project naming
|
||||
version rootProject.version
|
||||
def name = "$rootProject.name"
|
||||
if (rootProject != project.parent) {
|
||||
name += "-${project.parent.name.capitalize()}"
|
||||
} else {
|
||||
name += "-${project.name.capitalize()}"
|
||||
}
|
||||
archivesBaseName = name
|
||||
|
||||
// Version-specific configuration
|
||||
if (['fabric', 'bukkit'].contains(project.parent?.name)) {
|
||||
compileJava.options.release.set 21
|
||||
version += "+mc.${project.name}"
|
||||
|
||||
if (project.parent?.name?.equals('fabric')) {
|
||||
apply plugin: 'dev.architectury.loom'
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
from '../LICENSE'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
archiveClassifier.set('')
|
||||
}
|
||||
|
||||
// API publishing
|
||||
if (project.name == 'common' || ['fabric', 'bukkit'].contains(project.parent?.name)) {
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
sourcesJar {
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
}
|
||||
javadocJar {
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
}
|
||||
shadowJar.dependsOn(sourcesJar, javadocJar)
|
||||
|
||||
publishing {
|
||||
if (['common'].contains(project.name)) {
|
||||
publications {
|
||||
mavenJavaCommon(MavenPublication) {
|
||||
groupId = 'net.william278.husksync'
|
||||
artifactId = 'husksync-common'
|
||||
version = "$rootProject.version"
|
||||
artifact shadowJar
|
||||
artifact sourcesJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (project.parent?.name?.equals('bukkit')) {
|
||||
publications {
|
||||
"mavenJavaBukkit_${project.name.replace('.', '_')}"(MavenPublication) {
|
||||
groupId = 'net.william278.husksync'
|
||||
artifactId = 'husksync-bukkit'
|
||||
version = "$rootProject.version+$project.name"
|
||||
artifact shadowJar
|
||||
artifact sourcesJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (project.parent?.name?.equals('fabric')) {
|
||||
publications {
|
||||
"mavenJavaFabric_${project.name.replace('.', '_')}"(MavenPublication) {
|
||||
groupId = 'net.william278.husksync'
|
||||
artifactId = 'husksync-fabric'
|
||||
version = "$rootProject.version+$project.name"
|
||||
artifact remapJar
|
||||
artifact sourcesJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jar.dependsOn shadowJar
|
||||
clean.delete "$rootDir/target"
|
||||
}
|
||||
|
||||
logger.lifecycle("Building HuskSync ${version} by William278")
|
||||
|
||||
@SuppressWarnings('GrMethodMayBeStatic')
|
||||
def versionMetadata() {
|
||||
// Require grgit
|
||||
if (grgit == null) {
|
||||
return System.getenv("GITHUB_RUN_NUMBER") ? 'build.' + System.getenv("GITHUB_RUN_NUMBER") : 'unknown'
|
||||
return '-unknown'
|
||||
}
|
||||
return 'rev.' + grgit.head().abbreviatedId + (grgit.status().clean ? '' : '-indev')
|
||||
}
|
||||
|
||||
// If unclean, return the last commit hash with -indev
|
||||
if (!grgit.status().clean) {
|
||||
return '-' + grgit.head().abbreviatedId + '-indev'
|
||||
}
|
||||
|
||||
// Otherwise if this matches a tag, return nothing
|
||||
def tag = grgit.tag.list().find { it.commit.id == grgit.head().id }
|
||||
if (tag != null) {
|
||||
return ''
|
||||
}
|
||||
return '-' + grgit.head().abbreviatedId
|
||||
}
|
||||
|
||||
4
bukkit/1.21.1/gradle.properties
Normal file
4
bukkit/1.21.1/gradle.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
minecraft_version_range=1.21.1
|
||||
minecraft_version_numeric=12101
|
||||
minecraft_api_version=1.21
|
||||
paper_api_version=1.21.1-R0.1-SNAPSHOT
|
||||
4
bukkit/1.21.10/gradle.properties
Normal file
4
bukkit/1.21.10/gradle.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
minecraft_version_range=>=1.21.9 <=1.21.10
|
||||
minecraft_version_numeric=12110
|
||||
minecraft_api_version=1.21
|
||||
paper_api_version=1.21.10-R0.1-SNAPSHOT
|
||||
4
bukkit/1.21.11/gradle.properties
Normal file
4
bukkit/1.21.11/gradle.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
minecraft_version_range=>=1.21.11 <=1.21.11
|
||||
minecraft_version_numeric=12111
|
||||
minecraft_api_version=1.21
|
||||
paper_api_version=1.21.11-R0.1-SNAPSHOT
|
||||
4
bukkit/1.21.4/gradle.properties
Normal file
4
bukkit/1.21.4/gradle.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
minecraft_version_range=1.21.4
|
||||
minecraft_version_numeric=12104
|
||||
minecraft_api_version=1.21
|
||||
paper_api_version=1.21.4-R0.1-SNAPSHOT
|
||||
4
bukkit/1.21.5/gradle.properties
Normal file
4
bukkit/1.21.5/gradle.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
minecraft_version_range=1.21.5
|
||||
minecraft_version_numeric=12105
|
||||
minecraft_api_version=1.21
|
||||
paper_api_version=1.21.5-R0.1-SNAPSHOT
|
||||
4
bukkit/1.21.8/gradle.properties
Normal file
4
bukkit/1.21.8/gradle.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
minecraft_version_range=>=1.21.7 <=1.21.8
|
||||
minecraft_version_numeric=12108
|
||||
minecraft_api_version=1.21
|
||||
paper_api_version=1.21.8-R0.1-SNAPSHOT
|
||||
@@ -1,18 +1,102 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'net.william278.preprocessor' version '1.0'
|
||||
id 'xyz.jpenilla.run-paper' version '2.3.1'
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(path: ':common')
|
||||
|
||||
implementation 'org.bstats:bstats-bukkit:3.0.0'
|
||||
implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
|
||||
implementation 'net.william278:mpdbdataconverter:1.0'
|
||||
implementation 'net.william278.uniform:uniform-bukkit:1.3.9'
|
||||
implementation 'net.william278.uniform:uniform-paper:1.3.9'
|
||||
implementation 'net.william278.toilet:toilet-bukkit:1.0.16'
|
||||
implementation 'net.william278:mpdbdataconverter:1.0.1'
|
||||
implementation 'net.william278:hsldataconverter:1.0'
|
||||
implementation 'net.william278:mapdataapi:2.0'
|
||||
implementation 'org.bstats:bstats-bukkit:3.1.0'
|
||||
implementation 'net.kyori:adventure-platform-bukkit:4.4.1'
|
||||
implementation 'dev.triumphteam:triumph-gui:3.1.12'
|
||||
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
|
||||
implementation 'de.tr7zw:item-nbt-api:2.15.5'
|
||||
|
||||
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
|
||||
compileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
compileOnly "io.papermc.paper:paper-api:${paper_api_version}"
|
||||
compileOnly 'com.github.retrooper:packetevents-spigot:2.10.1'
|
||||
compileOnly 'com.github.dmulloy2:ProtocolLib:5.3.0'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.42'
|
||||
compileOnly 'commons-io:commons-io:2.21.0'
|
||||
compileOnly 'org.json:json:20250517'
|
||||
compileOnly 'net.william278:minedown:1.8.2'
|
||||
compileOnly 'de.exlll:configlib-yaml:4.6.4'
|
||||
compileOnly 'com.zaxxer:HikariCP:7.0.2'
|
||||
compileOnly 'net.william278:DesertWell:2.0.4'
|
||||
compileOnly 'net.william278:AdvancementAPI:97a9583413'
|
||||
compileOnly "redis.clients:jedis:$jedis_version"
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.42'
|
||||
}
|
||||
|
||||
processResources {
|
||||
filesMatching(['**/*.json', '**/*.yml']) {
|
||||
expand([
|
||||
version: version,
|
||||
paper_api_version: paper_api_version,
|
||||
minecraft_version: project.name,
|
||||
minecraft_version_range: minecraft_version_range,
|
||||
minecraft_api_version: minecraft_api_version
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
java.srcDirs '../src/main/java'
|
||||
resources.srcDirs '../src/main/resources'
|
||||
}
|
||||
javadoc.setSource('./build/generated/preprocessed/main/java')
|
||||
|
||||
preprocess {
|
||||
vars.put('MC', minecraft_version_numeric)
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
dependencies {
|
||||
exclude(dependency('com.mojang:brigadier'))
|
||||
}
|
||||
|
||||
relocate 'org.apache.commons.io', 'net.william278.husksync.libraries.commons.io'
|
||||
relocate 'org.apache.commons.text', 'net.william278.husksync.libraries.commons.text'
|
||||
relocate 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3'
|
||||
relocate 'com.google.gson', 'net.william278.husksync.libraries.gson'
|
||||
relocate 'com.fatboyindustrial', 'net.william278.husksync.libraries'
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.apache', 'net.william278.husksync.libraries'
|
||||
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'de.exlll', 'net.william278.husksync.libraries'
|
||||
relocate 'net.william278.uniform', 'net.william278.husksync.libraries.uniform'
|
||||
relocate 'net.william278.toilet', 'net.william278.husksync.libraries.toilet'
|
||||
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
|
||||
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
|
||||
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
|
||||
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
|
||||
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
|
||||
relocate 'org.json', 'net.william278.husksync.libraries.json'
|
||||
relocate 'net.querz', 'net.william278.husksync.libraries.nbtparser'
|
||||
relocate 'net.roxeez', 'net.william278.husksync.libraries'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
|
||||
relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib'
|
||||
relocate 'de.tr7zw.changeme.nbtapi', 'net.william278.husksync.libraries.nbtapi'
|
||||
|
||||
minimize()
|
||||
}
|
||||
|
||||
tasks {
|
||||
runServer {
|
||||
minecraftVersion(project.name)
|
||||
|
||||
downloadPlugins {
|
||||
github("plan-player-analytics", "Plan", "5.6.2965", "Plan-5.6-build-2965.jar")
|
||||
}
|
||||
}
|
||||
}
|
||||
5
bukkit/gradle.properties
Normal file
5
bukkit/gradle.properties
Normal file
@@ -0,0 +1,5 @@
|
||||
org.gradle.daemon=false
|
||||
org.gradle.parallel=true
|
||||
org.gradle.configureoncommand=true
|
||||
org.gradle.parallel.threads=4
|
||||
org.gradle.jvmargs=-Xmx8G
|
||||
0
bukkit/root.gradle
Normal file
0
bukkit/root.gradle
Normal file
@@ -1,57 +0,0 @@
|
||||
package me.william278.husksync.bukkit.data;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.EntityType;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Holds legacy data store methods for data storage
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("DeprecatedIsStillUsed")
|
||||
public class DataSerializer {
|
||||
|
||||
/**
|
||||
* A record used to store data for advancement synchronisation
|
||||
*
|
||||
* @deprecated Old format - Use {@link AdvancementRecordDate} instead
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("DeprecatedIsStillUsed")
|
||||
// Suppress deprecation warnings here (still used for backwards compatibility)
|
||||
public record AdvancementRecord(String advancementKey,
|
||||
ArrayList<String> awardedAdvancementCriteria) implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* A record used to store data for a player's statistics
|
||||
*/
|
||||
public record StatisticData(HashMap<Statistic, Integer> untypedStatisticValues,
|
||||
HashMap<Statistic, HashMap<Material, Integer>> blockStatisticValues,
|
||||
HashMap<Statistic, HashMap<Material, Integer>> itemStatisticValues,
|
||||
HashMap<Statistic, HashMap<EntityType, Integer>> entityStatisticValues) implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* A record used to store data for native advancement synchronisation, tracking advancement date progress
|
||||
*/
|
||||
public record AdvancementRecordDate(String key, Map<String, Date> criteriaMap) implements Serializable {
|
||||
public AdvancementRecordDate(String key, List<String> criteriaList) {
|
||||
this(key, new HashMap<>() {{
|
||||
criteriaList.forEach(s -> put(s, Date.from(Instant.EPOCH)));
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A record used to store data for a player's location
|
||||
*/
|
||||
public record PlayerLocation(double x, double y, double z, float yaw, float pitch,
|
||||
String worldName, World.Environment environment) implements Serializable {
|
||||
}
|
||||
}
|
||||
403
bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java
Normal file
403
bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java
Normal file
@@ -0,0 +1,403 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.gson.Gson;
|
||||
import de.tr7zw.changeme.nbtapi.NBT;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.kyori.adventure.platform.AudienceProvider;
|
||||
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.adapter.GsonAdapter;
|
||||
import net.william278.husksync.adapter.SnappyGsonAdapter;
|
||||
import net.william278.husksync.api.BukkitHuskSyncAPI;
|
||||
import net.william278.husksync.command.PluginCommand;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.config.Server;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.*;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.database.MongoDbDatabase;
|
||||
import net.william278.husksync.database.MySqlDatabase;
|
||||
import net.william278.husksync.database.PostgresDatabase;
|
||||
import net.william278.husksync.event.BukkitEventDispatcher;
|
||||
import net.william278.husksync.hook.PlanHook;
|
||||
import net.william278.husksync.listener.BukkitEventListener;
|
||||
import net.william278.husksync.listener.LockedHandler;
|
||||
import net.william278.husksync.maps.BukkitMapHandler;
|
||||
import net.william278.husksync.migrator.LegacyMigrator;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.migrator.MpdbMigrator;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.sync.DataSyncer;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.util.BukkitLegacyConverter;
|
||||
import net.william278.husksync.util.BukkitTask;
|
||||
import net.william278.husksync.util.LegacyConverter;
|
||||
import net.william278.toilet.BukkitToilet;
|
||||
import net.william278.toilet.Toilet;
|
||||
import net.william278.uniform.Uniform;
|
||||
import net.william278.uniform.bukkit.BukkitUniform;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.map.MapView;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import space.arim.morepaperlib.MorePaperLib;
|
||||
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
|
||||
import space.arim.morepaperlib.scheduling.AttachedScheduler;
|
||||
import space.arim.morepaperlib.scheduling.GracefulScheduling;
|
||||
import space.arim.morepaperlib.scheduling.RegionalScheduler;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("unchecked")
|
||||
public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier,
|
||||
BukkitEventDispatcher, BukkitMapHandler {
|
||||
|
||||
/**
|
||||
* Metrics ID for <a href="https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140">HuskSync on Bukkit</a>.
|
||||
*/
|
||||
private static final int METRICS_ID = 13140;
|
||||
private static final String PLATFORM_TYPE_ID = "bukkit";
|
||||
|
||||
private final HashMap<Identifier, Serializer<? extends Data>> serializers = Maps.newHashMap();
|
||||
private final Map<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap();
|
||||
private final Map<Integer, MapView> mapViews = Maps.newConcurrentMap();
|
||||
private final List<Migrator> availableMigrators = Lists.newArrayList();
|
||||
private final Set<UUID> lockedPlayers = Sets.newConcurrentHashSet();
|
||||
private final Set<UUID> disconnectingPlayers = Sets.newConcurrentHashSet();
|
||||
|
||||
private boolean disabling;
|
||||
private Gson gson;
|
||||
private AudienceProvider audiences;
|
||||
private Toilet toilet;
|
||||
private MorePaperLib paperLib;
|
||||
private Database database;
|
||||
private RedisManager redisManager;
|
||||
private BukkitEventListener eventListener;
|
||||
private DataAdapter dataAdapter;
|
||||
private DataSyncer dataSyncer;
|
||||
private LegacyConverter legacyConverter;
|
||||
private AsynchronousScheduler asyncScheduler;
|
||||
private RegionalScheduler regionalScheduler;
|
||||
@Setter
|
||||
private Settings settings;
|
||||
@Setter
|
||||
private Locales locales;
|
||||
@Setter
|
||||
@Getter(AccessLevel.NONE)
|
||||
private Server serverName;
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
// Initial plugin setup
|
||||
this.disabling = false;
|
||||
this.gson = createGson();
|
||||
this.paperLib = new MorePaperLib(this);
|
||||
|
||||
// Load settings and locales
|
||||
initialize("plugin config & locale files", (plugin) -> {
|
||||
loadSettings();
|
||||
loadLocales();
|
||||
loadServer();
|
||||
validateConfigFiles();
|
||||
});
|
||||
|
||||
this.eventListener = createEventListener();
|
||||
eventListener.onLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
this.audiences = BukkitAudiences.create(this);
|
||||
this.toilet = BukkitToilet.create(getDumpOptions());
|
||||
|
||||
// Check compatibility
|
||||
checkCompatibility();
|
||||
|
||||
// Preload NBT-API
|
||||
if (!NBT.preloadApi()) {
|
||||
log(Level.SEVERE, "Failed to load NBT API. HuskSync will not be initialized!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Register commands
|
||||
initialize("commands", (plugin) -> getUniform().register(PluginCommand.Type.create(this)));
|
||||
|
||||
// Prepare data adapter
|
||||
initialize("data adapter", (plugin) -> {
|
||||
if (settings.getSynchronization().isCompressData()) {
|
||||
dataAdapter = new SnappyGsonAdapter(this);
|
||||
} else {
|
||||
dataAdapter = new GsonAdapter(this);
|
||||
}
|
||||
});
|
||||
|
||||
// Prepare serializers
|
||||
initialize("data serializers", (plugin) -> {
|
||||
registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
|
||||
registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this));
|
||||
registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this));
|
||||
registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(this));
|
||||
registerSerializer(Identifier.STATISTICS, new Serializer.Json<>(this, BukkitData.Statistics.class));
|
||||
registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this));
|
||||
registerSerializer(Identifier.GAME_MODE, new Serializer.Json<>(this, BukkitData.GameMode.class));
|
||||
registerSerializer(Identifier.FLIGHT_STATUS, new Serializer.Json<>(this, BukkitData.FlightStatus.class));
|
||||
registerSerializer(Identifier.ATTRIBUTES, new Serializer.Json<>(this, BukkitData.Attributes.class));
|
||||
registerSerializer(Identifier.HEALTH, new Serializer.Json<>(this, BukkitData.Health.class));
|
||||
registerSerializer(Identifier.HUNGER, new Serializer.Json<>(this, BukkitData.Hunger.class));
|
||||
registerSerializer(Identifier.EXPERIENCE, new Serializer.Json<>(this, BukkitData.Experience.class));
|
||||
registerSerializer(Identifier.LOCATION, new Serializer.Json<>(this, BukkitData.Location.class));
|
||||
validateDependencies();
|
||||
});
|
||||
|
||||
// Setup available migrators
|
||||
initialize("data migrators/converters", (plugin) -> {
|
||||
availableMigrators.add(new LegacyMigrator(this));
|
||||
if (isDependencyLoaded("MySqlPlayerDataBridge")) {
|
||||
availableMigrators.add(new MpdbMigrator(this));
|
||||
}
|
||||
legacyConverter = new BukkitLegacyConverter(this);
|
||||
});
|
||||
|
||||
// Initialize the database
|
||||
initialize(getSettings().getDatabase().getType().getDisplayName() + " database connection", (plugin) -> {
|
||||
this.database = switch (settings.getDatabase().getType()) {
|
||||
case MYSQL, MARIADB -> new MySqlDatabase(this);
|
||||
case POSTGRES -> new PostgresDatabase(this);
|
||||
case MONGO -> new MongoDbDatabase(this);
|
||||
};
|
||||
this.database.initialize();
|
||||
});
|
||||
|
||||
// Prepare redis connection
|
||||
initialize("Redis server connection", (plugin) -> {
|
||||
this.redisManager = new RedisManager(this);
|
||||
this.redisManager.initialize();
|
||||
});
|
||||
|
||||
// Prepare data syncer
|
||||
initialize("data syncer", (plugin) -> {
|
||||
dataSyncer = getSettings().getSynchronization().getMode().create(this);
|
||||
dataSyncer.initialize();
|
||||
});
|
||||
|
||||
// Register events
|
||||
initialize("events", (plugin) -> eventListener.onEnable());
|
||||
|
||||
// Register plugin hooks
|
||||
initialize("hooks", (plugin) -> {
|
||||
if (isDependencyLoaded("Plan") && getSettings().isEnablePlanHook()) {
|
||||
new PlanHook(this).hookIntoPlan();
|
||||
}
|
||||
});
|
||||
|
||||
// Register API
|
||||
initialize("api", (plugin) -> BukkitHuskSyncAPI.register(this));
|
||||
|
||||
// Hook into bStats and check for updates
|
||||
initialize("metrics", (plugin) -> this.registerMetrics(METRICS_ID));
|
||||
this.checkForUpdates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
// Handle shutdown
|
||||
this.disabling = true;
|
||||
|
||||
// Close the event listener / data syncer
|
||||
if (this.dataSyncer != null) {
|
||||
this.dataSyncer.terminate();
|
||||
}
|
||||
if (this.eventListener != null) {
|
||||
this.eventListener.handlePluginDisable();
|
||||
}
|
||||
|
||||
// Unregister API and cancel tasks
|
||||
BukkitHuskSyncAPI.unregister();
|
||||
this.cancelTasks();
|
||||
|
||||
// Complete shutdown
|
||||
log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected BukkitEventListener createEventListener() {
|
||||
return new BukkitEventListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Set<OnlineUser> getOnlineUsers() {
|
||||
return getServer().getOnlinePlayers().stream()
|
||||
.map(player -> BukkitUser.adapt(player, this))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
|
||||
final Player player = getServer().getPlayer(uuid);
|
||||
if (player == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(BukkitUser.adapt(player, this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDataSyncer(@NotNull DataSyncer dataSyncer) {
|
||||
log(Level.INFO, String.format("Switching data syncer to %s", dataSyncer.getClass().getSimpleName()));
|
||||
this.dataSyncer = dataSyncer;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Uniform getUniform() {
|
||||
return BukkitUniform.getInstance(this);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
|
||||
return playerCustomDataStore.compute(
|
||||
user.getUuid(),
|
||||
(uuid, data) -> data == null ? Maps.newHashMap() : data
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getServerName() {
|
||||
return serverName == null ? "server" : serverName.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDependencyLoaded(@NotNull String name) {
|
||||
final Plugin plugin = getServer().getPluginManager().getPlugin(name);
|
||||
return plugin != null;
|
||||
}
|
||||
|
||||
// Register bStats metrics
|
||||
public void registerMetrics(int metricsId) {
|
||||
if (!getPluginVersion().getMetadata().isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
new Metrics(this, metricsId);
|
||||
} catch (Throwable e) {
|
||||
log(Level.WARNING, "Failed to register bStats metrics (%s)".formatted(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable) {
|
||||
if (throwable.length > 0) {
|
||||
getLogger().log(level, message, throwable[0]);
|
||||
} else {
|
||||
getLogger().log(level, message);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Version getPluginVersion() {
|
||||
return Version.fromString(getDescription().getVersion(), "-");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Version getMinecraftVersion() {
|
||||
return Version.fromString(getServer().getBukkitVersion());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getPlatformType() {
|
||||
return PLATFORM_TYPE_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getServerVersion() {
|
||||
return String.format("%s/%s", getServer().getName(), getServer().getVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<LegacyConverter> getLegacyConverter() {
|
||||
return Optional.of(legacyConverter);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public LockedHandler getLockedHandler() {
|
||||
return eventListener.getLockedHandler();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public GracefulScheduling getScheduler() {
|
||||
return paperLib.scheduling();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public AsynchronousScheduler getAsyncScheduler() {
|
||||
return asyncScheduler == null
|
||||
? asyncScheduler = getScheduler().asyncScheduler() : asyncScheduler;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public RegionalScheduler getSyncScheduler() {
|
||||
return regionalScheduler == null
|
||||
? regionalScheduler = getScheduler().globalRegionalScheduler() : regionalScheduler;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public AttachedScheduler getUserSyncScheduler(@NotNull UserDataHolder user) {
|
||||
return getScheduler().entitySpecificScheduler(((BukkitUser) user).getPlayer());
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Path getConfigDirectory() {
|
||||
return getDataFolder().toPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public BukkitHuskSync getPlugin() {
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.util.BukkitUpdateChecker;
|
||||
import net.william278.husksync.bukkit.util.PlayerSetter;
|
||||
import net.william278.husksync.bukkit.config.ConfigLoader;
|
||||
import net.william278.husksync.bukkit.data.BukkitDataCache;
|
||||
import net.william278.husksync.bukkit.listener.BukkitRedisListener;
|
||||
import net.william278.husksync.bukkit.listener.BukkitEventListener;
|
||||
import net.william278.husksync.bukkit.migrator.MPDBDeserializer;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class HuskSyncBukkit extends JavaPlugin {
|
||||
|
||||
// Bukkit bStats ID (Different to BungeeCord)
|
||||
private static final int METRICS_ID = 13140;
|
||||
|
||||
private static HuskSyncBukkit instance;
|
||||
public static HuskSyncBukkit getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static BukkitDataCache bukkitCache;
|
||||
|
||||
public static BukkitRedisListener redisListener;
|
||||
|
||||
// Used for establishing a handshake with redis
|
||||
public static UUID serverUUID;
|
||||
|
||||
// Has a handshake been established with the Bungee?
|
||||
public static boolean handshakeCompleted = false;
|
||||
|
||||
// The handshake task to execute
|
||||
private static BukkitTask handshakeTask;
|
||||
|
||||
// Whether MySqlPlayerDataBridge is installed
|
||||
public static boolean isMySqlPlayerDataBridgeInstalled;
|
||||
|
||||
// Establish the handshake with the proxy
|
||||
public static void establishRedisHandshake() {
|
||||
serverUUID = UUID.randomUUID();
|
||||
getInstance().getLogger().log(Level.INFO, "Executing handshake with Proxy server...");
|
||||
final int[] attempts = {0}; // How many attempts to establish communication have been made
|
||||
handshakeTask = Bukkit.getScheduler().runTaskTimerAsynchronously(getInstance(), () -> {
|
||||
if (handshakeCompleted) {
|
||||
handshakeTask.cancel();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.CONNECTION_HANDSHAKE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
serverUUID.toString(),
|
||||
Boolean.toString(isMySqlPlayerDataBridgeInstalled),
|
||||
Bukkit.getName(),
|
||||
getInstance().getDescription().getVersion())
|
||||
.send();
|
||||
attempts[0]++;
|
||||
if (attempts[0] == 10) {
|
||||
getInstance().getLogger().log(Level.WARNING, "Failed to complete handshake with the Proxy server; Please make sure your Proxy server is online and has HuskSync installed in its' /plugins/ folder. HuskSync will continue to try and establish a connection.");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
getInstance().getLogger().log(Level.SEVERE, "Failed to serialize Redis message for handshake establishment", e);
|
||||
}
|
||||
}, 0, 60);
|
||||
}
|
||||
|
||||
private void closeRedisHandshake() {
|
||||
if (!handshakeCompleted) return;
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.TERMINATE_HANDSHAKE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
serverUUID.toString(),
|
||||
Bukkit.getName()).send();
|
||||
} catch (IOException e) {
|
||||
getInstance().getLogger().log(Level.SEVERE, "Failed to serialize Redis message for handshake termination", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// Plugin startup logic
|
||||
|
||||
// Load the config file
|
||||
getConfig().options().copyDefaults(true);
|
||||
saveDefaultConfig();
|
||||
saveConfig();
|
||||
reloadConfig();
|
||||
ConfigLoader.loadSettings(getConfig());
|
||||
|
||||
// Do update checker
|
||||
if (Settings.automaticUpdateChecks) {
|
||||
new BukkitUpdateChecker().logToConsole();
|
||||
}
|
||||
|
||||
// Check if MySqlPlayerDataBridge is installed
|
||||
Plugin mySqlPlayerDataBridge = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
|
||||
if (mySqlPlayerDataBridge != null) {
|
||||
isMySqlPlayerDataBridgeInstalled = mySqlPlayerDataBridge.isEnabled();
|
||||
MPDBDeserializer.setMySqlPlayerDataBridge();
|
||||
getLogger().info("MySQLPlayerDataBridge detected! Disabled data synchronisation to prevent data loss. To perform a migration, run \"husksync migrate\" in your Proxy (Bungeecord, Waterfall, etc) server console.");
|
||||
}
|
||||
|
||||
// Initialize last data update UUID cache
|
||||
bukkitCache = new BukkitDataCache();
|
||||
|
||||
// Initialize event listener
|
||||
getServer().getPluginManager().registerEvents(new BukkitEventListener(), this);
|
||||
|
||||
// Initialize the redis listener
|
||||
redisListener = new BukkitRedisListener();
|
||||
|
||||
// Ensure redis is connected; establish a handshake
|
||||
establishRedisHandshake();
|
||||
|
||||
// Initialize bStats metrics
|
||||
try {
|
||||
new Metrics(this, METRICS_ID);
|
||||
} catch (Exception e) {
|
||||
getLogger().info("Skipped metrics initialization");
|
||||
}
|
||||
|
||||
// Log to console
|
||||
getLogger().info("Enabled HuskSync (" + getServer().getName() + ") v" + getDescription().getVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
// Update player data for disconnecting players
|
||||
if (HuskSyncBukkit.handshakeCompleted && !HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled && Bukkit.getOnlinePlayers().size() > 0) {
|
||||
getLogger().info("Saving data for remaining online players...");
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
PlayerSetter.updatePlayerData(player, false);
|
||||
|
||||
// Clear player inventory and ender chest
|
||||
player.getInventory().clear();
|
||||
player.getEnderChest().clear();
|
||||
}
|
||||
getLogger().info("Data save complete!");
|
||||
}
|
||||
|
||||
|
||||
// Send termination handshake to proxy
|
||||
closeRedisHandshake();
|
||||
|
||||
// Plugin shutdown logic
|
||||
getLogger().info("Disabled HuskSync (" + getServer().getName() + ") v" + getDescription().getVersion());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync;
|
||||
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.listener.BukkitEventListener;
|
||||
import net.william278.husksync.listener.PaperEventListener;
|
||||
import net.william278.uniform.Uniform;
|
||||
import net.william278.uniform.paper.PaperUniform;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@SuppressWarnings({"unused"})
|
||||
public class PaperHuskSync extends BukkitHuskSync {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected BukkitEventListener createEventListener() {
|
||||
return new PaperEventListener(this);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Audience getAudience(@NotNull UUID user) {
|
||||
final Player player = getServer().getPlayer(user);
|
||||
return player == null || !player.isOnline() ? Audience.empty() : player;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Version getMinecraftVersion() {
|
||||
return Version.fromString(getServer().getMinecraftVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Uniform getUniform() {
|
||||
return PaperUniform.getInstance(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync;
|
||||
|
||||
import de.exlll.configlib.Configuration;
|
||||
import de.exlll.configlib.YamlConfigurations;
|
||||
import io.papermc.paper.plugin.loader.PluginClasspathBuilder;
|
||||
import io.papermc.paper.plugin.loader.PluginLoader;
|
||||
import io.papermc.paper.plugin.loader.library.impl.MavenLibraryResolver;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.eclipse.aether.artifact.DefaultArtifact;
|
||||
import org.eclipse.aether.graph.Dependency;
|
||||
import org.eclipse.aether.repository.RemoteRepository;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public class PaperHuskSyncLoader implements PluginLoader {
|
||||
|
||||
@Override
|
||||
public void classloader(@NotNull PluginClasspathBuilder classpathBuilder) {
|
||||
final MavenLibraryResolver resolver = new MavenLibraryResolver();
|
||||
|
||||
resolveLibraries(classpathBuilder).stream()
|
||||
.map(DefaultArtifact::new)
|
||||
.forEach(artifact -> resolver.addDependency(new Dependency(artifact, null)));
|
||||
resolver.addRepository(new RemoteRepository.Builder("maven", "default", getMavenUrl()).build());
|
||||
|
||||
classpathBuilder.addLibrary(resolver);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String getMavenUrl() {
|
||||
return Stream.of(
|
||||
System.getenv("PAPER_DEFAULT_CENTRAL_REPOSITORY"),
|
||||
System.getProperty("org.bukkit.plugin.java.LibraryLoader.centralURL"),
|
||||
"https://maven-central.storage-download.googleapis.com/maven2"
|
||||
).filter(Objects::nonNull).findFirst().orElseThrow(IllegalStateException::new);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<String> resolveLibraries(@NotNull PluginClasspathBuilder classpathBuilder) {
|
||||
try (InputStream input = getLibraryListFile()) {
|
||||
return YamlConfigurations.read(
|
||||
Objects.requireNonNull(input, "Failed to read libraries file"),
|
||||
PaperLibraries.class
|
||||
).libraries;
|
||||
} catch (Throwable e) {
|
||||
classpathBuilder.getContext().getLogger().error("Failed to resolve libraries", e);
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static InputStream getLibraryListFile() {
|
||||
return PaperHuskSyncLoader.class.getClassLoader().getResourceAsStream("paper-libraries.yml");
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@NoArgsConstructor
|
||||
public static class PaperLibraries {
|
||||
|
||||
private List<String> libraries;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.api;
|
||||
|
||||
import net.william278.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.data.DataHolder;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* The HuskSync API implementation for the Bukkit platform
|
||||
* </p>
|
||||
* Retrieve an instance of the API class via {@link #getInstance()}.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitHuskSyncAPI extends HuskSyncAPI {
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Constructor, instantiating the API.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
private BukkitHuskSyncAPI(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint to the HuskSync API on the bukkit platform - returns an instance of the API
|
||||
*
|
||||
* @return instance of the HuskSync API
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public static BukkitHuskSyncAPI getInstance() {
|
||||
if (!JavaPlugin.getProvidingPlugin(BukkitHuskSyncAPI.class).getName().equals("HuskSync")) {
|
||||
throw new NotRegisteredException("This is likely because you have shaded HuskSync into your plugin JAR " +
|
||||
"and need to fix your maven/gradle/build script so that it *compiles against* HuskSync instead.");
|
||||
}
|
||||
if (instance == null) {
|
||||
throw new NotRegisteredException();
|
||||
}
|
||||
return (BukkitHuskSyncAPI) instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Register the API for this platform.
|
||||
*
|
||||
* @param plugin the plugin instance
|
||||
* @since 3.0
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public static void register(@NotNull BukkitHuskSync plugin) {
|
||||
instance = new BukkitHuskSyncAPI(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link OnlineUser} instance for the given bukkit {@link Player}.
|
||||
*
|
||||
* @param player the bukkit player to get the {@link OnlineUser} instance for
|
||||
* @return the {@link OnlineUser} instance for the given bukkit {@link Player}
|
||||
* @since 2.0
|
||||
*/
|
||||
@NotNull
|
||||
public BukkitUser getUser(@NotNull Player player) {
|
||||
return BukkitUser.adapt(player, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
*
|
||||
* @param user the user to get the inventory of
|
||||
* @return the {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<BukkitData.Items.Inventory>> getCurrentInventory(@NotNull User user) {
|
||||
return getCurrentData(user).thenApply(data -> data.flatMap(DataHolder::getInventory)
|
||||
.map(BukkitData.Items.Inventory.class::cast));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current {@link BukkitData.Items.Inventory} of the given {@link Player}
|
||||
*
|
||||
* @param user the user to get the inventory of
|
||||
* @return the {@link BukkitData.Items.Inventory} of the given {@link Player}
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<ItemStack[]>> getCurrentInventoryContents(@NotNull User user) {
|
||||
return getCurrentInventory(user)
|
||||
.thenApply(inventory -> inventory.map(BukkitData.Items.Inventory::getContents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
*
|
||||
* @param user the user to set the inventory of
|
||||
* @param contents the contents to set the inventory to
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setCurrentInventory(@NotNull User user, @NotNull BukkitData.Items.Inventory contents) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.setInventory(contents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
*
|
||||
* @param user the user to set the inventory of
|
||||
* @param contents the contents to set the inventory to
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setCurrentInventoryContents(@NotNull User user, @NotNull ItemStack[] contents) {
|
||||
editCurrentData(
|
||||
user,
|
||||
dataHolder -> dataHolder.getInventory().ifPresent(
|
||||
inv -> inv.setContents(adaptItems(contents))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the current {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
*
|
||||
* @param user the user to edit the inventory of
|
||||
* @param editor the editor to apply to the inventory
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editCurrentInventory(@NotNull User user, ThrowingConsumer<BukkitData.Items.Inventory> editor) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.getInventory()
|
||||
.map(BukkitData.Items.Inventory.class::cast)
|
||||
.ifPresent(editor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the current {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
*
|
||||
* @param user the user to edit the inventory of
|
||||
* @param editor the editor to apply to the inventory
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editCurrentInventoryContents(@NotNull User user, ThrowingConsumer<ItemStack[]> editor) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.getInventory()
|
||||
.map(BukkitData.Items.Inventory.class::cast)
|
||||
.ifPresent(inventory -> editor.accept(inventory.getContents())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current {@link BukkitData.Items.EnderChest} of the given {@link User}
|
||||
*
|
||||
* @param user the user to get the ender chest of
|
||||
* @return the {@link BukkitData.Items.EnderChest} of the given {@link User}, or {@link Optional#empty()} if the
|
||||
* user data could not be found
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<BukkitData.Items.EnderChest>> getCurrentEnderChest(@NotNull User user) {
|
||||
return getCurrentData(user).thenApply(data -> data.flatMap(DataHolder::getEnderChest)
|
||||
.map(BukkitData.Items.EnderChest.class::cast));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current {@link BukkitData.Items.EnderChest} of the given {@link Player}
|
||||
*
|
||||
* @param user the user to get the ender chest of
|
||||
* @return the {@link BukkitData.Items.EnderChest} of the given {@link Player}, or {@link Optional#empty()} if the
|
||||
* user data could not be found
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<ItemStack[]>> getCurrentEnderChestContents(@NotNull User user) {
|
||||
return getCurrentEnderChest(user)
|
||||
.thenApply(enderChest -> enderChest.map(BukkitData.Items.EnderChest::getContents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current {@link BukkitData.Items.EnderChest} of the given {@link User}
|
||||
*
|
||||
* @param user the user to set the ender chest of
|
||||
* @param contents the contents to set the ender chest to
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setCurrentEnderChest(@NotNull User user, @NotNull BukkitData.Items.EnderChest contents) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.setEnderChest(contents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current {@link BukkitData.Items.EnderChest} of the given {@link User}
|
||||
*
|
||||
* @param user the user to set the ender chest of
|
||||
* @param contents the contents to set the ender chest to
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setCurrentEnderChestContents(@NotNull User user, @NotNull ItemStack[] contents) {
|
||||
editCurrentData(
|
||||
user,
|
||||
dataHolder -> dataHolder.getEnderChest().ifPresent(
|
||||
enderChest -> enderChest.setContents(adaptItems(contents))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the current {@link BukkitData.Items.EnderChest} of the given {@link User}
|
||||
*
|
||||
* @param user the user to edit the ender chest of
|
||||
* @param editor the editor to apply to the ender chest
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editCurrentEnderChest(@NotNull User user, Consumer<BukkitData.Items.EnderChest> editor) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.getEnderChest()
|
||||
.map(BukkitData.Items.EnderChest.class::cast)
|
||||
.ifPresent(editor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the current {@link BukkitData.Items.EnderChest} of the given {@link User}
|
||||
*
|
||||
* @param user the user to edit the ender chest of
|
||||
* @param editor the editor to apply to the ender chest
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editCurrentEnderChestContents(@NotNull User user, Consumer<ItemStack[]> editor) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.getEnderChest()
|
||||
.map(BukkitData.Items.EnderChest.class::cast)
|
||||
.ifPresent(enderChest -> editor.accept(enderChest.getContents())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts an array of {@link ItemStack} to a {@link BukkitData.Items} instance
|
||||
*
|
||||
* @param contents the contents to adapt
|
||||
* @return the adapted {@link BukkitData.Items} instance
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public BukkitData.Items adaptItems(@NotNull ItemStack[] contents) {
|
||||
return BukkitData.Items.ItemArray.adapt(contents);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package net.william278.husksync.bukkit.config;
|
||||
|
||||
import net.william278.husksync.Settings;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
|
||||
public class ConfigLoader {
|
||||
|
||||
public static void loadSettings(FileConfiguration config) throws IllegalArgumentException {
|
||||
Settings.serverType = Settings.ServerType.BUKKIT;
|
||||
Settings.automaticUpdateChecks = config.getBoolean("check_for_updates", true);
|
||||
Settings.cluster = config.getString("cluster_id", "main");
|
||||
Settings.redisHost = config.getString("redis_settings.host", "localhost");
|
||||
Settings.redisPort = config.getInt("redis_settings.port", 6379);
|
||||
Settings.redisPassword = config.getString("redis_settings.password", "");
|
||||
Settings.redisSSL = config.getBoolean("redis_settings.use_ssl", false);
|
||||
|
||||
Settings.syncInventories = config.getBoolean("synchronisation_settings.inventories", true);
|
||||
Settings.syncEnderChests = config.getBoolean("synchronisation_settings.ender_chests", true);
|
||||
Settings.syncHealth = config.getBoolean("synchronisation_settings.health", true);
|
||||
Settings.syncHunger = config.getBoolean("synchronisation_settings.hunger", true);
|
||||
Settings.syncExperience = config.getBoolean("synchronisation_settings.experience", true);
|
||||
Settings.syncPotionEffects = config.getBoolean("synchronisation_settings.potion_effects", true);
|
||||
Settings.syncStatistics = config.getBoolean("synchronisation_settings.statistics", true);
|
||||
Settings.syncGameMode = config.getBoolean("synchronisation_settings.game_mode", true);
|
||||
Settings.syncAdvancements = config.getBoolean("synchronisation_settings.advancements", true);
|
||||
Settings.syncLocation = config.getBoolean("synchronisation_settings.location", false);
|
||||
Settings.syncFlight = config.getBoolean("synchronisation_settings.flight", false);
|
||||
|
||||
Settings.useNativeImplementation = config.getBoolean("native_advancement_synchronization", false);
|
||||
Settings.saveOnWorldSave = config.getBoolean("save_on_world_save", true);
|
||||
Settings.synchronizationTimeoutRetryDelay = config.getLong("synchronization_timeout_retry_delay", 15L);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package net.william278.husksync.bukkit.data;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.UUID;
|
||||
|
||||
public class BukkitDataCache {
|
||||
|
||||
/**
|
||||
* Map of Player UUIDs to request on join
|
||||
*/
|
||||
private static HashSet<UUID> requestOnJoin;
|
||||
|
||||
public boolean isPlayerRequestingOnJoin(UUID uuid) {
|
||||
return requestOnJoin.contains(uuid);
|
||||
}
|
||||
|
||||
public void setRequestOnJoin(UUID uuid) {
|
||||
requestOnJoin.add(uuid);
|
||||
}
|
||||
|
||||
public void removeRequestOnJoin(UUID uuid) {
|
||||
requestOnJoin.remove(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of Player UUIDs whose data has not been set yet
|
||||
*/
|
||||
private static HashSet<UUID> awaitingDataFetch;
|
||||
|
||||
public boolean isAwaitingDataFetch(UUID uuid) {
|
||||
return awaitingDataFetch.contains(uuid);
|
||||
}
|
||||
|
||||
public void setAwaitingDataFetch(UUID uuid) {
|
||||
awaitingDataFetch.add(uuid);
|
||||
}
|
||||
|
||||
public void removeAwaitingDataFetch(UUID uuid) {
|
||||
awaitingDataFetch.remove(uuid);
|
||||
}
|
||||
|
||||
public HashSet<UUID> getAwaitingDataFetch() {
|
||||
return awaitingDataFetch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of data being viewed by players
|
||||
*/
|
||||
private static HashMap<UUID, DataViewer.DataView> viewingPlayerData;
|
||||
|
||||
public void setViewing(UUID uuid, DataViewer.DataView dataView) {
|
||||
viewingPlayerData.put(uuid, dataView);
|
||||
}
|
||||
|
||||
public void removeViewing(UUID uuid) {
|
||||
viewingPlayerData.remove(uuid);
|
||||
}
|
||||
|
||||
public boolean isViewing(UUID uuid) {
|
||||
return viewingPlayerData.containsKey(uuid);
|
||||
}
|
||||
|
||||
public DataViewer.DataView getViewing(UUID uuid) {
|
||||
return viewingPlayerData.get(uuid);
|
||||
}
|
||||
|
||||
// Cache object
|
||||
public BukkitDataCache() {
|
||||
requestOnJoin = new HashSet<>();
|
||||
viewingPlayerData = new HashMap<>();
|
||||
awaitingDataFetch = new HashSet<>();
|
||||
}
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
package net.william278.husksync.bukkit.data;
|
||||
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.advancement.Advancement;
|
||||
import org.bukkit.advancement.AdvancementProgress;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Class that contains static methods for serializing and deserializing data from {@link net.william278.husksync.PlayerData}
|
||||
*/
|
||||
public class DataSerializer {
|
||||
|
||||
/**
|
||||
* Returns a serialized array of {@link ItemStack}s
|
||||
*
|
||||
* @param inventoryContents The contents of the inventory
|
||||
* @return The serialized inventory contents
|
||||
*/
|
||||
public static String serializeInventory(ItemStack[] inventoryContents) {
|
||||
// Return an empty string if there is no inventory item data to serialize
|
||||
if (inventoryContents.length == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Create an output stream that will be encoded into base 64
|
||||
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
|
||||
|
||||
try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
|
||||
// Define the length of the inventory array to serialize
|
||||
bukkitOutputStream.writeInt(inventoryContents.length);
|
||||
|
||||
// Write each serialize each ItemStack to the output stream
|
||||
for (ItemStack inventoryItem : inventoryContents) {
|
||||
bukkitOutputStream.writeObject(serializeItemStack(inventoryItem));
|
||||
}
|
||||
|
||||
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
|
||||
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Failed to serialize item stack data");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of ItemStacks from serialized inventory data. Note: empty slots will be represented by {@code null}
|
||||
*
|
||||
* @param inventoryData The serialized {@link ItemStack[]} array
|
||||
* @return The inventory contents as an array of {@link ItemStack}s
|
||||
* @throws IOException If the deserialization fails reading data from the InputStream
|
||||
* @throws ClassNotFoundException If the deserialization class cannot be found
|
||||
*/
|
||||
public static ItemStack[] deserializeInventory(String inventoryData) throws IOException, ClassNotFoundException {
|
||||
// Return empty array if there is no inventory data (set the player as having an empty inventory)
|
||||
if (inventoryData.isEmpty()) {
|
||||
return new ItemStack[0];
|
||||
}
|
||||
|
||||
// Create a byte input stream to read the serialized data
|
||||
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(inventoryData))) {
|
||||
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
|
||||
// Read the length of the Bukkit input stream and set the length of the array to this value
|
||||
ItemStack[] inventoryContents = new ItemStack[bukkitInputStream.readInt()];
|
||||
|
||||
// Set the ItemStacks in the array from deserialized ItemStack data
|
||||
int slotIndex = 0;
|
||||
for (ItemStack ignored : inventoryContents) {
|
||||
inventoryContents[slotIndex] = deserializeItemStack(bukkitInputStream.readObject());
|
||||
slotIndex++;
|
||||
}
|
||||
|
||||
// Return the finished, serialized inventory contents
|
||||
return inventoryContents;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized version of an {@link ItemStack} as a string to object Map
|
||||
*
|
||||
* @param item The {@link ItemStack} to serialize
|
||||
* @return The serialized {@link ItemStack}
|
||||
*/
|
||||
private static Map<String, Object> serializeItemStack(ItemStack item) {
|
||||
return item != null ? item.serialize() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deserialized {@link ItemStack} from the Object read from the {@link BukkitObjectInputStream}
|
||||
*
|
||||
* @param serializedItemStack The serialized item stack; a String-Object map
|
||||
* @return The deserialized {@link ItemStack}
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
|
||||
private static ItemStack deserializeItemStack(Object serializedItemStack) {
|
||||
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized array of {@link PotionEffect}s
|
||||
*
|
||||
* @param potionEffects The potion effect array
|
||||
* @return The serialized potion effects
|
||||
*/
|
||||
public static String serializePotionEffects(PotionEffect[] potionEffects) {
|
||||
// Return an empty string if there are no effects to serialize
|
||||
if (potionEffects.length == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Create an output stream that will be encoded into base 64
|
||||
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
|
||||
|
||||
try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
|
||||
// Define the length of the potion effect array to serialize
|
||||
bukkitOutputStream.writeInt(potionEffects.length);
|
||||
|
||||
// Write each serialize each PotionEffect to the output stream
|
||||
for (PotionEffect potionEffect : potionEffects) {
|
||||
bukkitOutputStream.writeObject(serializePotionEffect(potionEffect));
|
||||
}
|
||||
|
||||
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
|
||||
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Failed to serialize potion effect data");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of ItemStacks from serialized potion effect data
|
||||
*
|
||||
* @param potionEffectData The serialized {@link PotionEffect[]} array
|
||||
* @return The {@link PotionEffect}s
|
||||
* @throws IOException If the deserialization fails reading data from the InputStream
|
||||
* @throws ClassNotFoundException If the deserialization class cannot be found
|
||||
*/
|
||||
public static PotionEffect[] deserializePotionEffects(String potionEffectData) throws IOException, ClassNotFoundException {
|
||||
// Return empty array if there is no potion effect data (don't apply any effects to the player)
|
||||
if (potionEffectData.isEmpty()) {
|
||||
return new PotionEffect[0];
|
||||
}
|
||||
|
||||
// Create a byte input stream to read the serialized data
|
||||
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(potionEffectData))) {
|
||||
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
|
||||
// Read the length of the Bukkit input stream and set the length of the array to this value
|
||||
PotionEffect[] potionEffects = new PotionEffect[bukkitInputStream.readInt()];
|
||||
|
||||
// Set the potion effects in the array from deserialized PotionEffect data
|
||||
int potionIndex = 0;
|
||||
for (PotionEffect ignored : potionEffects) {
|
||||
potionEffects[potionIndex] = deserializePotionEffect(bukkitInputStream.readObject());
|
||||
potionIndex++;
|
||||
}
|
||||
|
||||
// Return the finished, serialized potion effect array
|
||||
return potionEffects;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized version of an {@link ItemStack} as a string to object Map
|
||||
*
|
||||
* @param potionEffect The {@link ItemStack} to serialize
|
||||
* @return The serialized {@link ItemStack}
|
||||
*/
|
||||
private static Map<String, Object> serializePotionEffect(PotionEffect potionEffect) {
|
||||
return potionEffect != null ? potionEffect.serialize() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deserialized {@link PotionEffect} from the Object read from the {@link BukkitObjectInputStream}
|
||||
*
|
||||
* @param serializedPotionEffect The serialized potion effect; a String-Object map
|
||||
* @return The deserialized {@link PotionEffect}
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
|
||||
private static PotionEffect deserializePotionEffect(Object serializedPotionEffect) {
|
||||
return serializedPotionEffect != null ? new PotionEffect((Map<String, Object>) serializedPotionEffect) : null;
|
||||
}
|
||||
|
||||
public static me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation deserializePlayerLocationData(String serializedLocationData) throws IOException {
|
||||
if (serializedLocationData.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return (me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation) RedisMessage.deserialize(serializedLocationData);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IOException("Unable to decode class type.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getSerializedLocation(Player player) throws IOException {
|
||||
final Location playerLocation = player.getLocation();
|
||||
return RedisMessage.serialize(new me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation(playerLocation.getX(), playerLocation.getY(), playerLocation.getZ(),
|
||||
playerLocation.getYaw(), playerLocation.getPitch(), player.getWorld().getName(), player.getWorld().getEnvironment()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a player's advancement data as serialized with {@link #getSerializedAdvancements(Player)} into {@link me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate} data.
|
||||
*
|
||||
* @param serializedAdvancementData The serialized advancement data {@link String}
|
||||
* @return The deserialized {@link me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate} for the player
|
||||
* @throws IOException If the deserialization fails
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // Ignore the unchecked cast here
|
||||
public static List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> deserializeAdvancementData(String serializedAdvancementData) throws IOException {
|
||||
if (serializedAdvancementData.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
List<?> deserialize = (List<?>) RedisMessage.deserialize(serializedAdvancementData);
|
||||
|
||||
// Migrate old AdvancementRecord into date format
|
||||
if (!deserialize.isEmpty() && deserialize.get(0) instanceof me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecord) {
|
||||
deserialize = ((List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecord>) deserialize).stream()
|
||||
.map(o -> new me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate(
|
||||
o.advancementKey(),
|
||||
o.awardedAdvancementCriteria()
|
||||
)).toList();
|
||||
}
|
||||
|
||||
return (List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate>) deserialize;
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IOException("Unable to decode class type.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of a player's advancements that can be deserialized with {@link #deserializeStatisticData(String)}
|
||||
*
|
||||
* @param player {@link Player} to serialize advancement data of
|
||||
* @return The serialized advancement data as a {@link String}
|
||||
* @throws IOException If the serialization fails
|
||||
*/
|
||||
public static String getSerializedAdvancements(Player player) throws IOException {
|
||||
Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
|
||||
ArrayList<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> advancementData = new ArrayList<>();
|
||||
|
||||
while (serverAdvancements.hasNext()) {
|
||||
final AdvancementProgress progress = player.getAdvancementProgress(serverAdvancements.next());
|
||||
final NamespacedKey advancementKey = progress.getAdvancement().getKey();
|
||||
|
||||
final Map<String, Date> awardedCriteria = new HashMap<>();
|
||||
progress.getAwardedCriteria().forEach(s -> awardedCriteria.put(s, progress.getDateAwarded(s)));
|
||||
|
||||
advancementData.add(new me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate(advancementKey.getNamespace() + ":" + advancementKey.getKey(), awardedCriteria));
|
||||
}
|
||||
|
||||
return RedisMessage.serialize(advancementData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a player's statistic data as serialized with {@link #getSerializedStatisticData(Player)} into {@link me.william278.husksync.bukkit.data.DataSerializer.StatisticData}.
|
||||
*
|
||||
* @param serializedStatisticData The serialized statistic data {@link String}
|
||||
* @return The deserialized {@link me.william278.husksync.bukkit.data.DataSerializer.StatisticData} for the player
|
||||
* @throws IOException If the deserialization fails
|
||||
*/
|
||||
public static me.william278.husksync.bukkit.data.DataSerializer.StatisticData deserializeStatisticData(String serializedStatisticData) throws IOException {
|
||||
if (serializedStatisticData.isEmpty()) {
|
||||
return new me.william278.husksync.bukkit.data.DataSerializer.StatisticData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
|
||||
}
|
||||
try {
|
||||
return (me.william278.husksync.bukkit.data.DataSerializer.StatisticData) RedisMessage.deserialize(serializedStatisticData);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IOException("Unable to decode class type.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of a player's statistic data that can be deserialized with {@link #deserializeStatisticData(String)}
|
||||
*
|
||||
* @param player {@link Player} to serialize statistic data of
|
||||
* @return The serialized statistic data as a {@link String}
|
||||
* @throws IOException If the serialization fails
|
||||
*/
|
||||
public static String getSerializedStatisticData(Player player) throws IOException {
|
||||
HashMap<Statistic, Integer> untypedStatisticValues = new HashMap<>();
|
||||
HashMap<Statistic, HashMap<Material, Integer>> blockStatisticValues = new HashMap<>();
|
||||
HashMap<Statistic, HashMap<Material, Integer>> itemStatisticValues = new HashMap<>();
|
||||
HashMap<Statistic, HashMap<EntityType, Integer>> entityStatisticValues = new HashMap<>();
|
||||
for (Statistic statistic : Statistic.values()) {
|
||||
switch (statistic.getType()) {
|
||||
case ITEM -> {
|
||||
HashMap<Material, Integer> itemValues = new HashMap<>();
|
||||
for (Material itemMaterial : Arrays.stream(Material.values()).filter(Material::isItem).toList()) {
|
||||
itemValues.put(itemMaterial, player.getStatistic(statistic, itemMaterial));
|
||||
}
|
||||
itemStatisticValues.put(statistic, itemValues);
|
||||
}
|
||||
case BLOCK -> {
|
||||
HashMap<Material, Integer> blockValues = new HashMap<>();
|
||||
for (Material blockMaterial : Arrays.stream(Material.values()).filter(Material::isBlock).toList()) {
|
||||
blockValues.put(blockMaterial, player.getStatistic(statistic, blockMaterial));
|
||||
}
|
||||
blockStatisticValues.put(statistic, blockValues);
|
||||
}
|
||||
case ENTITY -> {
|
||||
HashMap<EntityType, Integer> entityValues = new HashMap<>();
|
||||
for (EntityType type : Arrays.stream(EntityType.values()).filter(EntityType::isAlive).toList()) {
|
||||
entityValues.put(type, player.getStatistic(statistic, type));
|
||||
}
|
||||
entityStatisticValues.put(statistic, entityValues);
|
||||
}
|
||||
case UNTYPED -> untypedStatisticValues.put(statistic, player.getStatistic(statistic));
|
||||
}
|
||||
}
|
||||
|
||||
me.william278.husksync.bukkit.data.DataSerializer.StatisticData statisticData = new me.william278.husksync.bukkit.data.DataSerializer.StatisticData(untypedStatisticValues, blockStatisticValues, itemStatisticValues, entityStatisticValues);
|
||||
return RedisMessage.serialize(statisticData);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package net.william278.husksync.bukkit.data;
|
||||
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.util.PlayerSetter;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Class used for managing viewing inventories using inventory-see command
|
||||
*/
|
||||
public class DataViewer {
|
||||
|
||||
/**
|
||||
* Show a viewer's data to a viewer
|
||||
*
|
||||
* @param viewer The viewing {@link Player} who will see the data
|
||||
* @param data The {@link DataView} to show the viewer
|
||||
* @throws IOException If an exception occurred deserializing item data
|
||||
*/
|
||||
public static void showData(Player viewer, DataView data) throws IOException, ClassNotFoundException {
|
||||
// Show an inventory with the viewer's inventory and equipment
|
||||
viewer.closeInventory();
|
||||
viewer.openInventory(createInventory(viewer, data));
|
||||
|
||||
// Set the viewer as viewing
|
||||
HuskSyncBukkit.bukkitCache.setViewing(viewer.getUniqueId(), data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles what happens after a data viewer finishes viewing data
|
||||
*
|
||||
* @param viewer The viewing {@link Player} who was looking at data
|
||||
* @param inventory The {@link Inventory} that was being viewed
|
||||
* @throws IOException If an exception occurred serializing item data
|
||||
*/
|
||||
public static void stopShowing(Player viewer, Inventory inventory) throws IOException {
|
||||
// Get the DataView the player was looking at
|
||||
DataView dataView = HuskSyncBukkit.bukkitCache.getViewing(viewer.getUniqueId());
|
||||
|
||||
// Set the player as no longer viewing an inventory
|
||||
HuskSyncBukkit.bukkitCache.removeViewing(viewer.getUniqueId());
|
||||
|
||||
// Get and update the PlayerData with the new item data
|
||||
PlayerData playerData = dataView.playerData();
|
||||
String serializedItemData = DataSerializer.serializeInventory(inventory.getContents());
|
||||
switch (dataView.inventoryType()) {
|
||||
case INVENTORY -> playerData.setSerializedInventory(serializedItemData);
|
||||
case ENDER_CHEST -> playerData.setSerializedEnderChest(serializedItemData);
|
||||
}
|
||||
|
||||
// Send a redis message with the updated data after the viewing
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
RedisMessage.serialize(playerData), Boolean.toString(true))
|
||||
.send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the inventory object that the viewer will see
|
||||
*
|
||||
* @param viewer The {@link Player} who will view the data
|
||||
* @param data The {@link DataView} data to view
|
||||
* @return The {@link Inventory} that the viewer will see
|
||||
* @throws IOException If an exception occurred deserializing item data
|
||||
*/
|
||||
private static Inventory createInventory(Player viewer, DataView data) throws IOException, ClassNotFoundException {
|
||||
Inventory inventory = switch (data.inventoryType) {
|
||||
case INVENTORY -> Bukkit.createInventory(viewer, 45, data.ownerName + "'s Inventory");
|
||||
case ENDER_CHEST -> Bukkit.createInventory(viewer, 27, data.ownerName + "'s Ender Chest");
|
||||
};
|
||||
PlayerSetter.setInventory(inventory, data.getDeserializedData());
|
||||
return inventory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents Player Data being viewed by a {@link Player}
|
||||
*/
|
||||
public record DataView(PlayerData playerData, String ownerName, InventoryType inventoryType) {
|
||||
/**
|
||||
* What kind of item data is being viewed
|
||||
*/
|
||||
public enum InventoryType {
|
||||
/**
|
||||
* A player's inventory
|
||||
*/
|
||||
INVENTORY,
|
||||
|
||||
/**
|
||||
* A player's ender chest
|
||||
*/
|
||||
ENDER_CHEST
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the deserialized data currently being viewed
|
||||
*
|
||||
* @return The deserialized item data, as an {@link ItemStack[]} array
|
||||
* @throws IOException If an exception occurred deserializing item data
|
||||
*/
|
||||
public ItemStack[] getDeserializedData() throws IOException, ClassNotFoundException {
|
||||
return switch (inventoryType) {
|
||||
case INVENTORY -> DataSerializer.deserializeInventory(playerData.getSerializedInventory());
|
||||
case ENDER_CHEST -> DataSerializer.deserializeInventory(playerData.getSerializedEnderChest());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package net.william278.husksync.bukkit.events;
|
||||
|
||||
import net.william278.husksync.PlayerData;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.player.PlayerEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Represents an event that will be fired when a {@link Player} has finished being synchronised with the correct {@link PlayerData}.
|
||||
*/
|
||||
public class SyncCompleteEvent extends PlayerEvent {
|
||||
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
private final PlayerData data;
|
||||
|
||||
public SyncCompleteEvent(Player player, PlayerData data) {
|
||||
super(player);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link PlayerData} which has just been set on the {@link Player}
|
||||
* @return The {@link PlayerData} that has been set
|
||||
*/
|
||||
public PlayerData getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package net.william278.husksync.bukkit.events;
|
||||
|
||||
import net.william278.husksync.PlayerData;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.player.PlayerEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Represents an event that will be fired before a {@link Player} is about to be synchronised with their {@link PlayerData}.
|
||||
*/
|
||||
public class SyncEvent extends PlayerEvent implements Cancellable {
|
||||
|
||||
private boolean cancelled;
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
private PlayerData data;
|
||||
|
||||
public SyncEvent(Player player, PlayerData data) {
|
||||
super(player);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link PlayerData} which has just been set on the {@link Player}
|
||||
*
|
||||
* @return The {@link PlayerData} that has been set
|
||||
*/
|
||||
public PlayerData getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link PlayerData} to be synchronised to this player
|
||||
*
|
||||
* @param data The {@link PlayerData} to set to the player
|
||||
*/
|
||||
public void setData(PlayerData data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cancellation state of this event. A cancelled event will not be executed in the server, but will still pass to other plugins
|
||||
*
|
||||
* @return true if this event is cancelled
|
||||
*/
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cancellation state of this event. A cancelled event will not be executed in the server, but will still pass to other plugins.
|
||||
*
|
||||
* @param cancel true if you wish to cancel this event
|
||||
*/
|
||||
@Override
|
||||
public void setCancelled(boolean cancel) {
|
||||
this.cancelled = cancel;
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
package net.william278.husksync.bukkit.listener;
|
||||
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.data.DataViewer;
|
||||
import net.william278.husksync.bukkit.util.PlayerSetter;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.entity.EntityPickupItemEvent;
|
||||
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||
import org.bukkit.event.player.*;
|
||||
import org.bukkit.event.world.WorldSaveEvent;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitEventListener implements Listener {
|
||||
|
||||
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||
// When a player leaves a Bukkit server
|
||||
final Player player = event.getPlayer();
|
||||
|
||||
// If the player was awaiting data fetch, remove them and prevent data from being overwritten
|
||||
if (HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(player.getUniqueId())) {
|
||||
HuskSyncBukkit.bukkitCache.removeAwaitingDataFetch(player.getUniqueId());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled)
|
||||
return; // If the plugin has not been initialized correctly
|
||||
|
||||
// Update the player's data
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
// Update data to proxy
|
||||
PlayerSetter.updatePlayerData(player, true);
|
||||
|
||||
// Clear player inventory and ender chest
|
||||
player.getInventory().clear();
|
||||
player.getEnderChest().clear();
|
||||
});
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
if (!plugin.isEnabled()) return; // If the plugin has not been initialized correctly
|
||||
|
||||
// When a player joins a Bukkit server
|
||||
final Player player = event.getPlayer();
|
||||
|
||||
// Mark the player as awaiting data fetch
|
||||
HuskSyncBukkit.bukkitCache.setAwaitingDataFetch(player.getUniqueId());
|
||||
|
||||
if (!HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled) {
|
||||
return; // If the data handshake has not been completed yet (or MySqlPlayerDataBridge is installed)
|
||||
}
|
||||
|
||||
// Send a redis message requesting the player data (if they need to)
|
||||
if (HuskSyncBukkit.bukkitCache.isPlayerRequestingOnJoin(player.getUniqueId())) {
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
PlayerSetter.requestPlayerData(player.getUniqueId());
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData fetch request", e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If the player's data wasn't set after the synchronization timeout retry delay ticks, ensure it will be
|
||||
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, () -> {
|
||||
if (player.isOnline()) {
|
||||
try {
|
||||
if (HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(player.getUniqueId())) {
|
||||
PlayerSetter.requestPlayerData(player.getUniqueId());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData fetch request", e);
|
||||
}
|
||||
}
|
||||
}, Settings.synchronizationTimeoutRetryDelay);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryClose(InventoryCloseEvent event) {
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId()))
|
||||
return; // If the plugin has not been initialized correctly
|
||||
|
||||
// When a player closes an Inventory
|
||||
final Player player = (Player) event.getPlayer();
|
||||
|
||||
// Handle a player who has finished viewing a player's item data
|
||||
if (HuskSyncBukkit.bukkitCache.isViewing(player.getUniqueId())) {
|
||||
try {
|
||||
DataViewer.stopShowing(player, event.getInventory());
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to serialize updated item data", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Events to cancel if the player has not been set yet
|
||||
*/
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onDropItem(PlayerDropItemEvent event) {
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
|
||||
event.setCancelled(true); // If the plugin / player has not been set
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onPickupItem(EntityPickupItemEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(player.getUniqueId())) {
|
||||
event.setCancelled(true); // If the plugin / player has not been set
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
|
||||
event.setCancelled(true); // If the plugin / player has not been set
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onBlockPlace(BlockPlaceEvent event) {
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
|
||||
event.setCancelled(true); // If the plugin / player has not been set
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onBlockBreak(BlockBreakEvent event) {
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
|
||||
event.setCancelled(true); // If the plugin / player has not been set
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onInventoryOpen(InventoryOpenEvent event) {
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
|
||||
event.setCancelled(true); // If the plugin / player has not been set
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.NORMAL)
|
||||
public void onWorldSave(WorldSaveEvent event) {
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted) {
|
||||
return;
|
||||
}
|
||||
for (Player playerInWorld : event.getWorld().getPlayers()) {
|
||||
PlayerSetter.updatePlayerData(playerInWorld, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
package net.william278.husksync.bukkit.listener;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.config.ConfigLoader;
|
||||
import net.william278.husksync.bukkit.data.DataViewer;
|
||||
import net.william278.husksync.bukkit.migrator.MPDBDeserializer;
|
||||
import net.william278.husksync.bukkit.util.PlayerSetter;
|
||||
import net.william278.husksync.migrator.MPDBPlayerData;
|
||||
import net.william278.husksync.redis.RedisListener;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.william278.husksync.util.MessageManager;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitRedisListener extends RedisListener {
|
||||
|
||||
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||
|
||||
public static HashMap<UUID, CompletableFuture<PlayerData>> apiRequests = new HashMap<>();
|
||||
|
||||
// Initialize the listener on the bukkit server
|
||||
public BukkitRedisListener() {
|
||||
super();
|
||||
listen();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming {@link RedisMessage}
|
||||
*
|
||||
* @param message The {@link RedisMessage} to handle
|
||||
*/
|
||||
@Override
|
||||
public void handleMessage(RedisMessage message) {
|
||||
// Ignore messages for proxy servers
|
||||
if (!message.getMessageTarget().targetServerType().equals(Settings.ServerType.BUKKIT)) {
|
||||
return;
|
||||
}
|
||||
// Ignore messages if the plugin is disabled
|
||||
if (!plugin.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
// Ignore messages for other clusters if applicable
|
||||
final String targetClusterId = message.getMessageTarget().targetClusterId();
|
||||
if (targetClusterId != null) {
|
||||
if (!targetClusterId.equalsIgnoreCase(Settings.cluster)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the incoming redis message; either for a specific player or the system
|
||||
if (message.getMessageTarget().targetPlayerUUID() == null) {
|
||||
switch (message.getMessageType()) {
|
||||
case REQUEST_DATA_ON_JOIN -> {
|
||||
UUID playerUUID = UUID.fromString(message.getMessageDataElements()[1]);
|
||||
switch (RedisMessage.RequestOnJoinUpdateType.valueOf(message.getMessageDataElements()[0])) {
|
||||
case ADD_REQUESTER -> HuskSyncBukkit.bukkitCache.setRequestOnJoin(playerUUID);
|
||||
case REMOVE_REQUESTER -> HuskSyncBukkit.bukkitCache.removeRequestOnJoin(playerUUID);
|
||||
}
|
||||
}
|
||||
case CONNECTION_HANDSHAKE -> {
|
||||
UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
String proxyBrand = message.getMessageDataElements()[1];
|
||||
if (serverUUID.equals(HuskSyncBukkit.serverUUID)) {
|
||||
HuskSyncBukkit.handshakeCompleted = true;
|
||||
log(Level.INFO, "Completed handshake with " + proxyBrand + " proxy (" + serverUUID + ")");
|
||||
|
||||
// If there are any players awaiting a data update, request it
|
||||
for (UUID uuid : HuskSyncBukkit.bukkitCache.getAwaitingDataFetch()) {
|
||||
try {
|
||||
PlayerSetter.requestPlayerData(uuid);
|
||||
} catch (IOException e) {
|
||||
log(Level.SEVERE, "Failed to serialize handshake message data");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case TERMINATE_HANDSHAKE -> {
|
||||
UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
String proxyBrand = message.getMessageDataElements()[1];
|
||||
if (serverUUID.equals(HuskSyncBukkit.serverUUID)) {
|
||||
HuskSyncBukkit.handshakeCompleted = false;
|
||||
log(Level.WARNING, proxyBrand + " proxy has terminated communications; attempting to re-establish (" + serverUUID + ")");
|
||||
|
||||
// Attempt to re-establish communications via another handshake
|
||||
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, HuskSyncBukkit::establishRedisHandshake, 20);
|
||||
}
|
||||
}
|
||||
case DECODE_MPDB_DATA -> {
|
||||
UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
String encodedData = message.getMessageDataElements()[1];
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
if (serverUUID.equals(HuskSyncBukkit.serverUUID)) {
|
||||
try {
|
||||
MPDBPlayerData data = (MPDBPlayerData) RedisMessage.deserialize(encodedData);
|
||||
new RedisMessage(RedisMessage.MessageType.DECODED_MPDB_DATA_SET,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
RedisMessage.serialize(MPDBDeserializer.convertMPDBData(data)),
|
||||
data.playerName)
|
||||
.send();
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to serialize encoded MPDB data");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
case API_DATA_RETURN -> {
|
||||
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
if (apiRequests.containsKey(requestUUID)) {
|
||||
try {
|
||||
final PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageDataElements()[1]);
|
||||
apiRequests.get(requestUUID).complete(data);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to serialize returned API-requested player data");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
case API_DATA_CANCEL -> {
|
||||
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
// Cancel requests if no data could be found on the proxy
|
||||
if (apiRequests.containsKey(requestUUID)) {
|
||||
apiRequests.get(requestUUID).cancel(true);
|
||||
}
|
||||
}
|
||||
case RELOAD_CONFIG -> {
|
||||
plugin.reloadConfig();
|
||||
ConfigLoader.loadSettings(plugin.getConfig());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (player.getUniqueId().equals(message.getMessageTarget().targetPlayerUUID())) {
|
||||
switch (message.getMessageType()) {
|
||||
case PLAYER_DATA_SET -> {
|
||||
if (HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled) return;
|
||||
try {
|
||||
// Deserialize the received PlayerData
|
||||
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageData());
|
||||
|
||||
// Set the player's data
|
||||
PlayerSetter.setPlayerFrom(player, data);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to deserialize PlayerData when handling data from the proxy");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
case SEND_PLUGIN_INFORMATION -> {
|
||||
String proxyBrand = message.getMessageDataElements()[0];
|
||||
String proxyVersion = message.getMessageDataElements()[1];
|
||||
assert plugin.getDescription().getDescription() != null;
|
||||
player.spigot().sendMessage(new MineDown(MessageManager.PLUGIN_INFORMATION.toString()
|
||||
.replaceAll("%plugin_description%", plugin.getDescription().getDescription())
|
||||
.replaceAll("%proxy_brand%", proxyBrand)
|
||||
.replaceAll("%proxy_version%", proxyVersion)
|
||||
.replaceAll("%bukkit_brand%", Bukkit.getName())
|
||||
.replaceAll("%bukkit_version%", plugin.getDescription().getVersion()))
|
||||
.toComponent());
|
||||
}
|
||||
case OPEN_INVENTORY -> {
|
||||
// Get the name of the inventory owner
|
||||
String inventoryOwnerName = message.getMessageDataElements()[0];
|
||||
|
||||
// Synchronously do inventory setting, etc
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
try {
|
||||
// Get that player's data
|
||||
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageDataElements()[1]);
|
||||
|
||||
// Show the data to the player
|
||||
DataViewer.showData(player, new DataViewer.DataView(data, inventoryOwnerName, DataViewer.DataView.InventoryType.INVENTORY));
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to deserialize PlayerData when handling inventory-see data from the proxy");
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
case OPEN_ENDER_CHEST -> {
|
||||
// Get the name of the inventory owner
|
||||
String enderChestOwnerName = message.getMessageDataElements()[0];
|
||||
|
||||
// Synchronously do inventory setting, etc
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
try {
|
||||
// Get that player's data
|
||||
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageDataElements()[1]);
|
||||
|
||||
// Show the data to the player
|
||||
DataViewer.showData(player, new DataViewer.DataView(data, enderChestOwnerName, DataViewer.DataView.InventoryType.ENDER_CHEST));
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to deserialize PlayerData when handling ender chest-see data from the proxy");
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log to console
|
||||
*
|
||||
* @param level The {@link Level} to log
|
||||
* @param message Message to log
|
||||
*/
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
plugin.getLogger().log(level, message);
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package net.william278.husksync.bukkit.migrator;
|
||||
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.bukkit.data.DataSerializer;
|
||||
import net.william278.husksync.bukkit.util.PlayerSetter;
|
||||
import net.william278.husksync.migrator.MPDBPlayerData;
|
||||
import net.william278.mpdbconverter.MPDBConverter;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class MPDBDeserializer {
|
||||
|
||||
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||
|
||||
// Instance of MySqlPlayerDataBridge
|
||||
private static MPDBConverter mpdbConverter;
|
||||
|
||||
public static void setMySqlPlayerDataBridge() {
|
||||
Plugin mpdbPlugin = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
|
||||
assert mpdbPlugin != null;
|
||||
mpdbConverter = MPDBConverter.getInstance(mpdbPlugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert MySqlPlayerDataBridge ({@link MPDBPlayerData}) data to HuskSync's {@link PlayerData}
|
||||
*
|
||||
* @param mpdbPlayerData The {@link MPDBPlayerData} to convert
|
||||
* @return The converted {@link PlayerData}
|
||||
*/
|
||||
public static PlayerData convertMPDBData(MPDBPlayerData mpdbPlayerData) {
|
||||
PlayerData playerData = PlayerData.DEFAULT_PLAYER_DATA(mpdbPlayerData.playerUUID);
|
||||
playerData.useDefaultData = false;
|
||||
if (!HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled) {
|
||||
plugin.getLogger().log(Level.SEVERE, "MySqlPlayerDataBridge is not installed, failed to serialize data!");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert the data
|
||||
try {
|
||||
// Set inventory contents
|
||||
Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
|
||||
if (!mpdbPlayerData.inventoryData.isEmpty() && !mpdbPlayerData.inventoryData.equalsIgnoreCase("none")) {
|
||||
PlayerSetter.setInventory(inventory, mpdbConverter.getItemStackFromSerializedData(mpdbPlayerData.inventoryData));
|
||||
}
|
||||
|
||||
// Set armor (if there is data; MPDB stores empty data with literally the word "none". Obviously.)
|
||||
int armorSlot = 36;
|
||||
if (!mpdbPlayerData.armorData.isEmpty() && !mpdbPlayerData.armorData.equalsIgnoreCase("none")) {
|
||||
ItemStack[] armorItems = mpdbConverter.getItemStackFromSerializedData(mpdbPlayerData.armorData);
|
||||
for (ItemStack armorPiece : armorItems) {
|
||||
if (armorPiece != null) {
|
||||
inventory.setItem(armorSlot, armorPiece);
|
||||
}
|
||||
armorSlot++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Now apply the contents and clear the temporary inventory variable
|
||||
playerData.setSerializedInventory(DataSerializer.serializeInventory(inventory.getContents()));
|
||||
|
||||
// Set ender chest (again, if there is data)
|
||||
ItemStack[] enderChestData;
|
||||
if (!mpdbPlayerData.enderChestData.isEmpty() && !mpdbPlayerData.enderChestData.equalsIgnoreCase("none")) {
|
||||
enderChestData = mpdbConverter.getItemStackFromSerializedData(mpdbPlayerData.enderChestData);
|
||||
} else {
|
||||
enderChestData = new ItemStack[0];
|
||||
}
|
||||
playerData.setSerializedEnderChest(DataSerializer.serializeInventory(enderChestData));
|
||||
|
||||
// Set experience
|
||||
playerData.setExpLevel(mpdbPlayerData.expLevel);
|
||||
playerData.setExpProgress(mpdbPlayerData.expProgress);
|
||||
playerData.setTotalExperience(mpdbPlayerData.totalExperience);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to convert MPDB data to HuskSync's format!");
|
||||
e.printStackTrace();
|
||||
}
|
||||
return playerData;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package net.william278.husksync.bukkit.util;
|
||||
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.util.UpdateChecker;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitUpdateChecker extends UpdateChecker {
|
||||
|
||||
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||
|
||||
public BukkitUpdateChecker() {
|
||||
super(plugin.getDescription().getVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
plugin.getLogger().log(level, message);
|
||||
}
|
||||
}
|
||||
@@ -1,479 +0,0 @@
|
||||
package net.william278.husksync.bukkit.util;
|
||||
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.events.SyncCompleteEvent;
|
||||
import net.william278.husksync.bukkit.events.SyncEvent;
|
||||
import net.william278.husksync.bukkit.data.DataSerializer;
|
||||
import net.william278.husksync.bukkit.util.nms.AdvancementUtils;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.advancement.Advancement;
|
||||
import org.bukkit.advancement.AdvancementProgress;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class PlayerSetter {
|
||||
|
||||
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||
|
||||
/**
|
||||
* Returns the new serialized PlayerData for a player.
|
||||
*
|
||||
* @param player The {@link Player} to get the new serialized PlayerData for
|
||||
* @return The {@link PlayerData}, serialized as a {@link String}
|
||||
* @throws IOException If the serialization fails
|
||||
*/
|
||||
private static String getNewSerializedPlayerData(Player player) throws IOException {
|
||||
final double maxHealth = getMaxHealth(player); // Get the player's max health (used to determine health as well)
|
||||
return RedisMessage.serialize(new PlayerData(player.getUniqueId(),
|
||||
DataSerializer.serializeInventory(player.getInventory().getContents()),
|
||||
DataSerializer.serializeInventory(player.getEnderChest().getContents()),
|
||||
Math.min(player.getHealth(), maxHealth),
|
||||
maxHealth,
|
||||
player.isHealthScaled() ? player.getHealthScale() : 0D,
|
||||
player.getFoodLevel(),
|
||||
player.getSaturation(),
|
||||
player.getExhaustion(),
|
||||
player.getInventory().getHeldItemSlot(),
|
||||
DataSerializer.serializePotionEffects(getPlayerPotionEffects(player)),
|
||||
player.getTotalExperience(),
|
||||
player.getLevel(),
|
||||
player.getExp(),
|
||||
player.getGameMode().toString(),
|
||||
DataSerializer.getSerializedStatisticData(player),
|
||||
player.isFlying(),
|
||||
DataSerializer.getSerializedAdvancements(player),
|
||||
DataSerializer.getSerializedLocation(player)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Player}'s maximum health, minus any health boost effects
|
||||
*
|
||||
* @param player The {@link Player} to get the maximum health of
|
||||
* @return The {@link Player}'s max health
|
||||
*/
|
||||
private static double getMaxHealth(Player player) {
|
||||
double maxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).getBaseValue();
|
||||
|
||||
// If the player has additional health bonuses from synchronised potion effects, subtract these from this number as they are synchronised separately
|
||||
if (player.hasPotionEffect(PotionEffectType.HEALTH_BOOST) && maxHealth > 20D) {
|
||||
PotionEffect healthBoostEffect = player.getPotionEffect(PotionEffectType.HEALTH_BOOST);
|
||||
assert healthBoostEffect != null;
|
||||
double healthBoostBonus = 4 * (healthBoostEffect.getAmplifier() + 1);
|
||||
maxHealth -= healthBoostBonus;
|
||||
}
|
||||
return maxHealth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Player}'s active potion effects in a {@link PotionEffect} array
|
||||
*
|
||||
* @param player The {@link Player} to get the effects of
|
||||
* @return The {@link PotionEffect} array
|
||||
*/
|
||||
private static PotionEffect[] getPlayerPotionEffects(Player player) {
|
||||
PotionEffect[] potionEffects = new PotionEffect[player.getActivePotionEffects().size()];
|
||||
int arrayIndex = 0;
|
||||
for (PotionEffect effect : player.getActivePotionEffects()) {
|
||||
potionEffects[arrayIndex] = effect;
|
||||
arrayIndex++;
|
||||
}
|
||||
return potionEffects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a {@link Player}'s data, sending it to the proxy
|
||||
*
|
||||
* @param player {@link Player} to send data to proxy
|
||||
* @param bounceBack whether the plugin should bounce-back the updated data to the player (used for server switching)
|
||||
*/
|
||||
public static void updatePlayerData(Player player, boolean bounceBack) {
|
||||
// Send a redis message with the player's last updated PlayerData version UUID and their new PlayerData
|
||||
try {
|
||||
final String serializedPlayerData = getNewSerializedPlayerData(player);
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
serializedPlayerData, Boolean.toString(bounceBack)).send();
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData update to the proxy", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a {@link Player}'s data from the proxy
|
||||
*
|
||||
* @param playerUUID The {@link UUID} of the {@link Player} to fetch PlayerData from
|
||||
* @throws IOException If the request Redis message data fails to serialize
|
||||
*/
|
||||
public static void requestPlayerData(UUID playerUUID) throws IOException {
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_REQUEST,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
playerUUID.toString()).send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a player from their PlayerData, based on settings
|
||||
*
|
||||
* @param player The {@link Player} to set
|
||||
* @param dataToSet The {@link PlayerData} to assign to the player
|
||||
*/
|
||||
public static void setPlayerFrom(Player player, PlayerData dataToSet) {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
// Handle the SyncEvent
|
||||
SyncEvent syncEvent = new SyncEvent(player, dataToSet);
|
||||
Bukkit.getPluginManager().callEvent(syncEvent);
|
||||
final PlayerData data = syncEvent.getData();
|
||||
if (syncEvent.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the data is flagged as being default data, skip setting
|
||||
if (data.useDefaultData) {
|
||||
HuskSyncBukkit.bukkitCache.removeAwaitingDataFetch(player.getUniqueId());
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear player
|
||||
player.getInventory().clear();
|
||||
player.getEnderChest().clear();
|
||||
player.setExp(0);
|
||||
player.setLevel(0);
|
||||
|
||||
HuskSyncBukkit.bukkitCache.removeAwaitingDataFetch(player.getUniqueId());
|
||||
|
||||
// Set the player's data from the PlayerData
|
||||
try {
|
||||
if (Settings.syncAdvancements) {
|
||||
List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> advancementRecords
|
||||
= DataSerializer.deserializeAdvancementData(data.getSerializedAdvancements());
|
||||
|
||||
if (Settings.useNativeImplementation) {
|
||||
try {
|
||||
nativeSyncPlayerAdvancements(player, advancementRecords);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING,
|
||||
"Your server does not support a native implementation of achievements synchronization");
|
||||
plugin.getLogger().log(Level.WARNING,
|
||||
"Your server version is {0}. Please disable using native implementation!", Bukkit.getVersion());
|
||||
|
||||
Settings.useNativeImplementation = false;
|
||||
setPlayerAdvancements(player, advancementRecords, data);
|
||||
plugin.getLogger().log(Level.SEVERE, e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
setPlayerAdvancements(player, advancementRecords, data);
|
||||
}
|
||||
}
|
||||
if (Settings.syncInventories) {
|
||||
setPlayerInventory(player, DataSerializer.deserializeInventory(data.getSerializedInventory()));
|
||||
player.getInventory().setHeldItemSlot(data.getSelectedSlot());
|
||||
}
|
||||
if (Settings.syncEnderChests) {
|
||||
setPlayerEnderChest(player, DataSerializer.deserializeInventory(data.getSerializedEnderChest()));
|
||||
}
|
||||
if (Settings.syncHealth) {
|
||||
setPlayerHealth(player, data.getHealth(), data.getMaxHealth(), data.getHealthScale());
|
||||
}
|
||||
if (Settings.syncHunger) {
|
||||
player.setFoodLevel(data.getHunger());
|
||||
player.setSaturation(data.getSaturation());
|
||||
player.setExhaustion(data.getSaturationExhaustion());
|
||||
}
|
||||
if (Settings.syncExperience) {
|
||||
// This is also handled when syncing advancements to ensure its correct
|
||||
setPlayerExperience(player, data);
|
||||
}
|
||||
if (Settings.syncPotionEffects) {
|
||||
setPlayerPotionEffects(player, DataSerializer.deserializePotionEffects(data.getSerializedEffectData()));
|
||||
}
|
||||
if (Settings.syncStatistics) {
|
||||
setPlayerStatistics(player, DataSerializer.deserializeStatisticData(data.getSerializedStatistics()));
|
||||
}
|
||||
if (Settings.syncGameMode) {
|
||||
player.setGameMode(GameMode.valueOf(data.getGameMode()));
|
||||
}
|
||||
if (Settings.syncLocation) {
|
||||
setPlayerLocation(player, DataSerializer.deserializePlayerLocationData(data.getSerializedLocation()));
|
||||
}
|
||||
if (Settings.syncFlight) {
|
||||
if (data.isFlying()) {
|
||||
player.setAllowFlight(true);
|
||||
}
|
||||
player.setFlying(player.getAllowFlight() && data.isFlying());
|
||||
}
|
||||
|
||||
// Handle the SyncCompleteEvent
|
||||
Bukkit.getPluginManager().callEvent(new SyncCompleteEvent(player, data));
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to deserialize PlayerData", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a player's ender chest from a set of {@link ItemStack}s
|
||||
*
|
||||
* @param player The player to set the inventory of
|
||||
* @param items The array of {@link ItemStack}s to set
|
||||
*/
|
||||
private static void setPlayerEnderChest(Player player, ItemStack[] items) {
|
||||
setInventory(player.getEnderChest(), items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a player's inventory from a set of {@link ItemStack}s
|
||||
*
|
||||
* @param player The player to set the inventory of
|
||||
* @param items The array of {@link ItemStack}s to set
|
||||
*/
|
||||
private static void setPlayerInventory(Player player, ItemStack[] items) {
|
||||
setInventory(player.getInventory(), items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an inventory's contents from an array of {@link ItemStack}s
|
||||
*
|
||||
* @param inventory The inventory to set
|
||||
* @param items The {@link ItemStack}s to fill it with
|
||||
*/
|
||||
public static void setInventory(Inventory inventory, ItemStack[] items) {
|
||||
inventory.clear();
|
||||
int index = 0;
|
||||
for (ItemStack item : items) {
|
||||
if (item != null) {
|
||||
inventory.setItem(index, item);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a player's current potion effects from a set of {@link PotionEffect[]}
|
||||
*
|
||||
* @param player The player to set the potion effects of
|
||||
* @param effects The array of {@link PotionEffect}s to set
|
||||
*/
|
||||
private static void setPlayerPotionEffects(Player player, PotionEffect[] effects) {
|
||||
for (PotionEffect effect : player.getActivePotionEffects()) {
|
||||
player.removePotionEffect(effect.getType());
|
||||
}
|
||||
for (PotionEffect effect : effects) {
|
||||
player.addPotionEffect(effect);
|
||||
}
|
||||
}
|
||||
|
||||
private static void nativeSyncPlayerAdvancements(final Player player, final List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> advancementRecords) {
|
||||
final Object playerAdvancements = AdvancementUtils.getPlayerAdvancements(player);
|
||||
|
||||
// Clear
|
||||
AdvancementUtils.clearPlayerAdvancements(playerAdvancements);
|
||||
AdvancementUtils.clearVisibleAdvancements(playerAdvancements);
|
||||
|
||||
advancementRecords.forEach(advancementRecord -> {
|
||||
NamespacedKey namespacedKey = Objects.requireNonNull(
|
||||
NamespacedKey.fromString(advancementRecord.key()),
|
||||
"Invalid Namespaced key of " + advancementRecord.key()
|
||||
);
|
||||
|
||||
Advancement bukkitAdvancement = Bukkit.getAdvancement(namespacedKey);
|
||||
if (bukkitAdvancement == null) {
|
||||
plugin.getLogger().log(Level.WARNING, "Ignored advancement '{0}' - it doesn't exist anymore?", namespacedKey);
|
||||
return;
|
||||
}
|
||||
|
||||
Object advancement = AdvancementUtils.getHandle(bukkitAdvancement);
|
||||
Map<String, Date> criteriaList = advancementRecord.criteriaMap();
|
||||
{
|
||||
Map<String, Object> nativeCriteriaMap = new HashMap<>();
|
||||
criteriaList.forEach((criteria, date) ->
|
||||
nativeCriteriaMap.put(criteria, AdvancementUtils.newCriterionProgress(date))
|
||||
);
|
||||
Object nativeAdvancementProgress = AdvancementUtils.newAdvancementProgress(nativeCriteriaMap);
|
||||
|
||||
AdvancementUtils.startProgress(playerAdvancements, advancement, nativeAdvancementProgress);
|
||||
}
|
||||
});
|
||||
AdvancementUtils.ensureAllVisible(playerAdvancements); // Set all completed advancement is visible
|
||||
AdvancementUtils.markPlayerAdvancementsFirst(playerAdvancements); // Mark the sending of visible advancement as the first
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a player's advancements and progress to match the advancementData
|
||||
*
|
||||
* @param player The player to set the advancements of
|
||||
* @param advancementData The ArrayList of {@link me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate}s to set
|
||||
*/
|
||||
private static void setPlayerAdvancements(Player player, List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> advancementData, PlayerData data) {
|
||||
// Temporarily disable advancement announcing if needed
|
||||
boolean announceAdvancementUpdate = false;
|
||||
if (Boolean.TRUE.equals(player.getWorld().getGameRuleValue(GameRule.ANNOUNCE_ADVANCEMENTS))) {
|
||||
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false);
|
||||
announceAdvancementUpdate = true;
|
||||
}
|
||||
final boolean finalAnnounceAdvancementUpdate = announceAdvancementUpdate;
|
||||
|
||||
// Run async because advancement loading is very slow
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
|
||||
// Apply the advancements to the player
|
||||
final Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
|
||||
while (serverAdvancements.hasNext()) { // Iterate through all advancements
|
||||
boolean correctExperienceCheck = false; // Determines whether the experience might have changed warranting an update
|
||||
Advancement advancement = serverAdvancements.next();
|
||||
AdvancementProgress playerProgress = player.getAdvancementProgress(advancement);
|
||||
for (me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate record : advancementData) {
|
||||
// If the advancement is one on the data
|
||||
if (record.key().equals(advancement.getKey().getNamespace() + ":" + advancement.getKey().getKey())) {
|
||||
|
||||
// Award all criteria that the player does not have that they do on the cache
|
||||
ArrayList<String> currentlyAwardedCriteria = new ArrayList<>(playerProgress.getAwardedCriteria());
|
||||
for (String awardCriteria : record.criteriaMap().keySet()) {
|
||||
if (!playerProgress.getAwardedCriteria().contains(awardCriteria)) {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> player.getAdvancementProgress(advancement).awardCriteria(awardCriteria));
|
||||
correctExperienceCheck = true;
|
||||
}
|
||||
currentlyAwardedCriteria.remove(awardCriteria);
|
||||
}
|
||||
|
||||
// Revoke all criteria that the player does have but should not
|
||||
for (String awardCriteria : currentlyAwardedCriteria) {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> player.getAdvancementProgress(advancement).revokeCriteria(awardCriteria));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the player's experience in case the advancement changed that
|
||||
if (correctExperienceCheck) {
|
||||
if (Settings.syncExperience) {
|
||||
setPlayerExperience(player, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable announcing advancements (back on main thread again)
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (finalAnnounceAdvancementUpdate) {
|
||||
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a player's statistics (in the Statistic menu)
|
||||
*
|
||||
* @param player The player to set the statistics of
|
||||
* @param statisticData The {@link me.william278.husksync.bukkit.data.DataSerializer.StatisticData} to set
|
||||
*/
|
||||
private static void setPlayerStatistics(Player player, me.william278.husksync.bukkit.data.DataSerializer.StatisticData statisticData) {
|
||||
// Set untyped statistics
|
||||
for (Statistic statistic : statisticData.untypedStatisticValues().keySet()) {
|
||||
player.setStatistic(statistic, statisticData.untypedStatisticValues().get(statistic));
|
||||
}
|
||||
|
||||
// Set block statistics
|
||||
for (Statistic statistic : statisticData.blockStatisticValues().keySet()) {
|
||||
for (Material blockMaterial : statisticData.blockStatisticValues().get(statistic).keySet()) {
|
||||
player.setStatistic(statistic, blockMaterial, statisticData.blockStatisticValues().get(statistic).get(blockMaterial));
|
||||
}
|
||||
}
|
||||
|
||||
// Set item statistics
|
||||
for (Statistic statistic : statisticData.itemStatisticValues().keySet()) {
|
||||
for (Material itemMaterial : statisticData.itemStatisticValues().get(statistic).keySet()) {
|
||||
player.setStatistic(statistic, itemMaterial, statisticData.itemStatisticValues().get(statistic).get(itemMaterial));
|
||||
}
|
||||
}
|
||||
|
||||
// Set entity statistics
|
||||
for (Statistic statistic : statisticData.entityStatisticValues().keySet()) {
|
||||
for (EntityType entityType : statisticData.entityStatisticValues().get(statistic).keySet()) {
|
||||
player.setStatistic(statistic, entityType, statisticData.entityStatisticValues().get(statistic).get(entityType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a player's exp level, exp points & score
|
||||
*
|
||||
* @param player The {@link Player} to set
|
||||
* @param data The {@link PlayerData} to set them
|
||||
*/
|
||||
private static void setPlayerExperience(Player player, PlayerData data) {
|
||||
player.setTotalExperience(data.getTotalExperience());
|
||||
player.setLevel(data.getExpLevel());
|
||||
player.setExp(data.getExpProgress());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a player's location from {@link me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation} data
|
||||
*
|
||||
* @param player The {@link Player} to teleport
|
||||
* @param location The {@link me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation}
|
||||
*/
|
||||
private static void setPlayerLocation(Player player, me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation location) {
|
||||
// Don't teleport if the location is invalid
|
||||
if (location == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the world; if the names match, use that
|
||||
World world = Bukkit.getWorld(location.worldName());
|
||||
if (world == null) {
|
||||
|
||||
// If the names don't match, find the corresponding world with the same dimension environment
|
||||
for (World worldOnServer : Bukkit.getWorlds()) {
|
||||
if (worldOnServer.getEnvironment().equals(location.environment())) {
|
||||
world = worldOnServer;
|
||||
}
|
||||
}
|
||||
|
||||
// If that still fails, return
|
||||
if (world == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Teleport the player
|
||||
player.teleport(new Location(world, location.x(), location.y(), location.z(), location.yaw(), location.pitch()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Correctly set a {@link Player}'s health data
|
||||
*
|
||||
* @param player The {@link Player} to set
|
||||
* @param health Health to set to the player
|
||||
* @param maxHealth Max health to set to the player
|
||||
* @param healthScale Health scaling to apply to the player
|
||||
*/
|
||||
private static void setPlayerHealth(Player player, double health, double maxHealth, double healthScale) {
|
||||
// Set max health
|
||||
if (maxHealth != 0D) {
|
||||
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).setBaseValue(maxHealth);
|
||||
}
|
||||
|
||||
// Set health
|
||||
double currentHealth = player.getHealth();
|
||||
if (health != currentHealth) player.setHealth(currentHealth > maxHealth ? maxHealth : health);
|
||||
|
||||
// Set health scaling if needed
|
||||
if (healthScale != 0D) {
|
||||
player.setHealthScale(healthScale);
|
||||
} else {
|
||||
player.setHealthScale(maxHealth);
|
||||
}
|
||||
player.setHealthScaled(healthScale != 0D);
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package net.william278.husksync.bukkit.util.nms;
|
||||
|
||||
import net.william278.husksync.util.ThrowSupplier;
|
||||
import org.bukkit.advancement.Advancement;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class AdvancementUtils {
|
||||
|
||||
public final static Class<?> PLAYER_ADVANCEMENT;
|
||||
private final static Field PLAYER_ADVANCEMENTS_MAP;
|
||||
private final static Field PLAYER_VISIBLE_SET;
|
||||
private final static Field PLAYER_ADVANCEMENTS;
|
||||
private final static Field CRITERIA_MAP;
|
||||
private final static Field CRITERIA_DATE;
|
||||
private final static Field IS_FIRST_PACKET;
|
||||
private final static Method GET_HANDLE;
|
||||
private final static Method START_PROGRESS;
|
||||
private final static Method ENSURE_ALL_VISIBLE;
|
||||
private final static Class<?> ADVANCEMENT_PROGRESS;
|
||||
private final static Class<?> CRITERION_PROGRESS;
|
||||
|
||||
static {
|
||||
Class<?> SERVER_PLAYER = MinecraftVersionUtils.getMinecraftClass("level.EntityPlayer");
|
||||
PLAYER_ADVANCEMENTS = ThrowSupplier.get(() -> SERVER_PLAYER.getDeclaredField("cs"));
|
||||
PLAYER_ADVANCEMENTS.setAccessible(true);
|
||||
|
||||
Class<?> CRAFT_ADVANCEMENT = MinecraftVersionUtils.getBukkitClass("advancement.CraftAdvancement");
|
||||
GET_HANDLE = ThrowSupplier.get(() -> CRAFT_ADVANCEMENT.getDeclaredMethod("getHandle"));
|
||||
|
||||
ADVANCEMENT_PROGRESS = ThrowSupplier.get(() -> Class.forName("net.minecraft.advancements.AdvancementProgress"));
|
||||
CRITERIA_MAP = ThrowSupplier.get(() -> ADVANCEMENT_PROGRESS.getDeclaredField("a"));
|
||||
CRITERIA_MAP.setAccessible(true);
|
||||
|
||||
CRITERION_PROGRESS = ThrowSupplier.get(() -> Class.forName("net.minecraft.advancements.CriterionProgress"));
|
||||
CRITERIA_DATE = ThrowSupplier.get(() -> CRITERION_PROGRESS.getDeclaredField("b"));
|
||||
CRITERIA_DATE.setAccessible(true);
|
||||
|
||||
Class<?> ADVANCEMENT = ThrowSupplier.get(() -> Class.forName("net.minecraft.advancements.Advancement"));
|
||||
|
||||
PLAYER_ADVANCEMENT = MinecraftVersionUtils.getMinecraftClass("AdvancementDataPlayer");
|
||||
PLAYER_ADVANCEMENTS_MAP = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredField("h"));
|
||||
PLAYER_ADVANCEMENTS_MAP.setAccessible(true);
|
||||
|
||||
PLAYER_VISIBLE_SET = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredField("i"));
|
||||
PLAYER_VISIBLE_SET.setAccessible(true);
|
||||
|
||||
START_PROGRESS = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredMethod("a", ADVANCEMENT, ADVANCEMENT_PROGRESS));
|
||||
START_PROGRESS.setAccessible(true);
|
||||
|
||||
ENSURE_ALL_VISIBLE = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredMethod("c"));
|
||||
ENSURE_ALL_VISIBLE.setAccessible(true);
|
||||
|
||||
IS_FIRST_PACKET = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredField("n"));
|
||||
IS_FIRST_PACKET.setAccessible(true);
|
||||
}
|
||||
|
||||
public static void markPlayerAdvancementsFirst(final Object playerAdvancements) {
|
||||
try {
|
||||
IS_FIRST_PACKET.set(playerAdvancements, true);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Object getPlayerAdvancements(Player player) {
|
||||
Object nativePlayer = EntityUtils.getHandle(player);
|
||||
try {
|
||||
return PLAYER_ADVANCEMENTS.get(nativePlayer);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearPlayerAdvancements(final Object playerAdvancement) {
|
||||
try {
|
||||
((Map<?, ?>) PLAYER_ADVANCEMENTS_MAP.get(playerAdvancement))
|
||||
.clear();
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Object getHandle(Advancement advancement) {
|
||||
try {
|
||||
return GET_HANDLE.invoke(advancement);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Object newCriterionProgress(final Date date) {
|
||||
try {
|
||||
Object nativeCriterionProgress = CRITERION_PROGRESS.getDeclaredConstructor().newInstance();
|
||||
CRITERIA_DATE.set(nativeCriterionProgress, date);
|
||||
return nativeCriterionProgress;
|
||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked") // Suppress unchecked cast warnings here
|
||||
public static Object newAdvancementProgress(final Map<String, Object> criteria) {
|
||||
try {
|
||||
Object nativeAdvancementProgress = ADVANCEMENT_PROGRESS.getDeclaredConstructor().newInstance();
|
||||
|
||||
final Map<String, Object> criteriaMap = (Map<String, Object>) CRITERIA_MAP.get(nativeAdvancementProgress);
|
||||
criteriaMap.putAll(criteria);
|
||||
|
||||
return nativeAdvancementProgress;
|
||||
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void startProgress(final Object playerAdvancements, final Object advancement, final Object advancementProgress) {
|
||||
try {
|
||||
START_PROGRESS.invoke(playerAdvancements, advancement, advancementProgress);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ensureAllVisible(final Object playerAdvancements) {
|
||||
try {
|
||||
ENSURE_ALL_VISIBLE.invoke(playerAdvancements);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearVisibleAdvancements(final Object playerAdvancements) {
|
||||
try {
|
||||
((Set<?>) PLAYER_VISIBLE_SET.get(playerAdvancements))
|
||||
.clear();
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package net.william278.husksync.bukkit.util.nms;
|
||||
|
||||
import net.william278.husksync.util.ThrowSupplier;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class EntityUtils {
|
||||
|
||||
private final static Method GET_HANDLE;
|
||||
|
||||
static {
|
||||
final Class<?> CRAFT_ENTITY = MinecraftVersionUtils.getBukkitClass("entity.CraftEntity");
|
||||
GET_HANDLE = ThrowSupplier.get(() -> CRAFT_ENTITY.getDeclaredMethod("getHandle"));
|
||||
}
|
||||
|
||||
public static Object getHandle(LivingEntity livingEntity) throws RuntimeException {
|
||||
try {
|
||||
return GET_HANDLE.invoke(livingEntity);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package net.william278.husksync.bukkit.util.nms;
|
||||
|
||||
import net.william278.husksync.util.ThrowSupplier;
|
||||
import net.william278.husksync.util.VersionUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
public class MinecraftVersionUtils {
|
||||
|
||||
public final static String CRAFTBUKKIT_PACKAGE_PATH = Bukkit.getServer().getClass().getPackage().getName();
|
||||
|
||||
public final static String PACKAGE_VERSION = CRAFTBUKKIT_PACKAGE_PATH.split("\\.")[3];
|
||||
public final static VersionUtils.Version SERVER_VERSION
|
||||
= VersionUtils.Version.of(Bukkit.getBukkitVersion().split("-")[0]);
|
||||
public final static String MINECRAFT_PACKAGE = SERVER_VERSION.compareTo(VersionUtils.Version.of("1.17")) < 0 ?
|
||||
"net.minecraft.server.".concat(PACKAGE_VERSION) : "net.minecraft.server";
|
||||
|
||||
public static Class<?> getBukkitClass(String path) {
|
||||
return ThrowSupplier.get(() -> Class.forName(CRAFTBUKKIT_PACKAGE_PATH.concat(".").concat(path)));
|
||||
}
|
||||
|
||||
public static Class<?> getMinecraftClass(String path) {
|
||||
return ThrowSupplier.get(() -> Class.forName(MINECRAFT_PACKAGE.concat(".").concat(path)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,880 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import de.tr7zw.changeme.nbtapi.NBTCompound;
|
||||
import de.tr7zw.changeme.nbtapi.NBTPersistentDataContainer;
|
||||
import lombok.*;
|
||||
import net.william278.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.config.Settings.SynchronizationSettings.AttributeSettings;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.advancement.AdvancementProgress;
|
||||
import org.bukkit.attribute.AttributeInstance;
|
||||
import org.bukkit.attribute.AttributeModifier;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.inventory.EquipmentSlotGroup;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Range;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static net.william278.husksync.util.BukkitKeyedAdapter.*;
|
||||
|
||||
public abstract class BukkitData implements Data {
|
||||
|
||||
@Override
|
||||
public final void apply(@NotNull UserDataHolder dataHolder, @NotNull HuskSync plugin) throws IllegalStateException {
|
||||
this.apply((BukkitUser) dataHolder, (BukkitHuskSync) plugin);
|
||||
}
|
||||
|
||||
public abstract void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException;
|
||||
|
||||
@Getter
|
||||
public static abstract class Items extends BukkitData implements Data.Items {
|
||||
|
||||
private final @Nullable ItemStack @NotNull [] contents;
|
||||
|
||||
private Items(@Nullable ItemStack @NotNull [] contents) {
|
||||
this.contents = Arrays.stream(contents.clone())
|
||||
.map(i -> i == null || i.getType() == Material.AIR ? null : i)
|
||||
.toArray(ItemStack[]::new);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Stack @NotNull [] getStack() {
|
||||
return Arrays.stream(contents)
|
||||
.map(stack -> stack != null ? new Stack(
|
||||
stack.getType().getKey().toString(),
|
||||
stack.getAmount(),
|
||||
stack.hasItemMeta() ? (Objects.requireNonNull(
|
||||
stack.getItemMeta()).hasDisplayName() ? stack.getItemMeta().getDisplayName() : null)
|
||||
: null,
|
||||
stack.hasItemMeta() ? (Objects.requireNonNull(
|
||||
stack.getItemMeta()).hasLore() ? stack.getItemMeta().getLore() : null)
|
||||
: null,
|
||||
stack.hasItemMeta() && Objects.requireNonNull(stack.getItemMeta()).hasEnchants() ?
|
||||
stack.getItemMeta().getEnchants().keySet().stream()
|
||||
.map(enchantment -> enchantment.getKey().getKey())
|
||||
.toList()
|
||||
: List.of()
|
||||
) : null)
|
||||
.toArray(Stack[]::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
Arrays.fill(contents, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContents(@NotNull Data.Items contents) {
|
||||
this.setContents(((BukkitData.Items) contents).getContents());
|
||||
}
|
||||
|
||||
public void setContents(@Nullable ItemStack @NotNull [] contents) {
|
||||
// Ensure the array is the correct length for the inventory
|
||||
if (contents.length != this.contents.length) {
|
||||
contents = Arrays.copyOf(contents, this.contents.length);
|
||||
}
|
||||
System.arraycopy(contents, 0, this.contents, 0, this.contents.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof BukkitData.Items items) {
|
||||
return Arrays.equals(contents, items.getContents());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
public static class Inventory extends BukkitData.Items implements Data.Items.Inventory {
|
||||
|
||||
@Range(from = 0, to = 8)
|
||||
private int heldItemSlot;
|
||||
|
||||
private Inventory(@Nullable ItemStack @NotNull [] contents, int heldItemSlot) {
|
||||
super(contents);
|
||||
this.heldItemSlot = heldItemSlot;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Items.Inventory from(@Nullable ItemStack @NotNull [] contents, int heldItemSlot) {
|
||||
return new BukkitData.Items.Inventory(contents, heldItemSlot);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Items.Inventory empty() {
|
||||
return new BukkitData.Items.Inventory(new ItemStack[INVENTORY_SLOT_COUNT], 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSlotCount() {
|
||||
return INVENTORY_SLOT_COUNT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
final Player player = user.getPlayer();
|
||||
this.clearInventoryCraftingSlots(player);
|
||||
player.setItemOnCursor(null);
|
||||
player.getInventory().setContents(plugin.setMapViews(getContents()));
|
||||
player.getInventory().setHeldItemSlot(heldItemSlot);
|
||||
//noinspection UnstableApiUsage
|
||||
player.updateInventory();
|
||||
}
|
||||
|
||||
private void clearInventoryCraftingSlots(@NotNull Player player) {
|
||||
final org.bukkit.inventory.Inventory inventory = player.getOpenInventory().getTopInventory();
|
||||
if (inventory.getType() == InventoryType.CRAFTING) {
|
||||
for (int slot = 0; slot < 5; slot++) {
|
||||
inventory.setItem(slot, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class EnderChest extends BukkitData.Items implements Data.Items.EnderChest {
|
||||
|
||||
private EnderChest(@Nullable ItemStack @NotNull [] contents) {
|
||||
super(contents);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Items.EnderChest adapt(@Nullable ItemStack @NotNull [] contents) {
|
||||
return new BukkitData.Items.EnderChest(contents);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Items.EnderChest adapt(@NotNull Collection<ItemStack> items) {
|
||||
return adapt(items.toArray(ItemStack[]::new));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Items.EnderChest empty() {
|
||||
return new BukkitData.Items.EnderChest(new ItemStack[ENDER_CHEST_SLOT_COUNT]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
ItemStack[] fullContents = plugin.setMapViews(getContents());
|
||||
ItemStack[] enderChestContents = Arrays.copyOf(fullContents, Math.min(fullContents.length, user.getPlayer().getEnderChest().getSize()));
|
||||
user.getPlayer().getEnderChest().setContents(enderChestContents);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class ItemArray extends BukkitData.Items implements Data.Items {
|
||||
|
||||
private ItemArray(@Nullable ItemStack @NotNull [] contents) {
|
||||
super(contents);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static ItemArray adapt(@NotNull Collection<ItemStack> drops) {
|
||||
return new ItemArray(drops.toArray(ItemStack[]::new));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static ItemArray adapt(@Nullable ItemStack @NotNull [] drops) {
|
||||
return new ItemArray(drops);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
throw new UnsupportedOperationException("A generic item array cannot be applied to a player");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class PotionEffects extends BukkitData implements Data.PotionEffects {
|
||||
|
||||
private final Collection<PotionEffect> effects;
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> sei) {
|
||||
return new BukkitData.PotionEffects(Lists.newArrayList(sei.stream().filter(e -> !e.isAmbient()).toList()));
|
||||
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.PotionEffects adapt(@NotNull Collection<Effect> effects) {
|
||||
return from(effects.stream()
|
||||
.map(effect -> {
|
||||
final PotionEffectType type = matchEffectType(effect.type());
|
||||
return type != null ? new PotionEffect(
|
||||
type,
|
||||
effect.duration(),
|
||||
effect.amplifier(),
|
||||
effect.isAmbient(),
|
||||
effect.showParticles(),
|
||||
effect.hasIcon()
|
||||
) : null;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@SuppressWarnings("unused")
|
||||
public static BukkitData.PotionEffects empty() {
|
||||
return new BukkitData.PotionEffects(Lists.newArrayList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
final Player player = user.getPlayer();
|
||||
for (PotionEffect effect : player.getActivePotionEffects()) {
|
||||
player.removePotionEffect(effect.getType());
|
||||
}
|
||||
for (PotionEffect effect : this.getEffects()) {
|
||||
player.addPotionEffect(effect);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
@Unmodifiable
|
||||
public List<Effect> getActiveEffects() {
|
||||
return effects.stream()
|
||||
.map(potionEffect -> new Effect(
|
||||
potionEffect.getType().getKey().toString(),
|
||||
potionEffect.getAmplifier(),
|
||||
potionEffect.getDuration(),
|
||||
potionEffect.isAmbient(),
|
||||
potionEffect.hasParticles(),
|
||||
potionEffect.hasIcon()
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class Advancements extends BukkitData implements Data.Advancements {
|
||||
|
||||
private List<Advancement> completed;
|
||||
|
||||
// Iterate through the server advancement set and add all advancements to the list
|
||||
@NotNull
|
||||
public static BukkitData.Advancements adapt(@NotNull Player player) {
|
||||
final List<Advancement> advancements = Lists.newArrayList();
|
||||
forEachAdvancement(advancement -> {
|
||||
final AdvancementProgress advancementProgress = player.getAdvancementProgress(advancement);
|
||||
final Map<String, Date> awardedCriteria = Maps.newHashMap();
|
||||
|
||||
advancementProgress.getAwardedCriteria().forEach(criteriaKey -> awardedCriteria.put(criteriaKey,
|
||||
advancementProgress.getDateAwarded(criteriaKey)));
|
||||
|
||||
// Only save the advancement if criteria has been completed
|
||||
if (!awardedCriteria.isEmpty()) {
|
||||
advancements.add(Advancement.adapt(advancement.getKey().toString(), awardedCriteria));
|
||||
}
|
||||
});
|
||||
return new BukkitData.Advancements(advancements);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Advancements from(@NotNull List<Advancement> advancements) {
|
||||
return new BukkitData.Advancements(advancements);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
plugin.runAsync(() -> forEachAdvancement(advancement -> {
|
||||
final Player player = user.getPlayer();
|
||||
final AdvancementProgress progress = player.getAdvancementProgress(advancement);
|
||||
final Optional<Advancement> record = completed.stream()
|
||||
.filter(r -> r.getKey().equals(advancement.getKey().toString()))
|
||||
.findFirst();
|
||||
if (record.isEmpty()) {
|
||||
this.setAdvancement(plugin, advancement, player, user, List.of(), progress.getAwardedCriteria());
|
||||
return;
|
||||
}
|
||||
|
||||
final Map<String, Date> criteria = record.get().getCompletedCriteria();
|
||||
this.setAdvancement(
|
||||
plugin, advancement, player, user,
|
||||
criteria.keySet().stream().filter(key -> !progress.getAwardedCriteria().contains(key)).toList(),
|
||||
progress.getAwardedCriteria().stream().filter(key -> !criteria.containsKey(key)).toList()
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
private void setAdvancement(@NotNull HuskSync plugin,
|
||||
@NotNull org.bukkit.advancement.Advancement advancement,
|
||||
@NotNull Player player,
|
||||
@NotNull BukkitUser user,
|
||||
@NotNull Collection<String> toAward,
|
||||
@NotNull Collection<String> toRevoke) {
|
||||
plugin.runSync(() -> {
|
||||
// Track player exp level & progress
|
||||
final int expLevel = player.getLevel();
|
||||
final float expProgress = player.getExp();
|
||||
|
||||
// Award and revoke advancement criteria
|
||||
final AdvancementProgress progress = player.getAdvancementProgress(advancement);
|
||||
toAward.forEach(progress::awardCriteria);
|
||||
toRevoke.forEach(progress::revokeCriteria);
|
||||
|
||||
// Set player experience and level (prevent advancement awards applying twice), reset game rule
|
||||
if (!toAward.isEmpty()
|
||||
&& (player.getLevel() != expLevel || player.getExp() != expProgress)) {
|
||||
player.setLevel(expLevel);
|
||||
player.setExp(expProgress);
|
||||
}
|
||||
}, user);
|
||||
}
|
||||
|
||||
// Performs a consuming function for every advancement registered on the server
|
||||
private static void forEachAdvancement(@NotNull ThrowingConsumer<org.bukkit.advancement.Advancement> consumer) {
|
||||
Bukkit.getServer().advancementIterator().forEachRemaining(consumer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class Location extends BukkitData implements Data.Location, Adaptable {
|
||||
|
||||
@SerializedName("x")
|
||||
private double x;
|
||||
@SerializedName("y")
|
||||
private double y;
|
||||
@SerializedName("z")
|
||||
private double z;
|
||||
@SerializedName("yaw")
|
||||
private float yaw;
|
||||
@SerializedName("pitch")
|
||||
private float pitch;
|
||||
@SerializedName("world")
|
||||
private World world;
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Location from(double x, double y, double z,
|
||||
float yaw, float pitch, @NotNull World world) {
|
||||
return new BukkitData.Location(x, y, z, yaw, pitch, world);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Location adapt(@NotNull org.bukkit.Location location) {
|
||||
return from(
|
||||
location.getX(),
|
||||
location.getY(),
|
||||
location.getZ(),
|
||||
location.getYaw(),
|
||||
location.getPitch(),
|
||||
new World(
|
||||
Objects.requireNonNull(location.getWorld(), "World is null").getName(),
|
||||
location.getWorld().getUID(),
|
||||
location.getWorld().getEnvironment().name()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
try {
|
||||
final org.bukkit.Location location = new org.bukkit.Location(
|
||||
Bukkit.getWorld(world.name()), x, y, z, yaw, pitch
|
||||
);
|
||||
user.getPlayer().teleport(location);
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException("Failed to apply location", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class Statistics extends BukkitData implements Data.Statistics, Adaptable {
|
||||
|
||||
@SerializedName("generic")
|
||||
private Map<String, Integer> genericStatistics;
|
||||
@SerializedName("blocks")
|
||||
private Map<String, Map<String, Integer>> blockStatistics;
|
||||
@SerializedName("items")
|
||||
private Map<String, Map<String, Integer>> itemStatistics;
|
||||
@SerializedName("entities")
|
||||
private Map<String, Map<String, Integer>> entityStatistics;
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Statistics adapt(@NotNull Player player) {
|
||||
final Map<String, Integer> generic = Maps.newHashMap();
|
||||
final Map<String, Map<String, Integer>> blocks = Maps.newHashMap(),
|
||||
items = Maps.newHashMap(), entities = Maps.newHashMap();
|
||||
Registry.STATISTIC.forEach(id -> {
|
||||
switch (id.getType()) {
|
||||
case UNTYPED -> addStatistic(player, id, generic);
|
||||
// Todo - Future - Use BLOCK and ITEM registries when API stabilizes
|
||||
case BLOCK -> addStatistic(player, id, Registry.MATERIAL, blocks);
|
||||
case ITEM -> addStatistic(player, id, Registry.MATERIAL, items);
|
||||
case ENTITY -> addStatistic(player, id, Registry.ENTITY_TYPE, entities);
|
||||
}
|
||||
});
|
||||
return new BukkitData.Statistics(generic, blocks, items, entities);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Statistics from(@NotNull Map<String, Integer> generic,
|
||||
@NotNull Map<String, Map<String, Integer>> blocks,
|
||||
@NotNull Map<String, Map<String, Integer>> items,
|
||||
@NotNull Map<String, Map<String, Integer>> entities) {
|
||||
return new BukkitData.Statistics(generic, blocks, items, entities);
|
||||
}
|
||||
|
||||
private static void addStatistic(@NotNull Player p, @NotNull Statistic id, @NotNull Map<String, Integer> map) {
|
||||
final int stat = p.getStatistic(id);
|
||||
if (stat != 0) {
|
||||
map.put(id.getKey().getKey(), stat);
|
||||
}
|
||||
}
|
||||
|
||||
private static <R extends Keyed> void addStatistic(@NotNull Player p, @NotNull Statistic id,
|
||||
@NotNull Registry<R> registry,
|
||||
@NotNull Map<String, Map<String, Integer>> map) {
|
||||
registry.forEach(i -> {
|
||||
try {
|
||||
int stat = 0;
|
||||
if (i instanceof Material mat && ((id.getType() == Statistic.Type.BLOCK && mat.isBlock())
|
||||
|| (id.getType() == Statistic.Type.ITEM && mat.isItem()))) {
|
||||
stat = p.getStatistic(id, mat);
|
||||
} else if (i instanceof EntityType ent && id.getType() == Statistic.Type.ENTITY) {
|
||||
stat = p.getStatistic(id, ent);
|
||||
}
|
||||
if (stat != 0) {
|
||||
map.compute(id.getKey().getKey(), (k, v) -> v == null ? Maps.newHashMap() : v)
|
||||
.put(i.getKey().getKey(), stat);
|
||||
}
|
||||
} catch (IllegalStateException ignored) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync p) {
|
||||
genericStatistics.forEach((k, v) -> applyStat(p, user, k, Statistic.Type.UNTYPED, v));
|
||||
blockStatistics.forEach((k, m) -> m.forEach((b, v) -> applyStat(p, user, k, Statistic.Type.BLOCK, v, b)));
|
||||
itemStatistics.forEach((k, m) -> m.forEach((i, v) -> applyStat(p, user, k, Statistic.Type.ITEM, v, i)));
|
||||
entityStatistics.forEach((k, m) -> m.forEach((e, v) -> applyStat(p, user, k, Statistic.Type.ENTITY, v, e)));
|
||||
}
|
||||
|
||||
private void applyStat(@NotNull HuskSync plugin, @NotNull UserDataHolder user, @NotNull String id,
|
||||
@NotNull Statistic.Type type, int value, @NotNull String... key) {
|
||||
final Player player = ((BukkitUser) user).getPlayer();
|
||||
final Statistic stat = matchStatistic(id);
|
||||
if (stat == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case UNTYPED -> player.setStatistic(stat, value);
|
||||
case BLOCK, ITEM -> {
|
||||
Material material = matchMaterial(key.length > 0 ? key[0] : null);
|
||||
if (material != null) {
|
||||
player.setStatistic(stat, material, value);
|
||||
}
|
||||
}
|
||||
case ENTITY -> {
|
||||
EntityType entity = matchEntityType(key.length > 0 ? key[0] : null);
|
||||
if (entity != null) {
|
||||
player.setStatistic(stat, entity, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable a) {
|
||||
plugin.log(Level.WARNING, "Failed to apply statistic " + id, a);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class PersistentData extends BukkitData implements Data.PersistentData {
|
||||
private final NBTCompound persistentData;
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.PersistentData adapt(@NotNull PersistentDataContainer persistentData) {
|
||||
return new BukkitData.PersistentData(new NBTPersistentDataContainer(persistentData));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.PersistentData from(@NotNull NBTCompound compound) {
|
||||
return new BukkitData.PersistentData(compound);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
final NBTPersistentDataContainer container = new NBTPersistentDataContainer(
|
||||
user.getPlayer().getPersistentDataContainer()
|
||||
);
|
||||
container.clearNBT();
|
||||
container.mergeCompound(persistentData);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public static class Attributes extends BukkitData implements Data.Attributes, Adaptable {
|
||||
|
||||
private List<Attribute> attributes;
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Attributes adapt(@NotNull Player player, @NotNull HuskSync plugin) {
|
||||
if (!Bukkit.isPrimaryThread()) {
|
||||
try {
|
||||
return Bukkit.getScheduler().callSyncMethod((Plugin) plugin, () -> adapt(player, plugin)).get();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to adapt attributes on main thread", e);
|
||||
}
|
||||
}
|
||||
|
||||
final List<Attribute> attributes = Lists.newArrayList();
|
||||
final AttributeSettings settings = plugin.getSettings().getSynchronization().getAttributes();
|
||||
Registry.ATTRIBUTE.forEach(id -> {
|
||||
final AttributeInstance instance = player.getAttribute(id);
|
||||
if (settings.isIgnoredAttribute(id.getKey().toString()) || instance == null) {
|
||||
return; // We don't sync attributes not marked as to be synced
|
||||
}
|
||||
attributes.add(adapt(instance, settings));
|
||||
});
|
||||
return new BukkitData.Attributes(attributes);
|
||||
}
|
||||
|
||||
public Optional<Attribute> getAttribute(@NotNull org.bukkit.attribute.Attribute id) {
|
||||
return attributes.stream().filter(attribute -> attribute.name().equals(id.getKey().toString())).findFirst();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Optional<Attribute> getAttribute(@NotNull String key) {
|
||||
final org.bukkit.attribute.Attribute attribute = matchAttribute(key);
|
||||
if (attribute == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return getAttribute(attribute);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Attribute adapt(@NotNull AttributeInstance instance, @NotNull AttributeSettings settings) {
|
||||
return new Attribute(
|
||||
instance.getAttribute().getKey().toString(),
|
||||
instance.getBaseValue(),
|
||||
instance.getModifiers().stream()
|
||||
.filter(modifier -> !settings.isIgnoredModifier(modifier.getName()))
|
||||
.filter(modifier -> modifier.getSlotGroup() != EquipmentSlotGroup.ANY)
|
||||
.map(BukkitData.Attributes::adapt).collect(Collectors.toSet())
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Modifier adapt(@NotNull AttributeModifier modifier) {
|
||||
return new Modifier(
|
||||
modifier.getKey().toString(),
|
||||
modifier.getAmount(),
|
||||
modifier.getOperation().ordinal(),
|
||||
modifier.getSlotGroup().toString()
|
||||
);
|
||||
}
|
||||
|
||||
private static void applyAttribute(@Nullable AttributeInstance instance, @Nullable Attribute attribute) {
|
||||
if (instance == null) {
|
||||
return;
|
||||
}
|
||||
instance.getModifiers().forEach(instance::removeModifier);
|
||||
instance.setBaseValue(attribute == null ? instance.getValue() : attribute.baseValue());
|
||||
if (attribute != null) {
|
||||
attribute.modifiers().stream()
|
||||
.filter(mod -> instance.getModifiers().stream().map(AttributeModifier::getName)
|
||||
.noneMatch(n -> n.equals(mod.name())))
|
||||
.distinct().filter(mod -> !mod.hasUuid())
|
||||
.forEach(mod -> instance.addModifier(adapt(mod)));
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static AttributeModifier adapt(@NotNull Modifier modifier) {
|
||||
return new AttributeModifier(
|
||||
Objects.requireNonNull(NamespacedKey.fromString(modifier.name())),
|
||||
modifier.amount(),
|
||||
AttributeModifier.Operation.values()[modifier.operation()],
|
||||
Optional.ofNullable(EquipmentSlotGroup.getByName(modifier.slotGroup())).orElse(EquipmentSlotGroup.ANY)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
if (!Bukkit.isPrimaryThread()) {
|
||||
try {
|
||||
Bukkit.getScheduler().callSyncMethod(plugin, () -> { this.apply(user, plugin); return null; }).get();
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to apply attributes on main thread", e);
|
||||
}
|
||||
}
|
||||
|
||||
final AttributeSettings settings = plugin.getSettings().getSynchronization().getAttributes();
|
||||
Registry.ATTRIBUTE.forEach(id -> {
|
||||
if (settings.isIgnoredAttribute(id.getKey().toString())) {
|
||||
return;
|
||||
}
|
||||
applyAttribute(user.getPlayer().getAttribute(id), getAttribute(id).orElse(null));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class Health extends BukkitData implements Data.Health, Adaptable {
|
||||
@SerializedName("health")
|
||||
private double health;
|
||||
@SerializedName("health_scale")
|
||||
private double healthScale;
|
||||
@SerializedName("is_health_scaled")
|
||||
private boolean isHealthScaled;
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Health from(double health, double scale, boolean isScaled) {
|
||||
return new BukkitData.Health(health, scale, isScaled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #from(double, double, boolean)} instead
|
||||
*/
|
||||
@NotNull
|
||||
@Deprecated(since = "3.5.4")
|
||||
public static BukkitData.Health from(double health, double scale) {
|
||||
return from(health, scale, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #from(double, double, boolean)} instead
|
||||
*/
|
||||
@NotNull
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
public static BukkitData.Health from(double health, @SuppressWarnings("unused") double max, double scale) {
|
||||
return from(health, scale, false);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Health adapt(@NotNull Player player) {
|
||||
return from(
|
||||
player.getHealth(),
|
||||
player.getHealthScale(),
|
||||
player.isHealthScaled()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
final Player player = user.getPlayer();
|
||||
|
||||
// Set health
|
||||
try {
|
||||
player.setHealth(Math.min(health, player.getMaxHealth()));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.WARNING, "Error setting %s's health to %s".formatted(player.getName(), health), e);
|
||||
}
|
||||
|
||||
// Set health scale
|
||||
double scale = healthScale <= 0 ? player.getMaxHealth() : healthScale;
|
||||
try {
|
||||
player.setHealthScale(scale);
|
||||
player.setHealthScaled(isHealthScaled);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.WARNING, "Error setting %s's health scale to %s".formatted(player.getName(), scale), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class Hunger extends BukkitData implements Data.Hunger, Adaptable {
|
||||
|
||||
@SerializedName("food_level")
|
||||
private int foodLevel;
|
||||
@SerializedName("saturation")
|
||||
private float saturation;
|
||||
@SerializedName("exhaustion")
|
||||
private float exhaustion;
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Hunger adapt(@NotNull Player player) {
|
||||
return from(player.getFoodLevel(), player.getSaturation(), player.getExhaustion());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Hunger from(int foodLevel, float saturation, float exhaustion) {
|
||||
return new BukkitData.Hunger(foodLevel, saturation, exhaustion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
final Player player = user.getPlayer();
|
||||
player.setFoodLevel(foodLevel);
|
||||
player.setSaturation(saturation);
|
||||
player.setExhaustion(exhaustion);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class Experience extends BukkitData implements Data.Experience, Adaptable {
|
||||
|
||||
@SerializedName("total_experience")
|
||||
private int totalExperience;
|
||||
|
||||
@SerializedName("exp_level")
|
||||
private int expLevel;
|
||||
|
||||
@SerializedName("exp_progress")
|
||||
private float expProgress;
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Experience from(int totalExperience, int expLevel, float expProgress) {
|
||||
return new BukkitData.Experience(totalExperience, expLevel, expProgress);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Experience adapt(@NotNull Player player) {
|
||||
return from(player.getTotalExperience(), player.getLevel(), player.getExp());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
final Player player = user.getPlayer();
|
||||
player.setTotalExperience(totalExperience);
|
||||
player.setLevel(expLevel);
|
||||
player.setExp(expProgress);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class GameMode extends BukkitData implements Data.GameMode, Adaptable {
|
||||
|
||||
@SerializedName("game_mode")
|
||||
private String gameMode;
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.GameMode from(@NotNull String gameMode) {
|
||||
return new BukkitData.GameMode(gameMode);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
@SuppressWarnings("unused")
|
||||
public static BukkitData.GameMode from(@NotNull String gameMode, boolean allowFlight, boolean isFlying) {
|
||||
return new BukkitData.GameMode(gameMode);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.GameMode adapt(@NotNull Player player) {
|
||||
return from(player.getGameMode().name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
user.getPlayer().setGameMode(org.bukkit.GameMode.valueOf(gameMode));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class FlightStatus extends BukkitData implements Data.FlightStatus, Adaptable {
|
||||
|
||||
@SerializedName("allow_flight")
|
||||
private boolean allowFlight;
|
||||
@SerializedName("is_flying")
|
||||
private boolean flying;
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.FlightStatus from(boolean allowFlight, boolean flying) {
|
||||
return new BukkitData.FlightStatus(allowFlight, allowFlight && flying);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.FlightStatus adapt(@NotNull Player player) {
|
||||
return from(player.getAllowFlight(), player.isFlying());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
final Player player = user.getPlayer();
|
||||
player.setAllowFlight(allowFlight);
|
||||
player.setFlying(allowFlight && flying);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import de.tr7zw.changeme.nbtapi.NBT;
|
||||
import de.tr7zw.changeme.nbtapi.NBTCompound;
|
||||
import de.tr7zw.changeme.nbtapi.NBTContainer;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBTCompoundList;
|
||||
import de.tr7zw.changeme.nbtapi.utils.DataFixerUtil;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.api.HuskSyncAPI;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static net.william278.husksync.data.BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
|
||||
import static net.william278.husksync.data.Data.Items.Inventory.HELD_ITEM_SLOT_TAG;
|
||||
import static net.william278.husksync.data.Data.Items.Inventory.ITEMS_TAG;
|
||||
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class BukkitSerializer {
|
||||
|
||||
protected final HuskSync plugin;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public BukkitSerializer(@NotNull HuskSyncAPI api) {
|
||||
this.plugin = api.getPlugin();
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
@NotNull
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
public static class Inventory extends BukkitSerializer implements Serializer<BukkitData.Items.Inventory>,
|
||||
ItemDeserializer {
|
||||
|
||||
public Inventory(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.Items.Inventory deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
|
||||
throws DeserializationException {
|
||||
final ReadWriteNBT root = NBT.parseNBT(serialized);
|
||||
final ReadWriteNBT items = root.hasTag(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null;
|
||||
return BukkitData.Items.Inventory.from(
|
||||
items != null ? getItems(items, dataMcVersion) : new ItemStack[INVENTORY_SLOT_COUNT],
|
||||
root.hasTag(HELD_ITEM_SLOT_TAG) ? root.getInteger(HELD_ITEM_SLOT_TAG) : 0
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.Items.Inventory deserialize(@NotNull String serialized) {
|
||||
return deserialize(serialized, plugin.getMinecraftVersion());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.Items.Inventory data) throws SerializationException {
|
||||
final ReadWriteNBT root = NBT.createNBTObject();
|
||||
root.setItemStackArray(ITEMS_TAG, data.getContents());
|
||||
root.setInteger(HELD_ITEM_SLOT_TAG, data.getHeldItemSlot());
|
||||
return root.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class EnderChest extends BukkitSerializer implements Serializer<BukkitData.Items.EnderChest>,
|
||||
ItemDeserializer {
|
||||
|
||||
public EnderChest(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.Items.EnderChest deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
|
||||
throws DeserializationException {
|
||||
final ItemStack[] items = getItems(NBT.parseNBT(serialized), dataMcVersion);
|
||||
return items == null ? BukkitData.Items.EnderChest.empty() : BukkitData.Items.EnderChest.adapt(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.Items.EnderChest deserialize(@NotNull String serialized) {
|
||||
return deserialize(serialized, plugin.getMinecraftVersion());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.Items.EnderChest data) throws SerializationException {
|
||||
return NBT.itemStackArrayToNBT(data.getContents()).toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Utility interface for deserializing and upgrading item stacks from legacy versions
|
||||
private interface ItemDeserializer {
|
||||
|
||||
@Nullable
|
||||
default ItemStack[] getItems(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion) {
|
||||
if (mcVersion.compareTo(getPlugin().getMinecraftVersion()) < 0) {
|
||||
return upgradeItemStacks((NBTCompound) tag, mcVersion);
|
||||
}
|
||||
return NBT.itemStackArrayFromNBT(tag);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ItemStack @NotNull [] upgradeItemStacks(@NotNull NBTCompound itemsNbt, @NotNull Version mcVersion) {
|
||||
final ReadWriteNBTCompoundList items = itemsNbt.getCompoundList("items");
|
||||
final ItemStack[] itemStacks = new ItemStack[itemsNbt.getInteger("size")];
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
if (items.get(i) == null) {
|
||||
itemStacks[i] = new ItemStack(Material.AIR);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
itemStacks[i] = NBT.itemStackFromNBT(upgradeItemData(items.get(i), mcVersion));
|
||||
} catch (Throwable e) {
|
||||
itemStacks[i] = new ItemStack(Material.AIR);
|
||||
}
|
||||
}
|
||||
return itemStacks;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ReadWriteNBT upgradeItemData(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion)
|
||||
throws NoSuchFieldException, IllegalAccessException {
|
||||
return DataFixerUtil.fixUpItemData(
|
||||
tag,
|
||||
getPlugin().getDataVersion(mcVersion),
|
||||
DataFixerUtil.getCurrentVersion()
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
}
|
||||
|
||||
public static class PotionEffects extends BukkitSerializer implements Serializer<BukkitData.PotionEffects> {
|
||||
|
||||
private static final TypeToken<List<Data.PotionEffects.Effect>> TYPE = new TypeToken<>() {
|
||||
};
|
||||
|
||||
public PotionEffects(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.PotionEffects deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
return BukkitData.PotionEffects.adapt(
|
||||
plugin.getGson().fromJson(serialized, TYPE.getType())
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.PotionEffects element) throws SerializationException {
|
||||
return plugin.getGson().toJson(element.getActiveEffects());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Advancements extends BukkitSerializer implements Serializer<BukkitData.Advancements> {
|
||||
|
||||
private static final TypeToken<List<Data.Advancements.Advancement>> TYPE = new TypeToken<>() {
|
||||
};
|
||||
|
||||
public Advancements(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.Advancements deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
return BukkitData.Advancements.from(
|
||||
plugin.getGson().fromJson(serialized, TYPE.getType())
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.Advancements element) throws SerializationException {
|
||||
return plugin.getGson().toJson(element.getCompleted());
|
||||
}
|
||||
}
|
||||
|
||||
public static class PersistentData extends BukkitSerializer implements Serializer<BukkitData.PersistentData> {
|
||||
|
||||
public PersistentData(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.PersistentData deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
return BukkitData.PersistentData.from((NBTContainer) NBT.parseNBT(serialized));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.PersistentData element) throws SerializationException {
|
||||
return element.getPersistentData().toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link Serializer.Json} in the common module instead
|
||||
*/
|
||||
@Deprecated(since = "2.6")
|
||||
public class Json<T extends Data & Adaptable> extends Serializer.Json<T> {
|
||||
|
||||
public Json(@NotNull HuskSync plugin, @NotNull Class<T> type) {
|
||||
super(plugin, type);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public BukkitHuskSync getPlugin() {
|
||||
return (BukkitHuskSync) plugin;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.maps.BukkitMapHandler;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
|
||||
@Override
|
||||
default Optional<? extends Data> getData(@NotNull Identifier id) {
|
||||
if (id.isCustom()) {
|
||||
return Optional.ofNullable(getCustomDataStore().get(id));
|
||||
}
|
||||
|
||||
try {
|
||||
return switch (id.getKeyValue()) {
|
||||
case "inventory" -> getInventory();
|
||||
case "ender_chest" -> getEnderChest();
|
||||
case "potion_effects" -> getPotionEffects();
|
||||
case "advancements" -> getAdvancements();
|
||||
case "location" -> getLocation();
|
||||
case "statistics" -> getStatistics();
|
||||
case "health" -> getHealth();
|
||||
case "hunger" -> getHunger();
|
||||
case "attributes" -> getAttributes();
|
||||
case "experience" -> getExperience();
|
||||
case "game_mode" -> getGameMode();
|
||||
case "flight_status" -> getFlightStatus();
|
||||
case "persistent_data" -> getPersistentData();
|
||||
default -> throw new IllegalStateException(String.format("Unexpected data type: %s", id));
|
||||
};
|
||||
} catch (Throwable e) {
|
||||
getPlugin().debug("Failed to get data for key: " + id.asMinimalString(), e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setData(@NotNull Identifier id, @NotNull Data data) {
|
||||
if (id.isCustom()) {
|
||||
getCustomDataStore().put(id, data);
|
||||
}
|
||||
UserDataHolder.super.setData(id, data);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Items.Inventory> getInventory() {
|
||||
if ((isDead() && !getPlugin().getSettings().getSynchronization().getSaveOnDeath()
|
||||
.isSyncDeadPlayersChangingServer())) {
|
||||
return Optional.of(BukkitData.Items.Inventory.empty());
|
||||
}
|
||||
final PlayerInventory inventory = getPlayer().getInventory();
|
||||
return Optional.of(BukkitData.Items.Inventory.from(
|
||||
getMapPersister().persistLockedMaps(inventory.getContents(), getPlayer()),
|
||||
inventory.getHeldItemSlot()
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Items.EnderChest> getEnderChest() {
|
||||
return Optional.of(BukkitData.Items.EnderChest.adapt(
|
||||
getMapPersister().persistLockedMaps(getPlayer().getEnderChest().getContents(), getPlayer())
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.PotionEffects> getPotionEffects() {
|
||||
return Optional.of(BukkitData.PotionEffects.from(getPlayer().getActivePotionEffects()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Advancements> getAdvancements() {
|
||||
return Optional.of(BukkitData.Advancements.adapt(getPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Location> getLocation() {
|
||||
return Optional.of(BukkitData.Location.adapt(getPlayer().getLocation()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Statistics> getStatistics() {
|
||||
return Optional.of(BukkitData.Statistics.adapt(getPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Health> getHealth() {
|
||||
return Optional.of(BukkitData.Health.adapt(getPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Hunger> getHunger() {
|
||||
return Optional.of(BukkitData.Hunger.adapt(getPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Attributes> getAttributes() {
|
||||
return Optional.of(BukkitData.Attributes.adapt(getPlayer(), getPlugin()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Experience> getExperience() {
|
||||
return Optional.of(BukkitData.Experience.adapt(getPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.GameMode> getGameMode() {
|
||||
return Optional.of(BukkitData.GameMode.adapt(getPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.FlightStatus> getFlightStatus() {
|
||||
return Optional.of(BukkitData.FlightStatus.adapt(getPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.PersistentData> getPersistentData() {
|
||||
return Optional.of(BukkitData.PersistentData.adapt(getPlayer().getPersistentDataContainer()));
|
||||
}
|
||||
|
||||
boolean isDead();
|
||||
|
||||
@NotNull
|
||||
Player getPlayer();
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getPlayer()} instead
|
||||
*/
|
||||
@Deprecated(since = "3.6")
|
||||
@NotNull
|
||||
default Player getBukkitPlayer() {
|
||||
return getPlayer();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default BukkitMapHandler getMapPersister() {
|
||||
return (BukkitHuskSync) getPlugin();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, Cancellable {
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
private final HuskSync plugin;
|
||||
private final DataSnapshot.Packed snapshot;
|
||||
private final User user;
|
||||
private boolean cancelled = false;
|
||||
|
||||
protected BukkitDataSaveEvent(@NotNull User user, @NotNull DataSnapshot.Packed snapshot, @NotNull HuskSync plugin) {
|
||||
this.user = user;
|
||||
this.snapshot = snapshot;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCancelled(boolean cancelled) {
|
||||
this.cancelled = cancelled;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public DataSnapshot.Packed getData() {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class BukkitEvent extends Event implements net.william278.husksync.event.Event {
|
||||
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
|
||||
protected BukkitEvent() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface BukkitEventDispatcher extends EventDispatcher {
|
||||
|
||||
@Override
|
||||
default <T extends Event> boolean fireIsCancelled(@NotNull T event) {
|
||||
Bukkit.getPluginManager().callEvent((org.bukkit.event.Event) event);
|
||||
return event instanceof Cancellable cancellable && cancellable.isCancelled();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default PreSyncEvent getPreSyncEvent(@NotNull OnlineUser user, @NotNull DataSnapshot.Packed data) {
|
||||
return new BukkitPreSyncEvent(user, data, getPlugin());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default DataSaveEvent getDataSaveEvent(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
return new BukkitDataSaveEvent(user, data, getPlugin());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default SyncCompleteEvent getSyncCompleteEvent(@NotNull OnlineUser user) {
|
||||
return new BukkitSyncCompleteEvent(user, getPlugin());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class BukkitPlayerEvent extends BukkitEvent implements PlayerEvent {
|
||||
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
|
||||
protected final OnlineUser player;
|
||||
|
||||
protected BukkitPlayerEvent(@NotNull OnlineUser player) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public OnlineUser getUser() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEvent, Cancellable {
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
private final HuskSync plugin;
|
||||
private final DataSnapshot.Packed data;
|
||||
private boolean cancelled = false;
|
||||
|
||||
protected BukkitPreSyncEvent(@NotNull OnlineUser player, @NotNull DataSnapshot.Packed data, @NotNull HuskSync plugin) {
|
||||
super(player);
|
||||
this.data = data;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCancelled(boolean cancelled) {
|
||||
this.cancelled = cancelled;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public DataSnapshot.Packed getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitSyncCompleteEvent extends BukkitPlayerEvent implements SyncCompleteEvent {
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
|
||||
protected BukkitSyncCompleteEvent(@NotNull OnlineUser player, @NotNull HuskSync plugin) {
|
||||
super(player);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface BukkitDeathEventListener extends Listener {
|
||||
|
||||
boolean handleEvent(@NotNull EventListener.ListenerType type, @NotNull EventListener.Priority priority);
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
default void onPlayerDeathHighest(@NotNull PlayerDeathEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.DEATH_LISTENER, EventListener.Priority.HIGHEST)) {
|
||||
handlePlayerDeath(event);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
|
||||
default void onPlayerDeath(@NotNull PlayerDeathEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.DEATH_LISTENER, EventListener.Priority.NORMAL)) {
|
||||
handlePlayerDeath(event);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||
default void onPlayerDeathLowest(@NotNull PlayerDeathEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.DEATH_LISTENER, EventListener.Priority.LOWEST)) {
|
||||
handlePlayerDeath(event);
|
||||
}
|
||||
}
|
||||
|
||||
void handlePlayerDeath(@NotNull PlayerDeathEvent player);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
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.event.player.PlayerCommandPreprocessEvent;
|
||||
import org.bukkit.event.server.MapInitializeEvent;
|
||||
import org.bukkit.event.world.WorldSaveEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Getter
|
||||
public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
|
||||
BukkitDeathEventListener, Listener {
|
||||
|
||||
protected LockedHandler lockedHandler;
|
||||
|
||||
public BukkitEventListener(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
public void onLoad() {
|
||||
this.lockedHandler = createLockedHandler((BukkitHuskSync) plugin);
|
||||
}
|
||||
|
||||
public void onEnable() {
|
||||
getPlugin().getServer().getPluginManager().registerEvents(this, getPlugin());
|
||||
lockedHandler.onEnable();
|
||||
}
|
||||
|
||||
public void handlePluginDisable() {
|
||||
super.handlePluginDisable();
|
||||
lockedHandler.onDisable();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private LockedHandler createLockedHandler(@NotNull BukkitHuskSync plugin) {
|
||||
if (!getPlugin().getSettings().isCancelPackets()) {
|
||||
return new BukkitLockedEventListener(plugin);
|
||||
}
|
||||
if (getPlugin().isDependencyLoaded("PacketEvents")) {
|
||||
return new BukkitPacketEventsLockedPacketListener(plugin);
|
||||
} else if (getPlugin().isDependencyLoaded("ProtocolLib")) {
|
||||
return new BukkitProtocolLibLockedPacketListener(plugin);
|
||||
}
|
||||
|
||||
return new BukkitLockedEventListener(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleEvent(@NotNull ListenerType type, @NotNull Priority priority) {
|
||||
return plugin.getSettings().getSynchronization().getEventPriority(type).equals(priority);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePlayerQuit(@NotNull BukkitUser bukkitUser) {
|
||||
final Player player = bukkitUser.getPlayer();
|
||||
final ItemStack itemOnCursor = player.getItemOnCursor();
|
||||
if (!bukkitUser.isLocked() && !itemOnCursor.getType().isAir()) {
|
||||
player.setItemOnCursor(null);
|
||||
player.getWorld().dropItem(player.getLocation(), itemOnCursor);
|
||||
plugin.debug("Dropped " + itemOnCursor + " for " + player.getName() + " on quit");
|
||||
}
|
||||
super.handlePlayerQuit(bukkitUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePlayerJoin(@NotNull BukkitUser bukkitUser) {
|
||||
super.handlePlayerJoin(bukkitUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePlayerDeath(@NotNull PlayerDeathEvent event) {
|
||||
final OnlineUser user = BukkitUser.adapt(event.getEntity(), plugin);
|
||||
|
||||
// If the player is locked or the plugin disabling, clear their drops
|
||||
if (lockedHandler.cancelPlayerEvent(user.getUuid())) {
|
||||
event.getDrops().clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle saving player data snapshots on death
|
||||
if (!plugin.getSettings().getSynchronization().getSaveOnDeath().isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Truncate the dropped items list to the inventory size and save the player's inventory
|
||||
final int maxInventorySize = BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
|
||||
if (event.getDrops().size() > maxInventorySize) {
|
||||
event.getDrops().subList(maxInventorySize, event.getDrops().size()).clear();
|
||||
}
|
||||
super.saveOnPlayerDeath(user, BukkitData.Items.ItemArray.adapt(event.getDrops()));
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onWorldSave(@NotNull WorldSaveEvent event) {
|
||||
if (!plugin.getSettings().getSynchronization().isSaveOnWorldSave()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle saving player data snapshots when the world saves
|
||||
plugin.runAsync(() -> super.saveOnWorldSave(event.getWorld().getPlayers()
|
||||
.stream().map(player -> BukkitUser.adapt(player, plugin))
|
||||
.collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onMapInitialize(@NotNull MapInitializeEvent event) {
|
||||
if (plugin.getSettings().getSynchronization().isPersistLockedMaps() && event.getMap().isLocked()) {
|
||||
getPlugin().runAsync(() -> ((BukkitHuskSync) plugin).renderInitializingLockedMap(event.getMap()));
|
||||
}
|
||||
}
|
||||
|
||||
// We handle commands here to allow specific command handling on ProtocolLib servers
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onCommandProcessed(@NotNull PlayerCommandPreprocessEvent event) {
|
||||
if (!lockedHandler.isCommandDisabled(event.getMessage().substring(1).split(" ")[0])) {
|
||||
return;
|
||||
}
|
||||
if (lockedHandler.cancelPlayerEvent(event.getPlayer().getUniqueId())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public BukkitHuskSync getPlugin() {
|
||||
return (BukkitHuskSync) plugin;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface BukkitJoinEventListener extends Listener {
|
||||
|
||||
boolean handleEvent(@NotNull EventListener.ListenerType type, @NotNull EventListener.Priority priority);
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
default void onPlayerJoinHighest(@NotNull PlayerJoinEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.JOIN_LISTENER, EventListener.Priority.HIGHEST)) {
|
||||
handlePlayerJoin(BukkitUser.adapt(event.getPlayer(), getPlugin()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
|
||||
default void onPlayerJoin(@NotNull PlayerJoinEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.JOIN_LISTENER, EventListener.Priority.NORMAL)) {
|
||||
handlePlayerJoin(BukkitUser.adapt(event.getPlayer(), getPlugin()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||
default void onPlayerJoinLowest(@NotNull PlayerJoinEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.JOIN_LISTENER, EventListener.Priority.LOWEST)) {
|
||||
handlePlayerJoin(BukkitUser.adapt(event.getPlayer(), getPlugin()));
|
||||
}
|
||||
}
|
||||
|
||||
void handlePlayerJoin(@NotNull BukkitUser player);
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Projectile;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.entity.EntityPickupItemEvent;
|
||||
import org.bukkit.event.entity.ProjectileLaunchEvent;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||
import org.bukkit.event.player.PlayerArmorStandManipulateEvent;
|
||||
import org.bukkit.event.player.PlayerDropItemEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
public class BukkitLockedEventListener implements LockedHandler, Listener {
|
||||
|
||||
protected final BukkitHuskSync plugin;
|
||||
|
||||
protected BukkitLockedEventListener(@NotNull BukkitHuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onProjectileLaunch(@NotNull ProjectileLaunchEvent event) {
|
||||
final Projectile projectile = event.getEntity();
|
||||
if (projectile.getShooter() instanceof Player player) {
|
||||
cancelPlayerEvent(player.getUniqueId(), event);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onDropItem(@NotNull PlayerDropItemEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPickupItem(@NotNull EntityPickupItemEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
cancelPlayerEvent(player.getUniqueId(), event);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteract(@NotNull PlayerInteractEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteractEntity(@NotNull PlayerInteractEntityEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteractArmorStand(@NotNull PlayerArmorStandManipulateEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockPlace(@NotNull BlockPlaceEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockBreak(@NotNull BlockBreakEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
|
||||
if (event.getPlayer() instanceof Player player) {
|
||||
cancelPlayerEvent(player.getUniqueId(), event);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryClick(@NotNull InventoryClickEvent event) {
|
||||
cancelPlayerEvent(event.getWhoClicked().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
cancelPlayerEvent(player.getUniqueId(), event);
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelPlayerEvent(@NotNull UUID uuid, @NotNull Cancellable event) {
|
||||
if (cancelPlayerEvent(uuid)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import com.github.retrooper.packetevents.PacketEvents;
|
||||
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
|
||||
import com.github.retrooper.packetevents.event.PacketListenerPriority;
|
||||
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
|
||||
import com.github.retrooper.packetevents.event.PacketSendEvent;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.google.common.collect.Sets;
|
||||
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
|
||||
|
||||
protected BukkitPacketEventsLockedPacketListener(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public void onLoad() {
|
||||
super.onLoad();
|
||||
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(getPlugin()));
|
||||
PacketEvents.getAPI().getSettings().reEncodeByDefault(false).checkForUpdates(false);
|
||||
PacketEvents.getAPI().load();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
super.onEnable();
|
||||
PacketEvents.getAPI().getEventManager().registerListener(new PlayerPacketAdapter(this));
|
||||
PacketEvents.getAPI().init();
|
||||
plugin.log(Level.INFO, "Using PacketEvents to cancel packets for locked players");
|
||||
}
|
||||
|
||||
private static class PlayerPacketAdapter extends PacketListenerAbstract {
|
||||
|
||||
private static final Set<PacketType.Play.Client> ALLOWED_PACKETS = Set.of(
|
||||
PacketType.Play.Client.KEEP_ALIVE, PacketType.Play.Client.PONG, PacketType.Play.Client.PLUGIN_MESSAGE, // Connection packets
|
||||
PacketType.Play.Client.PLAYER_LOADED, PacketType.Play.Client.CLIENT_TICK_END, // Connection packets
|
||||
PacketType.Play.Client.CHAT_MESSAGE, PacketType.Play.Client.CHAT_COMMAND, PacketType.Play.Client.CHAT_SESSION_UPDATE, // Chat / command packets
|
||||
PacketType.Play.Client.PLAYER_POSITION, PacketType.Play.Client.PLAYER_POSITION_AND_ROTATION, PacketType.Play.Client.PLAYER_ROTATION, // Movement packets
|
||||
PacketType.Play.Client.HELD_ITEM_CHANGE, PacketType.Play.Client.ANIMATION, PacketType.Play.Client.TELEPORT_CONFIRM, // Animation packets
|
||||
PacketType.Play.Client.CLIENT_SETTINGS // Video setting packets
|
||||
);
|
||||
|
||||
private static final Set<PacketType.Play.Client> CANCEL_PACKETS = getPacketsToListenFor();
|
||||
|
||||
|
||||
private final BukkitPacketEventsLockedPacketListener listener;
|
||||
|
||||
public PlayerPacketAdapter(@NotNull BukkitPacketEventsLockedPacketListener listener) {
|
||||
super(PacketListenerPriority.HIGH);
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
|
||||
return;
|
||||
}
|
||||
if (!CANCEL_PACKETS.contains(client)) {
|
||||
return;
|
||||
}
|
||||
if (listener.cancelPlayerEvent(event.getUser().getUUID())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketSend(PacketSendEvent event) {
|
||||
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
|
||||
return;
|
||||
}
|
||||
if (!CANCEL_PACKETS.contains(client)) {
|
||||
return;
|
||||
}
|
||||
if (listener.cancelPlayerEvent(event.getUser().getUUID())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the set of ALL Server packets, excluding the set of allowed packets
|
||||
@NotNull
|
||||
private static Set<PacketType.Play.Client> getPacketsToListenFor() {
|
||||
return Sets.difference(
|
||||
Sets.newHashSet(PacketType.Play.Client.values()),
|
||||
ALLOWED_PACKETS
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import com.comphenix.protocol.PacketType;
|
||||
import com.comphenix.protocol.ProtocolLibrary;
|
||||
import com.comphenix.protocol.events.ListenerPriority;
|
||||
import com.comphenix.protocol.events.PacketAdapter;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.google.common.collect.Sets;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.comphenix.protocol.PacketType.Play.Client;
|
||||
|
||||
public class BukkitProtocolLibLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
|
||||
|
||||
protected BukkitProtocolLibLockedPacketListener(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
super.onEnable();
|
||||
ProtocolLibrary.getProtocolManager().addPacketListener(new PlayerPacketAdapter(this));
|
||||
plugin.log(Level.INFO, "Using ProtocolLib to cancel packets for locked players");
|
||||
}
|
||||
|
||||
private static class PlayerPacketAdapter extends PacketAdapter {
|
||||
|
||||
// Packets we want the player to still be able to send/receiver to/from the server - //todo update 1.21.4
|
||||
private static final Set<PacketType> ALLOWED_PACKETS = Set.of(
|
||||
Client.KEEP_ALIVE, Client.PONG, Client.CUSTOM_PAYLOAD, // Connection packets
|
||||
Client.CHAT_COMMAND, Client.CLIENT_COMMAND, Client.CHAT, Client.CHAT_SESSION_UPDATE, // Chat / command packets
|
||||
Client.POSITION, Client.POSITION_LOOK, Client.LOOK, // Movement packets
|
||||
Client.HELD_ITEM_SLOT, Client.ARM_ANIMATION, Client.TELEPORT_ACCEPT, // Animation packets
|
||||
Client.SETTINGS // Video setting packets
|
||||
);
|
||||
|
||||
private final BukkitProtocolLibLockedPacketListener listener;
|
||||
|
||||
public PlayerPacketAdapter(@NotNull BukkitProtocolLibLockedPacketListener listener) {
|
||||
super(listener.getPlugin(), ListenerPriority.HIGHEST, getPacketsToListenFor());
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketReceiving(@NotNull PacketEvent event) {
|
||||
if (listener.cancelPlayerEvent(event.getPlayer().getUniqueId()) && !event.isReadOnly()) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketSending(@NotNull PacketEvent event) {
|
||||
if (listener.cancelPlayerEvent(event.getPlayer().getUniqueId()) && !event.isReadOnly()) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the set of ALL Server packets, excluding the set of allowed packets
|
||||
@NotNull
|
||||
private static Set<PacketType> getPacketsToListenFor() {
|
||||
return Sets.difference(
|
||||
Client.getInstance().values().stream().filter(PacketType::isSupported).collect(Collectors.toSet()),
|
||||
ALLOWED_PACKETS
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface BukkitQuitEventListener extends Listener {
|
||||
|
||||
boolean handleEvent(@NotNull EventListener.ListenerType type, @NotNull EventListener.Priority priority);
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
default void onPlayerQuitHighest(@NotNull PlayerQuitEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.QUIT_LISTENER, EventListener.Priority.HIGHEST)) {
|
||||
handlePlayerQuit(BukkitUser.adapt(event.getPlayer(), getPlugin()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
|
||||
default void onPlayerQuit(@NotNull PlayerQuitEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.QUIT_LISTENER, EventListener.Priority.NORMAL)) {
|
||||
handlePlayerQuit(BukkitUser.adapt(event.getPlayer(), getPlugin()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||
default void onPlayerQuitLowest(@NotNull PlayerQuitEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.QUIT_LISTENER, EventListener.Priority.LOWEST)) {
|
||||
handlePlayerQuit(BukkitUser.adapt(event.getPlayer(), getPlugin()));
|
||||
}
|
||||
}
|
||||
|
||||
void handlePlayerQuit(@NotNull BukkitUser player);
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.player.PlayerAdvancementDoneEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import static net.william278.husksync.config.Settings.SynchronizationSettings.SaveOnDeathSettings;
|
||||
|
||||
public class PaperEventListener extends BukkitEventListener {
|
||||
|
||||
public PaperEventListener(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("RedundantMethodOverride")
|
||||
public void onEnable() {
|
||||
getPlugin().getServer().getPluginManager().registerEvents(this, getPlugin());
|
||||
lockedHandler.onEnable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePlayerDeath(@NotNull PlayerDeathEvent event) {
|
||||
// If the player is locked or the plugin disabling, clear their drops
|
||||
final OnlineUser user = BukkitUser.adapt(event.getEntity(), plugin);
|
||||
if (lockedHandler.cancelPlayerEvent(user.getUuid())) {
|
||||
event.getDrops().clear();
|
||||
event.getItemsToKeep().clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle saving player data snapshots on death
|
||||
final SaveOnDeathSettings settings = plugin.getSettings().getSynchronization().getSaveOnDeath();
|
||||
if (!settings.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Paper - support saving the player's items to keep if enabled
|
||||
final int maxInventorySize = BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
|
||||
final List<ItemStack> itemsToSave = switch (settings.getItemsToSave()) {
|
||||
case DROPS -> event.getDrops();
|
||||
case ITEMS_TO_KEEP -> preserveOrder(event.getEntity().getInventory(), event.getItemsToKeep());
|
||||
};
|
||||
if (itemsToSave.size() > maxInventorySize) {
|
||||
itemsToSave.subList(maxInventorySize, itemsToSave.size()).clear();
|
||||
}
|
||||
super.saveOnPlayerDeath(user, BukkitData.Items.ItemArray.adapt(itemsToSave));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerAdvancementDone(@NotNull PlayerAdvancementDoneEvent event) {
|
||||
if (lockedHandler.cancelPlayerEvent(event.getPlayer().getUniqueId())) {
|
||||
event.message(null);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<ItemStack> preserveOrder(@NotNull PlayerInventory inventory, @NotNull List<ItemStack> toKeep) {
|
||||
final List<ItemStack> preserved = Lists.newArrayList();
|
||||
final List<ItemStack> items = Lists.newArrayList(inventory.getContents());
|
||||
for (ItemStack item : toKeep) {
|
||||
final Iterator<ItemStack> iterator = items.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
final ItemStack originalItem = iterator.next();
|
||||
if (originalItem != null && originalItem.equals(item)) {
|
||||
preserved.add(originalItem);
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return preserved;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,620 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.maps;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import de.tr7zw.changeme.nbtapi.NBT;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadableItemNBT;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadableNBT;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.mapdataapi.MapBanner;
|
||||
import net.william278.mapdataapi.MapData;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Container;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.BlockStateMeta;
|
||||
import org.bukkit.inventory.meta.BundleMeta;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.map.*;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public interface BukkitMapHandler {
|
||||
|
||||
// The map used to store HuskSync data in ItemStack NBT
|
||||
String MAP_DATA_KEY = "husksync:persisted_locked_map";
|
||||
// The legacy map key used to store pixel data (3.7.3 and below)
|
||||
String MAP_LEGACY_PIXEL_DATA_KEY = "husksync:canvas_data";
|
||||
// Name of server the map originates from
|
||||
String MAP_ORIGIN_KEY = "origin";
|
||||
// Original map id
|
||||
String MAP_ID_KEY = "id";
|
||||
|
||||
/**
|
||||
* Persist locked maps in an array of {@link ItemStack}s
|
||||
*
|
||||
* @param items the array of {@link ItemStack}s to persist locked maps in
|
||||
* @param delegateRenderer the player to delegate the rendering of map pixel canvases to
|
||||
* @return the array of {@link ItemStack}s with locked maps persisted to serialized NBT
|
||||
*/
|
||||
@NotNull
|
||||
default ItemStack[] persistLockedMaps(@NotNull ItemStack[] items, @NotNull Player delegateRenderer) {
|
||||
if (!getPlugin().getSettings().getSynchronization().isPersistLockedMaps()) {
|
||||
return items;
|
||||
}
|
||||
return forEachMap(items, map -> this.persistMapView(map, delegateRenderer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply persisted locked maps to an array of {@link ItemStack}s
|
||||
*
|
||||
* @param items the array of {@link ItemStack}s to apply persisted locked maps to
|
||||
* @return the array of {@link ItemStack}s with persisted locked maps applied
|
||||
*/
|
||||
@Nullable
|
||||
default ItemStack @NotNull [] setMapViews(@Nullable ItemStack @NotNull [] items) {
|
||||
if (!getPlugin().getSettings().getSynchronization().isPersistLockedMaps()) {
|
||||
return items;
|
||||
}
|
||||
return forEachMap(items, this::applyMapView);
|
||||
}
|
||||
|
||||
// Perform an operation on each map in an array of ItemStacks
|
||||
@NotNull
|
||||
private ItemStack[] forEachMap(ItemStack[] items, @NotNull Function<ItemStack, ItemStack> function) {
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
final ItemStack item = items[i];
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) {
|
||||
items[i] = function.apply(item);
|
||||
} else if (item.getItemMeta() instanceof BlockStateMeta b && b.getBlockState() instanceof Container box
|
||||
&& !box.getInventory().isEmpty()) {
|
||||
forEachMap(box.getInventory().getContents(), function);
|
||||
b.setBlockState(box);
|
||||
item.setItemMeta(b);
|
||||
} else if (item.getItemMeta() instanceof BundleMeta bundle && bundle.hasItems()) {
|
||||
bundle.setItems(List.of(forEachMap(bundle.getItems().toArray(ItemStack[]::new), function)));
|
||||
item.setItemMeta(bundle);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
private void writeMapData(@NotNull String serverName, int mapId, MapData data) {
|
||||
final byte[] dataBytes = getPlugin().getDataAdapter().toBytes(new AdaptableMapData(data));
|
||||
getRedisManager().setMapData(serverName, mapId, dataBytes);
|
||||
getPlugin().getDatabase().saveMapData(serverName, mapId, dataBytes);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Blocking
|
||||
private MapData readMapData(@NotNull String serverName, int mapId) {
|
||||
final byte[] readData = fetchMapData(serverName, mapId);
|
||||
if (readData == null) {
|
||||
return null;
|
||||
}
|
||||
return deserializeMapData(readData);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Blocking
|
||||
private byte[] fetchMapData(@NotNull String serverName, int mapId) {
|
||||
return fetchMapData(serverName, mapId, true);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Blocking
|
||||
private byte[] fetchMapData(@NotNull String serverName, int mapId, boolean doReverseLookup) {
|
||||
// Read from Redis cache
|
||||
final byte[] redisData = getRedisManager().getMapData(serverName, mapId);
|
||||
if (redisData != null) {
|
||||
return redisData;
|
||||
}
|
||||
|
||||
// Read from database and set to Redis
|
||||
final byte[] databaseData = getPlugin().getDatabase().getMapData(serverName, mapId);
|
||||
if (databaseData != null) {
|
||||
getRedisManager().setMapData(serverName, mapId, databaseData);
|
||||
return databaseData;
|
||||
}
|
||||
|
||||
// Otherwise, lookup a reverse map binding
|
||||
if (doReverseLookup) {
|
||||
return fetchReversedMapData(serverName, mapId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private byte[] fetchReversedMapData(@NotNull String serverName, int mapId) {
|
||||
// Lookup binding from Redis cache, then fetch data if found
|
||||
Map.Entry<String, Integer> binding = getRedisManager().getReversedMapBound(serverName, mapId);
|
||||
if (binding != null) {
|
||||
return fetchMapData(binding.getKey(), binding.getValue(), false);
|
||||
}
|
||||
|
||||
// Lookup binding from database, then set to Redis & fetch data if found
|
||||
binding = getPlugin().getDatabase().getMapBinding(serverName, mapId);
|
||||
if (binding != null) {
|
||||
getRedisManager().bindMapIds(binding.getKey(), binding.getValue(), serverName, mapId);
|
||||
return fetchMapData(binding.getKey(), binding.getValue(), false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MapData deserializeMapData(byte @NotNull [] data) {
|
||||
try {
|
||||
return getPlugin().getDataAdapter().fromBytes(data, AdaptableMapData.class)
|
||||
.getData(getPlugin().getDataVersion(getPlugin().getMinecraftVersion()));
|
||||
} catch (IOException e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to deserialize map data", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the bound map ID
|
||||
private int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName) {
|
||||
// Get the map ID from Redis, if set
|
||||
final Optional<Integer> redisId = getRedisManager().getBoundMapId(fromServerName, fromMapId, toServerName);
|
||||
if (redisId.isPresent()) {
|
||||
return redisId.get();
|
||||
}
|
||||
|
||||
// Get from the database; if found, set to Redis
|
||||
final int result = getPlugin().getDatabase().getBoundMapId(fromServerName, fromMapId, toServerName);
|
||||
if (result != -1) {
|
||||
getPlugin().getRedisManager().bindMapIds(fromServerName, fromMapId, toServerName, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ItemStack persistMapView(@NotNull ItemStack map, @NotNull Player delegateRenderer) {
|
||||
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
|
||||
if (!meta.hasMapView()) {
|
||||
return map;
|
||||
}
|
||||
final MapView view = meta.getMapView();
|
||||
if (view == null || view.getWorld() == null || !view.isLocked() || view.isVirtual()) {
|
||||
return map;
|
||||
}
|
||||
|
||||
NBT.modify(map, nbt -> {
|
||||
// Don't save the map's data twice
|
||||
if (nbt.hasTag(MAP_DATA_KEY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Render the map
|
||||
final int dataVersion = getPlugin().getDataVersion(getPlugin().getMinecraftVersion());
|
||||
final PersistentMapCanvas canvas = new PersistentMapCanvas(view, dataVersion);
|
||||
for (MapRenderer renderer : view.getRenderers()) {
|
||||
renderer.render(view, canvas, delegateRenderer);
|
||||
getPlugin().debug(String.format("Rendered locked map canvas to view (#%s)", view.getId()));
|
||||
}
|
||||
|
||||
// Persist map data
|
||||
final ReadWriteNBT mapData = nbt.getOrCreateCompound(MAP_DATA_KEY);
|
||||
final String serverName = getPlugin().getServerName();
|
||||
mapData.setString(MAP_ORIGIN_KEY, serverName);
|
||||
mapData.setInteger(MAP_ID_KEY, meta.getMapId());
|
||||
if (readMapData(serverName, meta.getMapId()) == null) {
|
||||
writeMapData(serverName, meta.getMapId(), canvas.extractMapData());
|
||||
}
|
||||
getPlugin().debug(String.format("Saved data for locked map (#%s, server: %s)", view.getId(), serverName));
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@NotNull
|
||||
private ItemStack applyMapView(@NotNull ItemStack map) {
|
||||
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
|
||||
NBT.get(map, nbt -> {
|
||||
if (!nbt.hasTag(MAP_DATA_KEY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ReadableNBT mapData = nbt.getCompound(MAP_DATA_KEY);
|
||||
if (mapData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Server the map was originally created on, and the current server. If they match, isOrigin is true.
|
||||
final String originServer = mapData.getString(MAP_ORIGIN_KEY);
|
||||
final String currentServer = getPlugin().getServerName();
|
||||
final boolean isOrigin = currentServer.equals(originServer);
|
||||
|
||||
// Determine the map's ID on its origin server, and the new ID it should be bound to here.
|
||||
// Then, update the map item / data accordingly (re-rendering and caching the map if needed)
|
||||
final int originalId = mapData.getInteger(MAP_ID_KEY);
|
||||
int newId = isOrigin ? originalId : getBoundMapId(originServer, originalId, currentServer);
|
||||
if (newId != -1) {
|
||||
handleBoundMap(meta, nbt, originServer, originalId, newId, isOrigin);
|
||||
} else {
|
||||
handleUnboundMap(meta, nbt, originServer, originalId, currentServer);
|
||||
}
|
||||
|
||||
map.setItemMeta(meta);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
private void handleBoundMap(@NotNull MapMeta meta, @NotNull ReadableItemNBT nbt, @NotNull String originServer,
|
||||
int originalId, int newId, boolean isOrigin) {
|
||||
MapView view = Bukkit.getMap(newId);
|
||||
if (isOrigin && view != null) {
|
||||
meta.setMapView(view);
|
||||
getPlugin().debug("Map ID set to original ID #%s".formatted(newId));
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<MapView> optionalView = getMapView(newId);
|
||||
if (optionalView.isPresent()) {
|
||||
meta.setMapView(optionalView.get());
|
||||
getPlugin().debug("Map ID set to #%s".formatted(newId));
|
||||
return;
|
||||
}
|
||||
|
||||
getPlugin().debug("Deserializing map data from NBT and generating view...");
|
||||
MapData mapData = readMapData(originServer, originalId);
|
||||
if (mapData == null && nbt.hasTag(MAP_LEGACY_PIXEL_DATA_KEY)) {
|
||||
mapData = readLegacyMapItemData(nbt);
|
||||
}
|
||||
|
||||
if (mapData == null) {
|
||||
getPlugin().debug("Read pixel data was not found in database, skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
MapView newView = view != null ? view : Bukkit.createMap(getDefaultMapWorld());
|
||||
generateRenderedMap(mapData, newView);
|
||||
meta.setMapView(newView);
|
||||
}
|
||||
|
||||
private void handleUnboundMap(@NotNull MapMeta meta, @NotNull ReadableItemNBT nbt, @NotNull String originServer,
|
||||
int originalId, @NotNull String currentServer) {
|
||||
getPlugin().debug("Deserializing map data from NBT and generating view...");
|
||||
MapData mapData = readMapData(originServer, originalId);
|
||||
if (mapData == null && nbt.hasTag(MAP_LEGACY_PIXEL_DATA_KEY)) {
|
||||
mapData = readLegacyMapItemData(nbt);
|
||||
}
|
||||
|
||||
if (mapData == null) {
|
||||
getPlugin().debug("Read pixel data was not found in database, skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
final MapView view = generateRenderedMap(Objects.requireNonNull(mapData, "Pixel data null!"));
|
||||
meta.setMapView(view);
|
||||
|
||||
final int id = view.getId();
|
||||
getRedisManager().bindMapIds(originServer, originalId, currentServer, id);
|
||||
getPlugin().getDatabase().setMapBinding(originServer, originalId, currentServer, id);
|
||||
|
||||
getPlugin().debug("Bound map to view (#%s) on server %s".formatted(id, currentServer));
|
||||
}
|
||||
|
||||
// Render a persisted locked map that is initializing (i.e. in an item frame)
|
||||
default void renderInitializingLockedMap(@NotNull MapView view) {
|
||||
if (view.isVirtual()) {
|
||||
return;
|
||||
}
|
||||
final Optional<MapView> optionalView = getMapView(view.getId());
|
||||
if (optionalView.isPresent()) {
|
||||
view.getRenderers().clear();
|
||||
view.getRenderers().addAll(optionalView.get().getRenderers());
|
||||
view.setLocked(true);
|
||||
view.setScale(MapView.Scale.CLOSEST);
|
||||
view.setTrackingPosition(false);
|
||||
view.setUnlimitedTracking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
MapData data = readMapData(getPlugin().getServerName(), view.getId());
|
||||
if (data == null) {
|
||||
data = readLegacyMapFileData(view.getId());
|
||||
}
|
||||
|
||||
if (data == null) {
|
||||
World world = view.getWorld() == null ? getDefaultMapWorld() : view.getWorld();
|
||||
getPlugin().debug("Not rendering map: no data in DB for world %s, map #%s."
|
||||
.formatted(world.getName(), view.getId()));
|
||||
return;
|
||||
}
|
||||
renderMapView(view, data);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private MapView generateRenderedMap(@NotNull MapData canvasData) {
|
||||
return generateRenderedMap(canvasData, Bukkit.createMap(getDefaultMapWorld()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private MapView generateRenderedMap(@NotNull MapData canvasData, @NotNull MapView view) {
|
||||
renderMapView(view, canvasData);
|
||||
return view;
|
||||
}
|
||||
|
||||
private void renderMapView(@NotNull MapView view, @NotNull MapData canvasData) {
|
||||
view.getRenderers().clear();
|
||||
view.addRenderer(new PersistentMapRenderer(canvasData));
|
||||
view.setLocked(true);
|
||||
view.setScale(MapView.Scale.CLOSEST);
|
||||
view.setTrackingPosition(false);
|
||||
view.setUnlimitedTracking(false);
|
||||
setMapView(view);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static World getDefaultMapWorld() {
|
||||
final World world = Bukkit.getWorlds().get(0);
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("No worlds are loaded on the server!");
|
||||
}
|
||||
return world;
|
||||
}
|
||||
|
||||
default Optional<MapView> getMapView(int id) {
|
||||
return getMapViews().containsKey(id) ? Optional.of(getMapViews().get(id)) : Optional.empty();
|
||||
}
|
||||
|
||||
default void setMapView(@NotNull MapView view) {
|
||||
getMapViews().put(view.getId(), view);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link MapRenderer} that can be used to render persistently serialized {@link MapData} to a {@link MapView}
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
class PersistentMapRenderer extends MapRenderer {
|
||||
|
||||
private final MapData canvasData;
|
||||
|
||||
private PersistentMapRenderer(@NotNull MapData canvasData) {
|
||||
super(false);
|
||||
this.canvasData = canvasData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(@NotNull MapView map, @NotNull MapCanvas canvas, @NotNull Player player) {
|
||||
// We set the pixels in this order to avoid the map being rendered upside down
|
||||
for (int i = 0; i < 128; i++) {
|
||||
for (int j = 0; j < 128; j++) {
|
||||
canvas.setPixel(j, i, (byte) canvasData.getColorAt(i, j));
|
||||
}
|
||||
}
|
||||
|
||||
// Set the map banners and markers
|
||||
final MapCursorCollection cursors = canvas.getCursors();
|
||||
while (cursors.size() > 0) {
|
||||
cursors.removeCursor(cursors.getCursor(0));
|
||||
}
|
||||
|
||||
canvasData.getBanners().forEach(banner -> cursors.addCursor(createBannerCursor(banner)));
|
||||
canvas.setCursors(cursors);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static MapCursor createBannerCursor(@NotNull MapBanner banner) {
|
||||
return new MapCursor(
|
||||
(byte) banner.getPosition().getX(),
|
||||
(byte) banner.getPosition().getZ(),
|
||||
(byte) 8, // Always rotate banners upright
|
||||
switch (banner.getColor().toLowerCase(Locale.ENGLISH)) {
|
||||
case "white" -> MapCursor.Type.BANNER_WHITE;
|
||||
case "orange" -> MapCursor.Type.BANNER_ORANGE;
|
||||
case "magenta" -> MapCursor.Type.BANNER_MAGENTA;
|
||||
case "light_blue" -> MapCursor.Type.BANNER_LIGHT_BLUE;
|
||||
case "yellow" -> MapCursor.Type.BANNER_YELLOW;
|
||||
case "lime" -> MapCursor.Type.BANNER_LIME;
|
||||
case "pink" -> MapCursor.Type.BANNER_PINK;
|
||||
case "gray" -> MapCursor.Type.BANNER_GRAY;
|
||||
case "light_gray" -> MapCursor.Type.BANNER_LIGHT_GRAY;
|
||||
case "cyan" -> MapCursor.Type.BANNER_CYAN;
|
||||
case "purple" -> MapCursor.Type.BANNER_PURPLE;
|
||||
case "blue" -> MapCursor.Type.BANNER_BLUE;
|
||||
case "brown" -> MapCursor.Type.BANNER_BROWN;
|
||||
case "green" -> MapCursor.Type.BANNER_GREEN;
|
||||
case "red" -> MapCursor.Type.BANNER_RED;
|
||||
default -> MapCursor.Type.BANNER_BLACK;
|
||||
},
|
||||
true,
|
||||
banner.getText().isEmpty() ? null : banner.getText()
|
||||
);
|
||||
}
|
||||
|
||||
// Legacy - read maps from item stacks
|
||||
@Nullable
|
||||
@Blocking
|
||||
private MapData readLegacyMapItemData(@NotNull ReadableItemNBT nbt) {
|
||||
final int dataVer = getPlugin().getDataVersion(getPlugin().getMinecraftVersion());
|
||||
try {
|
||||
return MapData.fromByteArray(dataVer,
|
||||
Objects.requireNonNull(nbt.getByteArray(MAP_LEGACY_PIXEL_DATA_KEY)));
|
||||
} catch (IOException e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to read legacy map data", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy - read maps from files
|
||||
@Nullable
|
||||
private MapData readLegacyMapFileData(int mapId) {
|
||||
final Path path = getPlugin().getDataFolder().toPath().resolve("maps").resolve(mapId + ".dat");
|
||||
final File file = path.toFile();
|
||||
if (!file.exists()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return MapData.fromNbt(file);
|
||||
} catch (IOException e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to read legacy map file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link MapCanvas} implementation used for pre-rendering maps to be converted into {@link MapData}
|
||||
*/
|
||||
@SuppressWarnings({"deprecation", "removal"})
|
||||
class PersistentMapCanvas implements MapCanvas {
|
||||
|
||||
private static final String BANNER_PREFIX = "banner_";
|
||||
|
||||
private final int mapDataVersion;
|
||||
private final MapView mapView;
|
||||
private final int[][] pixels = new int[128][128];
|
||||
private MapCursorCollection cursors;
|
||||
|
||||
private PersistentMapCanvas(@NotNull MapView mapView, int mapDataVersion) {
|
||||
this.mapDataVersion = mapDataVersion;
|
||||
this.mapView = mapView;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public MapView getMapView() {
|
||||
return mapView;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public MapCursorCollection getCursors() {
|
||||
return cursors == null ? (cursors = new MapCursorCollection()) : cursors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCursors(@NotNull MapCursorCollection cursors) {
|
||||
this.cursors = cursors;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public void setPixel(int x, int y, byte color) {
|
||||
pixels[x][y] = color;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public byte getPixel(int x, int y) {
|
||||
return (byte) pixels[x][y];
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public byte getBasePixel(int x, int y) {
|
||||
return (byte) pixels[x][y];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPixelColor(int x, int y, @Nullable Color color) {
|
||||
pixels[x][y] = color == null ? -1 : MapPalette.matchColor(color);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Color getPixelColor(int x, int y) {
|
||||
return MapPalette.getColor((byte) pixels[x][y]);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Color getBasePixelColor(int x, int y) {
|
||||
return MapPalette.getColor((byte) pixels[x][y]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawImage(int x, int y, @NotNull Image image) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawText(int x, int y, @NotNull MapFont font, @NotNull String text) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String getDimension() {
|
||||
return mapView.getWorld() != null ? switch (mapView.getWorld().getEnvironment()) {
|
||||
case NETHER -> "minecraft:the_nether";
|
||||
case THE_END -> "minecraft:the_end";
|
||||
default -> "minecraft:overworld";
|
||||
} : "minecraft:overworld";
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the map data from the canvas. Must be rendered first
|
||||
*
|
||||
* @return the extracted map data
|
||||
*/
|
||||
@NotNull
|
||||
private MapData extractMapData() {
|
||||
final List<MapBanner> banners = Lists.newArrayList();
|
||||
for (int i = 0; i < getCursors().size(); i++) {
|
||||
final MapCursor cursor = getCursors().getCursor(i);
|
||||
final String type = cursor.getType().getKey().getKey();
|
||||
if (type.startsWith(BANNER_PREFIX)) {
|
||||
banners.add(new MapBanner(
|
||||
type.replaceAll(BANNER_PREFIX, ""),
|
||||
cursor.getCaption() == null ? "" : cursor.getCaption(),
|
||||
cursor.getX(),
|
||||
mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128,
|
||||
cursor.getY()
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
return MapData.fromPixels(mapDataVersion, pixels, getDimension(), (byte) 0, banners, List.of());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Map<Integer, MapView> getMapViews();
|
||||
|
||||
@ApiStatus.Internal
|
||||
RedisManager getRedisManager();
|
||||
|
||||
@ApiStatus.Internal
|
||||
@NotNull
|
||||
BukkitHuskSync getPlugin();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.migrator;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import me.william278.husksync.bukkit.data.DataSerializer;
|
||||
import net.william278.hslmigrator.HSLConverter;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import net.william278.husksync.util.BukkitLegacyConverter;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static net.william278.husksync.config.Settings.DatabaseSettings;
|
||||
|
||||
public class LegacyMigrator extends Migrator {
|
||||
|
||||
private final HSLConverter hslConverter;
|
||||
private String sourceHost;
|
||||
private int sourcePort;
|
||||
private String sourceUsername;
|
||||
private String sourcePassword;
|
||||
private String sourceDatabase;
|
||||
private String sourcePlayersTable;
|
||||
private String sourceDataTable;
|
||||
|
||||
public LegacyMigrator(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
this.hslConverter = HSLConverter.getInstance();
|
||||
|
||||
final DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
|
||||
this.sourceHost = credentials.getHost();
|
||||
this.sourcePort = credentials.getPort();
|
||||
this.sourceUsername = credentials.getUsername();
|
||||
this.sourcePassword = credentials.getPassword();
|
||||
this.sourceDatabase = credentials.getDatabase();
|
||||
this.sourcePlayersTable = "husksync_players";
|
||||
this.sourceDataTable = "husksync_data";
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> start() {
|
||||
plugin.log(Level.INFO, "Starting migration of legacy HuskSync v1.x data...");
|
||||
final long startTime = System.currentTimeMillis();
|
||||
return plugin.supplyAsync(() -> {
|
||||
// Wipe the existing database, preparing it for data import
|
||||
plugin.log(Level.INFO, "Preparing existing database (wiping)...");
|
||||
plugin.getDatabase().wipeDatabase();
|
||||
plugin.log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
|
||||
|
||||
// Create jdbc driver connection url
|
||||
final String jdbcUrl = "jdbc:mysql://" + sourceHost + ":" + sourcePort + "/" + sourceDatabase;
|
||||
|
||||
// Create a new data source for the mpdb converter
|
||||
try (final HikariDataSource connectionPool = new HikariDataSource()) {
|
||||
plugin.log(Level.INFO, "Establishing connection to legacy database...");
|
||||
connectionPool.setJdbcUrl(jdbcUrl);
|
||||
connectionPool.setUsername(sourceUsername);
|
||||
connectionPool.setPassword(sourcePassword);
|
||||
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase(Locale.ENGLISH));
|
||||
|
||||
plugin.log(Level.INFO, "Downloading raw data from the legacy database (this might take a while)...");
|
||||
final List<LegacyData> dataToMigrate = Lists.newArrayList();
|
||||
try (final Connection connection = connectionPool.getConnection()) {
|
||||
try (final PreparedStatement statement = connection.prepareStatement("""
|
||||
SELECT `uuid`, `username`, `inventory`, `ender_chest`, `health`, `max_health`, `health_scale`, `hunger`, `saturation`, `saturation_exhaustion`, `selected_slot`, `status_effects`, `total_experience`, `exp_level`, `exp_progress`, `game_mode`, `statistics`, `is_flying`, `advancements`, `location`
|
||||
FROM `%source_players_table%`
|
||||
INNER JOIN `%source_data_table%`
|
||||
ON `%source_players_table%`.`id` = `%source_data_table%`.`player_id`
|
||||
WHERE `username` IS NOT NULL;
|
||||
""".replaceAll(Pattern.quote("%source_players_table%"), sourcePlayersTable)
|
||||
.replaceAll(Pattern.quote("%source_data_table%"), sourceDataTable))) {
|
||||
try (final ResultSet resultSet = statement.executeQuery()) {
|
||||
int playersMigrated = 0;
|
||||
while (resultSet.next()) {
|
||||
dataToMigrate.add(new LegacyData(
|
||||
new User(UUID.fromString(resultSet.getString("uuid")),
|
||||
resultSet.getString("username")),
|
||||
resultSet.getString("inventory"),
|
||||
resultSet.getString("ender_chest"),
|
||||
resultSet.getDouble("health"),
|
||||
resultSet.getDouble("max_health"),
|
||||
resultSet.getDouble("health_scale"),
|
||||
resultSet.getInt("hunger"),
|
||||
resultSet.getFloat("saturation"),
|
||||
resultSet.getFloat("saturation_exhaustion"),
|
||||
resultSet.getInt("selected_slot"),
|
||||
resultSet.getString("status_effects"),
|
||||
resultSet.getInt("total_experience"),
|
||||
resultSet.getInt("exp_level"),
|
||||
resultSet.getFloat("exp_progress"),
|
||||
resultSet.getString("game_mode"),
|
||||
resultSet.getString("statistics"),
|
||||
resultSet.getBoolean("is_flying"),
|
||||
resultSet.getString("advancements"),
|
||||
resultSet.getString("location")
|
||||
));
|
||||
playersMigrated++;
|
||||
if (playersMigrated % 50 == 0) {
|
||||
plugin.log(Level.INFO, "Downloaded legacy data for " + playersMigrated + " players...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
plugin.log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the legacy database!");
|
||||
plugin.log(Level.INFO, "Converting HuskSync 1.x data to the new user data format (this might take a while)...");
|
||||
|
||||
final AtomicInteger playersConverted = new AtomicInteger();
|
||||
dataToMigrate.forEach(data -> {
|
||||
final DataSnapshot.Packed convertedData = data.toUserData(hslConverter, plugin);
|
||||
plugin.getDatabase().ensureUser(data.user());
|
||||
try {
|
||||
plugin.getDatabase().addSnapshot(data.user(), convertedData);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().getName() + ": " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
playersConverted.getAndIncrement();
|
||||
if (playersConverted.get() % 50 == 0) {
|
||||
plugin.log(Level.INFO, "Converted legacy data for " + playersConverted + " players...");
|
||||
}
|
||||
});
|
||||
plugin.log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "Error while migrating legacy data: " + e.getMessage() + " - are your source database credentials correct?", e);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleConfigurationCommand(@NotNull String[] args) {
|
||||
if (args.length == 2) {
|
||||
if (switch (args[0].toLowerCase(Locale.ENGLISH)) {
|
||||
case "host" -> {
|
||||
this.sourceHost = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "port" -> {
|
||||
try {
|
||||
this.sourcePort = Integer.parseInt(args[1]);
|
||||
yield true;
|
||||
} catch (NumberFormatException e) {
|
||||
yield false;
|
||||
}
|
||||
}
|
||||
case "username" -> {
|
||||
this.sourceUsername = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "password" -> {
|
||||
this.sourcePassword = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "database" -> {
|
||||
this.sourceDatabase = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "players_table" -> {
|
||||
this.sourcePlayersTable = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "data_table" -> {
|
||||
this.sourceDataTable = args[1];
|
||||
yield true;
|
||||
}
|
||||
default -> false;
|
||||
}) {
|
||||
plugin.log(Level.INFO, getHelpMenu());
|
||||
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]));
|
||||
} else {
|
||||
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||
}
|
||||
} else {
|
||||
plugin.log(Level.INFO, getHelpMenu());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
return "legacy";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "HuskSync v1.x --> v3.x Migrator";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getHelpMenu() {
|
||||
return """
|
||||
=== HuskSync v1.x --> v3.x Migration Wizard =========
|
||||
This will migrate all user data from HuskSync v1.x to
|
||||
HuskSync v3.x's new format. To perform the migration,
|
||||
please follow the steps below carefully.
|
||||
|
||||
[!] Existing data in the database will be wiped. [!]
|
||||
|
||||
STEP 1] Please ensure no players are on any servers.
|
||||
|
||||
STEP 2] HuskSync will need to connect to the database
|
||||
used to hold the existing, legacy HuskSync data.
|
||||
If this is the same database as the one you are
|
||||
currently using, you probably don't need to change
|
||||
anything.
|
||||
Please check that the credentials below are the
|
||||
correct credentials of the source legacy HuskSync
|
||||
database.
|
||||
- host: %source_host%
|
||||
- port: %source_port%
|
||||
- username: %source_username%
|
||||
- password: %source_password%
|
||||
- database: %source_database%
|
||||
- players_table: %source_players_table%
|
||||
- data_table: %source_data_table%
|
||||
If any of these are not correct, please correct them
|
||||
using the command:
|
||||
"husksync migrate legacy set <parameter> <value>"
|
||||
(e.g.: "husksync migrate legacy set host 1.2.3.4")
|
||||
|
||||
STEP 3] HuskSync will migrate data into the database
|
||||
tables configures in the config.yml file of this
|
||||
server. Please make sure you're happy with this
|
||||
before proceeding.
|
||||
|
||||
STEP 4] To start the migration, please run:
|
||||
"husksync migrate legacy start"
|
||||
""".replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost))
|
||||
.replaceAll(Pattern.quote("%source_port%"), Integer.toString(sourcePort))
|
||||
.replaceAll(Pattern.quote("%source_username%"), obfuscateDataString(sourceUsername))
|
||||
.replaceAll(Pattern.quote("%source_password%"), obfuscateDataString(sourcePassword))
|
||||
.replaceAll(Pattern.quote("%source_database%"), sourceDatabase)
|
||||
.replaceAll(Pattern.quote("%source_players_table%"), sourcePlayersTable)
|
||||
.replaceAll(Pattern.quote("%source_data_table%"), sourceDataTable);
|
||||
}
|
||||
|
||||
private record LegacyData(@NotNull User user,
|
||||
@NotNull String serializedInventory, @NotNull String serializedEnderChest,
|
||||
double health, double maxHealth, double healthScale, int hunger, float saturation,
|
||||
float saturationExhaustion, int selectedSlot, @NotNull String serializedPotionEffects,
|
||||
int totalExp, int expLevel, float expProgress,
|
||||
@NotNull String gameMode, @NotNull String serializedStatistics, boolean isFlying,
|
||||
@NotNull String serializedAdvancements, @NotNull String serializedLocation) {
|
||||
|
||||
@NotNull
|
||||
public DataSnapshot.Packed toUserData(@NotNull HSLConverter converter, @NotNull HuskSync plugin) {
|
||||
try {
|
||||
final DataSerializer.StatisticData stats = converter.deserializeStatisticData(serializedStatistics);
|
||||
final DataSerializer.PlayerLocation loc = converter.deserializePlayerLocationData(serializedLocation);
|
||||
final BukkitLegacyConverter adapter = (BukkitLegacyConverter) plugin.getLegacyConverter()
|
||||
.orElseThrow(() -> new IllegalStateException("Legacy converter not present"));
|
||||
|
||||
return DataSnapshot.builder(plugin)
|
||||
// Inventory
|
||||
.inventory(BukkitData.Items.Inventory.from(
|
||||
adapter.deserializeLegacyItemStacks(serializedInventory),
|
||||
selectedSlot
|
||||
))
|
||||
|
||||
// Ender chest
|
||||
.enderChest(BukkitData.Items.EnderChest.adapt(
|
||||
adapter.deserializeLegacyItemStacks(serializedEnderChest)
|
||||
))
|
||||
|
||||
// Location
|
||||
.location(BukkitData.Location.from(
|
||||
loc == null ? 0d : loc.x(),
|
||||
loc == null ? 64d : loc.y(),
|
||||
loc == null ? 0d : loc.z(),
|
||||
loc == null ? 90f : loc.yaw(),
|
||||
loc == null ? 180f : loc.pitch(),
|
||||
new Data.Location.World(
|
||||
loc == null ? "world" : loc.worldName(),
|
||||
UUID.randomUUID(), "NORMAL"
|
||||
)))
|
||||
|
||||
// Advancements
|
||||
.advancements(BukkitData.Advancements.from(converter
|
||||
.deserializeAdvancementData(serializedAdvancements).stream()
|
||||
.map(data -> Data.Advancements.Advancement.adapt(data.key(), data.criteriaMap()))
|
||||
.toList()))
|
||||
|
||||
// Stats
|
||||
.statistics(BukkitData.Statistics.from(
|
||||
convertStatisticMap(stats.untypedStatisticValues()),
|
||||
convertMaterialStatisticMap(stats.blockStatisticValues()),
|
||||
convertMaterialStatisticMap(stats.itemStatisticValues()),
|
||||
convertEntityStatisticMap(stats.entityStatisticValues())
|
||||
))
|
||||
|
||||
// Health, hunger, experience & game mode
|
||||
.health(BukkitData.Health.from(health, healthScale, false))
|
||||
.hunger(BukkitData.Hunger.from(hunger, saturation, saturationExhaustion))
|
||||
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
|
||||
.gameMode(BukkitData.GameMode.from(gameMode))
|
||||
.flightStatus(BukkitData.FlightStatus.from(isFlying, isFlying))
|
||||
|
||||
// Build & pack into new format
|
||||
.saveCause(DataSnapshot.SaveCause.LEGACY_MIGRATION).buildAndPack();
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Integer> convertStatisticMap(@NotNull HashMap<Statistic, Integer> rawMap) {
|
||||
final HashMap<String, Integer> convertedMap = Maps.newHashMap();
|
||||
for (Map.Entry<Statistic, Integer> entry : rawMap.entrySet()) {
|
||||
convertedMap.put(entry.getKey().getKey().toString(), entry.getValue());
|
||||
}
|
||||
return convertedMap;
|
||||
}
|
||||
|
||||
private Map<String, Map<String, Integer>> convertMaterialStatisticMap(@NotNull HashMap<Statistic, HashMap<Material, Integer>> rawMap) {
|
||||
final Map<String, Map<String, Integer>> convertedMap = Maps.newHashMap();
|
||||
for (Map.Entry<Statistic, HashMap<Material, Integer>> entry : rawMap.entrySet()) {
|
||||
for (Map.Entry<Material, Integer> materialEntry : entry.getValue().entrySet()) {
|
||||
convertedMap.computeIfAbsent(entry.getKey().getKey().toString(), k -> new HashMap<>())
|
||||
.put(materialEntry.getKey().getKey().toString(), materialEntry.getValue());
|
||||
}
|
||||
}
|
||||
return convertedMap;
|
||||
}
|
||||
|
||||
private Map<String, Map<String, Integer>> convertEntityStatisticMap(@NotNull HashMap<Statistic, HashMap<EntityType, Integer>> rawMap) {
|
||||
final Map<String, Map<String, Integer>> convertedMap = Maps.newHashMap();
|
||||
for (Map.Entry<Statistic, HashMap<EntityType, Integer>> entry : rawMap.entrySet()) {
|
||||
for (Map.Entry<EntityType, Integer> materialEntry : entry.getValue().entrySet()) {
|
||||
convertedMap.computeIfAbsent(entry.getKey().getKey().toString(), k -> new HashMap<>())
|
||||
.put(materialEntry.getKey().getKey().toString(), materialEntry.getValue());
|
||||
}
|
||||
}
|
||||
return convertedMap;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.migrator;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import net.william278.mpdbconverter.MPDBConverter;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static net.william278.husksync.config.Settings.DatabaseSettings;
|
||||
|
||||
/**
|
||||
* A migrator for migrating MySQLPlayerDataBridge data to HuskSync {@link DataSnapshot}s
|
||||
*/
|
||||
public class MpdbMigrator extends Migrator {
|
||||
|
||||
private final MPDBConverter mpdbConverter;
|
||||
private String sourceHost;
|
||||
private int sourcePort;
|
||||
private String sourceUsername;
|
||||
private String sourcePassword;
|
||||
private String sourceDatabase;
|
||||
private String sourceInventoryTable;
|
||||
private String sourceEnderChestTable;
|
||||
private String sourceExperienceTable;
|
||||
|
||||
public MpdbMigrator(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
this.mpdbConverter = MPDBConverter.getInstance(Objects.requireNonNull(
|
||||
Bukkit.getPluginManager().getPlugin("MySQLPlayerDataBridge"),
|
||||
"MySQLPlayerDataBridge dependency not found!"
|
||||
));
|
||||
final DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
|
||||
this.sourceHost = credentials.getHost();
|
||||
this.sourcePort = credentials.getPort();
|
||||
this.sourceUsername = credentials.getUsername();
|
||||
this.sourcePassword = credentials.getPassword();
|
||||
this.sourceDatabase = credentials.getDatabase();
|
||||
this.sourceInventoryTable = "mpdb_inventory";
|
||||
this.sourceEnderChestTable = "mpdb_enderchest";
|
||||
this.sourceExperienceTable = "mpdb_experience";
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> start() {
|
||||
plugin.log(Level.INFO, "Starting migration from MySQLPlayerDataBridge to HuskSync...");
|
||||
final long startTime = System.currentTimeMillis();
|
||||
return plugin.supplyAsync(() -> {
|
||||
// Wipe the existing database, preparing it for data import
|
||||
plugin.log(Level.INFO, "Preparing existing database (wiping)...");
|
||||
plugin.getDatabase().wipeDatabase();
|
||||
plugin.log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
|
||||
|
||||
// Create jdbc driver connection url
|
||||
final String jdbcUrl = "jdbc:mysql://" + sourceHost + ":" + sourcePort + "/" + sourceDatabase;
|
||||
|
||||
// Create a new data source for the mpdb converter
|
||||
try (final HikariDataSource connectionPool = new HikariDataSource()) {
|
||||
plugin.log(Level.INFO, "Establishing connection to MySQLPlayerDataBridge database...");
|
||||
connectionPool.setJdbcUrl(jdbcUrl);
|
||||
connectionPool.setUsername(sourceUsername);
|
||||
connectionPool.setPassword(sourcePassword);
|
||||
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase(Locale.ENGLISH));
|
||||
|
||||
plugin.log(Level.INFO, "Downloading raw data from the MySQLPlayerDataBridge database (this might take a while)...");
|
||||
final List<MpdbData> dataToMigrate = Lists.newArrayList();
|
||||
try (final Connection connection = connectionPool.getConnection()) {
|
||||
try (final PreparedStatement statement = connection.prepareStatement("""
|
||||
SELECT `%source_inventory_table%`.`player_uuid`, `%source_inventory_table%`.`player_name`, `inventory`, `armor`, `enderchest`, `exp_lvl`, `exp`, `total_exp`
|
||||
FROM `%source_inventory_table%`
|
||||
INNER JOIN `%source_ender_chest_table%`
|
||||
ON `%source_inventory_table%`.`player_uuid` = `%source_ender_chest_table%`.`player_uuid`
|
||||
INNER JOIN `%source_xp_table%`
|
||||
ON `%source_inventory_table%`.`player_uuid` = `%source_xp_table%`.`player_uuid`;
|
||||
""".replaceAll(Pattern.quote("%source_inventory_table%"), sourceInventoryTable)
|
||||
.replaceAll(Pattern.quote("%source_ender_chest_table%"), sourceEnderChestTable)
|
||||
.replaceAll(Pattern.quote("%source_xp_table%"), sourceExperienceTable))) {
|
||||
try (final ResultSet resultSet = statement.executeQuery()) {
|
||||
int playersMigrated = 0;
|
||||
while (resultSet.next()) {
|
||||
dataToMigrate.add(new MpdbData(
|
||||
new User(UUID.fromString(resultSet.getString("player_uuid")),
|
||||
resultSet.getString("player_name")),
|
||||
resultSet.getString("inventory"),
|
||||
resultSet.getString("armor"),
|
||||
resultSet.getString("enderchest"),
|
||||
resultSet.getInt("exp_lvl"),
|
||||
resultSet.getInt("exp"),
|
||||
resultSet.getInt("total_exp")
|
||||
));
|
||||
playersMigrated++;
|
||||
if (playersMigrated % 25 == 0) {
|
||||
plugin.log(Level.INFO, "Downloaded MySQLPlayerDataBridge data for " + playersMigrated + " players...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
plugin.log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the MySQLPlayerDataBridge database!");
|
||||
plugin.log(Level.INFO, "Converting raw MySQLPlayerDataBridge data to HuskSync user data (this might take a while)...");
|
||||
|
||||
final AtomicInteger playersConverted = new AtomicInteger();
|
||||
dataToMigrate.forEach(data -> {
|
||||
final DataSnapshot.Packed convertedData = data.toUserData(mpdbConverter, plugin);
|
||||
plugin.getDatabase().ensureUser(data.user());
|
||||
plugin.getDatabase().addSnapshot(data.user(), convertedData);
|
||||
playersConverted.getAndIncrement();
|
||||
if (playersConverted.get() % 50 == 0) {
|
||||
plugin.log(Level.INFO, "Converted MySQLPlayerDataBridge data for " + playersConverted + " players...");
|
||||
}
|
||||
});
|
||||
plugin.log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "Error while migrating data: " + e.getMessage() + " - are your source database credentials correct?");
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleConfigurationCommand(@NotNull String[] args) {
|
||||
if (args.length == 2) {
|
||||
if (switch (args[0].toLowerCase(Locale.ENGLISH)) {
|
||||
case "host" -> {
|
||||
this.sourceHost = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "port" -> {
|
||||
try {
|
||||
this.sourcePort = Integer.parseInt(args[1]);
|
||||
yield true;
|
||||
} catch (NumberFormatException e) {
|
||||
yield false;
|
||||
}
|
||||
}
|
||||
case "username" -> {
|
||||
this.sourceUsername = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "password" -> {
|
||||
this.sourcePassword = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "database" -> {
|
||||
this.sourceDatabase = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "inventory_table" -> {
|
||||
this.sourceInventoryTable = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "ender_chest_table" -> {
|
||||
this.sourceEnderChestTable = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "experience_table" -> {
|
||||
this.sourceExperienceTable = args[1];
|
||||
yield true;
|
||||
}
|
||||
default -> false;
|
||||
}) {
|
||||
plugin.log(Level.INFO, getHelpMenu());
|
||||
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]));
|
||||
} else {
|
||||
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||
}
|
||||
} else {
|
||||
plugin.log(Level.INFO, getHelpMenu());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
return "mpdb";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "MySQLPlayerDataBridge Migrator";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getHelpMenu() {
|
||||
return """
|
||||
=== MySQLPlayerDataBridge Migration Wizard ==========
|
||||
NOTE: This migrator currently WORKS WITH MPDB version
|
||||
v4.9.2 and below!
|
||||
|
||||
This will migrate inventories, ender chests and XP
|
||||
from the MySQLPlayerDataBridge plugin to HuskSync.
|
||||
|
||||
To prevent excessive migration times, other non-vital
|
||||
data will not be transferred.
|
||||
|
||||
[!] Existing data in the database will be wiped. [!]
|
||||
|
||||
STEP 1] Please ensure no players are on any servers.
|
||||
|
||||
STEP 2] HuskSync will need to connect to the database
|
||||
used to hold the source MySQLPlayerDataBridge data.
|
||||
Please check these database parameters are OK:
|
||||
- host: %source_host%
|
||||
- port: %source_port%
|
||||
- username: %source_username%
|
||||
- password: %source_password%
|
||||
- database: %source_database%
|
||||
- inventory_table: %source_inventory_table%
|
||||
- ender_chest_table: %source_ender_chest_table%
|
||||
- experience_table: %source_xp_table%
|
||||
If any of these are not correct, please correct them
|
||||
using the command:
|
||||
"husksync migrate mpdb set <parameter> <value>"
|
||||
(e.g.: "husksync migrate set mpdb host 1.2.3.4")
|
||||
|
||||
STEP 3] HuskSync will migrate data into the database
|
||||
tables configures in the config.yml file of this
|
||||
server. Please make sure you're happy with this
|
||||
before proceeding.
|
||||
|
||||
STEP 4] To start the migration, please run:
|
||||
"husksync migrate start mpdb"
|
||||
|
||||
NOTE: This migrator currently WORKS WITH MPDB version
|
||||
v4.9.2 and below!
|
||||
""".replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost))
|
||||
.replaceAll(Pattern.quote("%source_port%"), Integer.toString(sourcePort))
|
||||
.replaceAll(Pattern.quote("%source_username%"), obfuscateDataString(sourceUsername))
|
||||
.replaceAll(Pattern.quote("%source_password%"), obfuscateDataString(sourcePassword))
|
||||
.replaceAll(Pattern.quote("%source_database%"), sourceDatabase)
|
||||
.replaceAll(Pattern.quote("%source_inventory_table%"), sourceInventoryTable)
|
||||
.replaceAll(Pattern.quote("%source_ender_chest_table%"), sourceEnderChestTable)
|
||||
.replaceAll(Pattern.quote("%source_xp_table%"), sourceExperienceTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents data exported from the MySQLPlayerDataBridge source database
|
||||
*
|
||||
* @param user The user whose data is being migrated
|
||||
* @param serializedInventory The serialized inventory data
|
||||
* @param serializedArmor The serialized armor data
|
||||
* @param serializedEnderChest The serialized ender chest data
|
||||
* @param expLevel The player's current XP level
|
||||
* @param expProgress The player's current XP progress
|
||||
* @param totalExp The player's total XP score
|
||||
*/
|
||||
private record MpdbData(
|
||||
@NotNull User user,
|
||||
@NotNull String serializedInventory,
|
||||
@NotNull String serializedArmor,
|
||||
@NotNull String serializedEnderChest,
|
||||
int expLevel,
|
||||
float expProgress,
|
||||
int totalExp
|
||||
) {
|
||||
|
||||
/**
|
||||
* Converts exported MySQLPlayerDataBridge data into HuskSync's {@link DataSnapshot} object format
|
||||
*
|
||||
* @param converter The {@link MPDBConverter} to use for converting to {@link ItemStack}s
|
||||
* @return A {@link CompletableFuture} that will resolve to the converted {@link DataSnapshot} object
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Packed toUserData(@NotNull MPDBConverter converter, @NotNull HuskSync plugin) {
|
||||
// Combine inventory and armor
|
||||
final Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
|
||||
inventory.setContents(converter.getItemStackFromSerializedData(serializedInventory));
|
||||
final ItemStack[] armor = converter.getItemStackFromSerializedData(serializedArmor).clone();
|
||||
for (int i = 36; i < 36 + armor.length; i++) {
|
||||
inventory.setItem(i, armor[i - 36]);
|
||||
}
|
||||
final ItemStack[] enderChest = converter.getItemStackFromSerializedData(serializedEnderChest);
|
||||
|
||||
// Create user data record
|
||||
return DataSnapshot.builder(plugin)
|
||||
.inventory(BukkitData.Items.Inventory.from(inventory.getContents(), 0))
|
||||
.enderChest(BukkitData.Items.EnderChest.adapt(enderChest))
|
||||
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
|
||||
.gameMode(BukkitData.GameMode.from("SURVIVAL"))
|
||||
.saveCause(DataSnapshot.SaveCause.MPDB_MIGRATION)
|
||||
.buildAndPack();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.user;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import dev.triumphteam.gui.builder.gui.StorageBuilder;
|
||||
import dev.triumphteam.gui.guis.Gui;
|
||||
import dev.triumphteam.gui.guis.StorageGui;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.data.BukkitUserDataHolder;
|
||||
import net.william278.husksync.data.Data;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Bukkit platform implementation of an {@link OnlineUser}
|
||||
*/
|
||||
public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
||||
|
||||
private final HuskSync plugin;
|
||||
private final Player player;
|
||||
|
||||
private BukkitUser(@NotNull Player player, @NotNull HuskSync plugin) {
|
||||
super(player.getUniqueId(), player.getName());
|
||||
this.player = player;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public static BukkitUser adapt(@NotNull Player player, @NotNull HuskSync plugin) {
|
||||
return new BukkitUser(player, plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasDisconnected() {
|
||||
return getPlugin().getDisconnectingPlayers().contains(getUuid())
|
||||
|| player == null || !player.isOnline();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated(since = "3.6.7")
|
||||
public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
|
||||
@NotNull String iconMaterial, @NotNull String backgroundType) {
|
||||
plugin.log(Level.WARNING, "Toast notifications are deprecated. " +
|
||||
"Please change your notification display slot to CHAT, ACTION_BAR or NONE.");
|
||||
this.sendActionBar(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showGui(@NotNull Data.Items items, @NotNull MineDown title, boolean editable, int size,
|
||||
@NotNull Consumer<Data.Items> onClose) {
|
||||
final ItemStack[] contents = ((BukkitData.Items) items).getContents();
|
||||
final StorageBuilder builder = Gui.storage().rows((int) Math.ceil(size / 9.0d));
|
||||
if (!editable) {
|
||||
builder.disableAllInteractions();
|
||||
}
|
||||
final StorageGui gui = builder
|
||||
.apply(a -> a.getInventory().setContents(contents))
|
||||
.title(title.toComponent()).create();
|
||||
gui.setCloseGuiAction((close) -> onClose.accept(BukkitData.Items.ItemArray.adapt(
|
||||
Arrays.stream(close.getInventory().getContents()).limit(size).toArray(ItemStack[]::new)
|
||||
)));
|
||||
plugin.runSync(() -> gui.open(player), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(@NotNull String node) {
|
||||
return player.hasPermission(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDead() {
|
||||
return player.getHealth() <= 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLocked() {
|
||||
return plugin.getLockedPlayers().contains(player.getUniqueId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNpc() {
|
||||
return player.hasMetadata("NPC");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Bukkit {@link Player} instance of this user
|
||||
*
|
||||
* @return the {@link Player} instance
|
||||
* @since 3.6
|
||||
*/
|
||||
@NotNull
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
@ApiStatus.Internal
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
// Utility class for adapting "Keyed" Bukkit objects
|
||||
public final class BukkitKeyedAdapter {
|
||||
|
||||
@Nullable
|
||||
public static Statistic matchStatistic(@NotNull String key) {
|
||||
return getRegistryValue(Registry.STATISTIC, key);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static EntityType matchEntityType(@NotNull String key) {
|
||||
return getRegistryValue(Registry.ENTITY_TYPE, key);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Material matchMaterial(@NotNull String key) {
|
||||
return getRegistryValue(Registry.MATERIAL, key);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Attribute matchAttribute(@NotNull String key) {
|
||||
return getRegistryValue(Registry.ATTRIBUTE, key);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static PotionEffectType matchEffectType(@NotNull String key) {
|
||||
return getRegistryValue(Registry.EFFECT, key);
|
||||
}
|
||||
|
||||
private static <T extends Keyed> T getRegistryValue(@NotNull Registry<T> registry, @NotNull String keyString) {
|
||||
final NamespacedKey key = NamespacedKey.fromString(keyString);
|
||||
return key != null ? registry.get(key) : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static net.william278.husksync.util.BukkitKeyedAdapter.matchEntityType;
|
||||
import static net.william278.husksync.util.BukkitKeyedAdapter.matchMaterial;
|
||||
|
||||
public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
public BukkitLegacyConverter(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public DataSnapshot.Packed convert(byte @NotNull [] data, @NotNull UUID id,
|
||||
@NotNull OffsetDateTime timestamp) throws DataAdapter.AdaptionException {
|
||||
final JSONObject object = new JSONObject(plugin.getDataAdapter().bytesToString(data));
|
||||
final int version = object.getInt("format_version");
|
||||
if (version != 3) {
|
||||
plugin.log(Level.WARNING, String.format("Converting data from older v2 data format (%s).", version));
|
||||
}
|
||||
|
||||
// Read legacy data from the JSON object
|
||||
final DataSnapshot.Builder builder = DataSnapshot.builder(plugin)
|
||||
.id(id).timestamp(timestamp)
|
||||
.saveCause(DataSnapshot.SaveCause.CONVERTED_FROM_V2)
|
||||
.data(readStatusData(object));
|
||||
readInventory(object).ifPresent(builder::inventory);
|
||||
readEnderChest(object).ifPresent(builder::enderChest);
|
||||
readLocation(object).ifPresent(builder::location);
|
||||
readAdvancements(object).ifPresent(builder::advancements);
|
||||
readStatistics(object).ifPresent(builder::statistics);
|
||||
return builder.buildAndPack();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map<Identifier, Data> readStatusData(@NotNull JSONObject object) {
|
||||
if (!object.has("status_data")) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
final JSONObject status = object.getJSONObject("status_data");
|
||||
final HashMap<Identifier, Data> containers = Maps.newHashMap();
|
||||
if (Identifier.HEALTH.isEnabled()) {
|
||||
containers.put(Identifier.HEALTH, BukkitData.Health.from(
|
||||
status.getDouble("health"),
|
||||
status.getDouble("health_scale"),
|
||||
false
|
||||
));
|
||||
}
|
||||
if (Identifier.HUNGER.isEnabled()) {
|
||||
containers.put(Identifier.HUNGER, BukkitData.Hunger.from(
|
||||
status.getInt("hunger"),
|
||||
status.getFloat("saturation"),
|
||||
status.getFloat("saturation_exhaustion")
|
||||
));
|
||||
}
|
||||
if (Identifier.EXPERIENCE.isEnabled()) {
|
||||
containers.put(Identifier.EXPERIENCE, BukkitData.Experience.from(
|
||||
status.getInt("total_experience"),
|
||||
status.getInt("experience_level"),
|
||||
status.getFloat("experience_progress")
|
||||
));
|
||||
}
|
||||
if (Identifier.GAME_MODE.isEnabled()) {
|
||||
containers.put(Identifier.GAME_MODE, BukkitData.GameMode.from(
|
||||
status.getString("game_mode")
|
||||
));
|
||||
}
|
||||
if (Identifier.FLIGHT_STATUS.isEnabled()) {
|
||||
containers.put(Identifier.FLIGHT_STATUS, BukkitData.FlightStatus.from(
|
||||
status.getBoolean("is_flying"),
|
||||
status.getBoolean("is_flying")
|
||||
));
|
||||
}
|
||||
return containers;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Items.Inventory> readInventory(@NotNull JSONObject object) {
|
||||
if (!object.has("inventory") || !Identifier.INVENTORY.isEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final JSONObject inventoryData = object.getJSONObject("inventory");
|
||||
return Optional.of(BukkitData.Items.Inventory.from(
|
||||
deserializeLegacyItemStacks(inventoryData.getString("serialized_items")), 0
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Items.EnderChest> readEnderChest(@NotNull JSONObject object) {
|
||||
if (!object.has("ender_chest") || !Identifier.ENDER_CHEST.isEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final JSONObject inventoryData = object.getJSONObject("ender_chest");
|
||||
return Optional.of(BukkitData.Items.EnderChest.adapt(
|
||||
deserializeLegacyItemStacks(inventoryData.getString("serialized_items"))
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Location> readLocation(@NotNull JSONObject object) {
|
||||
if (!object.has("location") || !Identifier.LOCATION.isEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final JSONObject locationData = object.getJSONObject("location");
|
||||
return Optional.of(BukkitData.Location.from(
|
||||
locationData.getDouble("x"),
|
||||
locationData.getDouble("y"),
|
||||
locationData.getDouble("z"),
|
||||
locationData.getFloat("yaw"),
|
||||
locationData.getFloat("pitch"),
|
||||
new Data.Location.World(
|
||||
locationData.getString("world_name"),
|
||||
UUID.fromString(locationData.getString("world_uuid")),
|
||||
locationData.getString("world_environment")
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Advancements> readAdvancements(@NotNull JSONObject object) {
|
||||
if (!object.has("advancements") || !Identifier.ADVANCEMENTS.isEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final JSONArray advancements = object.getJSONArray("advancements");
|
||||
final List<Data.Advancements.Advancement> converted = Lists.newArrayList();
|
||||
advancements.iterator().forEachRemaining(o -> {
|
||||
final JSONObject advancement = (JSONObject) JSONObject.wrap(o);
|
||||
final String key = advancement.getString("key");
|
||||
|
||||
final JSONObject criteria = advancement.getJSONObject("completed_criteria");
|
||||
final Map<String, Date> criteriaMap = new LinkedHashMap<>();
|
||||
criteria.keys().forEachRemaining(criteriaKey -> criteriaMap.put(
|
||||
criteriaKey, parseDate(criteria.getString(criteriaKey)))
|
||||
);
|
||||
converted.add(Data.Advancements.Advancement.adapt(key, criteriaMap));
|
||||
});
|
||||
|
||||
return Optional.of(BukkitData.Advancements.from(converted));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Statistics> readStatistics(@NotNull JSONObject object) {
|
||||
if (!object.has("statistics") || !Identifier.STATISTICS.isEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final JSONObject stats = object.getJSONObject("statistics");
|
||||
return Optional.of(readStatisticMaps(
|
||||
stats.getJSONObject("untyped_statistics"),
|
||||
stats.getJSONObject("block_statistics"),
|
||||
stats.getJSONObject("item_statistics"),
|
||||
stats.getJSONObject("entity_statistics")
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private BukkitData.Statistics readStatisticMaps(@NotNull JSONObject untyped, @NotNull JSONObject blocks,
|
||||
@NotNull JSONObject items, @NotNull JSONObject entities) {
|
||||
// Read generic stats
|
||||
final Map<String, Integer> genericStats = Maps.newHashMap();
|
||||
untyped.keys().forEachRemaining(stat -> genericStats.put(stat, untyped.getInt(stat)));
|
||||
|
||||
// Read block & item stats
|
||||
final Map<String, Map<String, Integer>> blockStats, itemStats, entityStats;
|
||||
blockStats = readMaterialStatistics(blocks);
|
||||
itemStats = readMaterialStatistics(items);
|
||||
|
||||
// Read entity stats
|
||||
entityStats = Maps.newHashMap();
|
||||
entities.keys().forEachRemaining(stat -> {
|
||||
final JSONObject entityStat = entities.getJSONObject(stat);
|
||||
final Map<String, Integer> entityMap = Maps.newHashMap();
|
||||
entityStat.keys().forEachRemaining(entity -> {
|
||||
if (matchEntityType(entity) != null) {
|
||||
entityMap.put(entity, entityStat.getInt(entity));
|
||||
}
|
||||
});
|
||||
entityStats.put(stat, entityMap);
|
||||
});
|
||||
|
||||
return BukkitData.Statistics.from(genericStats, blockStats, itemStats, entityStats);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map<String, Map<String, Integer>> readMaterialStatistics(@NotNull JSONObject items) {
|
||||
final Map<String, Map<String, Integer>> itemStats = Maps.newHashMap();
|
||||
items.keys().forEachRemaining(stat -> {
|
||||
final JSONObject itemStat = items.getJSONObject(stat);
|
||||
final Map<String, Integer> itemMap = Maps.newHashMap();
|
||||
itemStat.keys().forEachRemaining(item -> {
|
||||
if (matchMaterial(item) != null) {
|
||||
itemMap.put(item, itemStat.getInt(item));
|
||||
}
|
||||
});
|
||||
itemStats.put(stat, itemMap);
|
||||
});
|
||||
return itemStats;
|
||||
}
|
||||
|
||||
// Deserialize a legacy item stack array
|
||||
@NotNull
|
||||
public ItemStack[] deserializeLegacyItemStacks(@NotNull String items) {
|
||||
// Return an empty array if there is no inventory data (set the player as having an empty inventory)
|
||||
if (items.isEmpty()) {
|
||||
return new ItemStack[0];
|
||||
}
|
||||
|
||||
// Create a byte input stream to read the serialized data
|
||||
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(items))) {
|
||||
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
|
||||
// Read the length of the Bukkit input stream and set the length of the array to this value
|
||||
final ItemStack[] inventoryContents = new ItemStack[bukkitInputStream.readInt()];
|
||||
|
||||
// Set the ItemStacks in the array from deserialized ItemStack data
|
||||
int slotIndex = 0;
|
||||
for (ItemStack ignored : inventoryContents) {
|
||||
final ItemStack deserialized = deserializeLegacyItemStack(bukkitInputStream.readObject());
|
||||
inventoryContents[slotIndex] = deserialized;
|
||||
slotIndex++;
|
||||
}
|
||||
|
||||
// Return the converted contents
|
||||
return inventoryContents;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
throw new DataAdapter.AdaptionException("Failed to deserialize legacy item stack data", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Deserialize a single legacy item stack
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
private static ItemStack deserializeLegacyItemStack(@Nullable Object serializedItemStack) {
|
||||
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Date parseDate(@NotNull String dateString) {
|
||||
try {
|
||||
return new SimpleDateFormat().parse(dateString);
|
||||
} catch (ParseException e) {
|
||||
return new Date();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.UserDataHolder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
|
||||
import space.arim.morepaperlib.scheduling.AttachedScheduler;
|
||||
import space.arim.morepaperlib.scheduling.RegionalScheduler;
|
||||
import space.arim.morepaperlib.scheduling.ScheduledTask;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
public interface BukkitTask extends Task {
|
||||
|
||||
class Sync extends Task.Sync implements BukkitTask {
|
||||
|
||||
private ScheduledTask task;
|
||||
private final @Nullable UserDataHolder user;
|
||||
|
||||
protected Sync(@NotNull HuskSync plugin, @NotNull Runnable runnable,
|
||||
@Nullable UserDataHolder user, long delayTicks) {
|
||||
super(plugin, runnable, delayTicks);
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (task != null && !cancelled) {
|
||||
task.cancel();
|
||||
}
|
||||
super.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (isPluginDisabled()) {
|
||||
runnable.run();
|
||||
return;
|
||||
}
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use entity-specific scheduler if user is not null
|
||||
if (user != null) {
|
||||
final AttachedScheduler scheduler = ((BukkitHuskSync) getPlugin()).getUserSyncScheduler(user);
|
||||
if (delayTicks > 0) {
|
||||
this.task = scheduler.runDelayed(runnable, null, delayTicks);
|
||||
} else {
|
||||
this.task = scheduler.run(runnable, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Or default to the global scheduler
|
||||
final RegionalScheduler scheduler = ((BukkitHuskSync) getPlugin()).getSyncScheduler();
|
||||
if (delayTicks > 0) {
|
||||
this.task = scheduler.runDelayed(runnable, delayTicks);
|
||||
} else {
|
||||
this.task = scheduler.run(runnable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Async extends Task.Async implements BukkitTask {
|
||||
|
||||
private ScheduledTask task;
|
||||
|
||||
protected Async(@NotNull HuskSync plugin, @NotNull Runnable runnable, long delayTicks) {
|
||||
super(plugin, runnable, delayTicks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (task != null && !cancelled) {
|
||||
task.cancel();
|
||||
}
|
||||
super.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (isPluginDisabled()) {
|
||||
runnable.run();
|
||||
return;
|
||||
}
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
final AsynchronousScheduler scheduler = ((BukkitHuskSync) getPlugin()).getAsyncScheduler();
|
||||
if (delayTicks > 0) {
|
||||
plugin.debug("Running async task with delay of " + delayTicks + " ticks");
|
||||
this.task = scheduler.runDelayed(
|
||||
runnable,
|
||||
Duration.of(delayTicks * 50L, ChronoUnit.MILLIS)
|
||||
);
|
||||
} else {
|
||||
this.task = scheduler.run(runnable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Repeating extends Task.Repeating implements BukkitTask {
|
||||
|
||||
private ScheduledTask task;
|
||||
|
||||
protected Repeating(@NotNull HuskSync plugin, @NotNull Runnable runnable, long repeatingTicks) {
|
||||
super(plugin, runnable, repeatingTicks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (task != null && !cancelled) {
|
||||
task.cancel();
|
||||
}
|
||||
super.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (isPluginDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cancelled) {
|
||||
final AsynchronousScheduler scheduler = ((BukkitHuskSync) getPlugin()).getAsyncScheduler();
|
||||
this.task = scheduler.runAtFixedRate(
|
||||
runnable, Duration.ZERO,
|
||||
Duration.of(repeatingTicks * 50L, ChronoUnit.MILLIS)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns if the Bukkit HuskSync plugin is disabled
|
||||
default boolean isPluginDisabled() {
|
||||
return !((BukkitHuskSync) getPlugin()).isEnabled();
|
||||
}
|
||||
|
||||
interface Supplier extends Task.Supplier {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Task.Sync getSyncTask(@NotNull Runnable runnable, @Nullable UserDataHolder user, long delayTicks) {
|
||||
return new Sync(getPlugin(), runnable, user, delayTicks);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Task.Async getAsyncTask(@NotNull Runnable runnable, long delayTicks) {
|
||||
return new Async(getPlugin(), runnable, delayTicks);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Task.Repeating getRepeatingTask(@NotNull Runnable runnable, long repeatingTicks) {
|
||||
return new Repeating(getPlugin(), runnable, repeatingTicks);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void cancelTasks() {
|
||||
((BukkitHuskSync) getPlugin()).getScheduler().cancelGlobalTasks();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
0
bukkit/src/main/resources/META-INF/.mojang-mapped
Normal file
0
bukkit/src/main/resources/META-INF/.mojang-mapped
Normal file
2
bukkit/src/main/resources/compatibility.yml
Normal file
2
bukkit/src/main/resources/compatibility.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
# File used for checking Minecraft server compatibility with this version of HuskSync
|
||||
minecraft_version_range: '${minecraft_version_range}'
|
||||
@@ -1,22 +0,0 @@
|
||||
redis_settings:
|
||||
host: 'localhost'
|
||||
port: 6379
|
||||
password: ''
|
||||
use_ssl: false
|
||||
synchronisation_settings:
|
||||
inventories: true
|
||||
ender_chests: true
|
||||
health: true
|
||||
hunger: true
|
||||
experience: true
|
||||
potion_effects: true
|
||||
statistics: true
|
||||
game_mode: true
|
||||
advancements: true
|
||||
location: false
|
||||
flight: false
|
||||
cluster_id: 'main'
|
||||
check_for_updates: true
|
||||
synchronization_timeout_retry_delay: 15
|
||||
save_on_world_save: true
|
||||
native_advancement_synchronization: false
|
||||
8
bukkit/src/main/resources/paper-libraries.yml
Normal file
8
bukkit/src/main/resources/paper-libraries.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
# Dependencies for HuskSync on Paper
|
||||
libraries:
|
||||
- 'redis.clients:jedis:${jedis_version}'
|
||||
- 'com.mysql:mysql-connector-j:${mysql_driver_version}'
|
||||
- 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}'
|
||||
- 'org.postgresql:postgresql:${postgres_driver_version}'
|
||||
- 'org.mongodb:mongodb-driver-sync:${mongodb_driver_version}'
|
||||
- 'org.xerial.snappy:snappy-java:${snappy_version}'
|
||||
27
bukkit/src/main/resources/paper-plugin.yml
Normal file
27
bukkit/src/main/resources/paper-plugin.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
name: 'HuskSync'
|
||||
description: '${description}'
|
||||
author: 'William278'
|
||||
website: 'https://william278.net/'
|
||||
main: 'net.william278.husksync.PaperHuskSync'
|
||||
loader: 'net.william278.husksync.PaperHuskSyncLoader'
|
||||
version: '${version}'
|
||||
api-version: '${minecraft_api_version}'
|
||||
folia-supported: true
|
||||
dependencies:
|
||||
server:
|
||||
packetevents:
|
||||
required: false
|
||||
load: BEFORE
|
||||
join-classpath: true
|
||||
ProtocolLib:
|
||||
required: false
|
||||
load: BEFORE
|
||||
join-classpath: true
|
||||
MysqlPlayerDataBridge:
|
||||
required: false
|
||||
load: BEFORE
|
||||
join-classpath: true
|
||||
Plan:
|
||||
required: false
|
||||
load: BEFORE
|
||||
join-classpath: true
|
||||
@@ -1,8 +1,20 @@
|
||||
name: HuskSync
|
||||
version: ${version}
|
||||
main: net.william278.husksync.HuskSyncBukkit
|
||||
api-version: 1.16
|
||||
author: William278
|
||||
description: 'A modern, cross-server player data synchronization system'
|
||||
name: 'HuskSync'
|
||||
version: '${version}'
|
||||
main: 'net.william278.husksync.BukkitHuskSync'
|
||||
api-version: '${minecraft_api_version}'
|
||||
author: 'William278'
|
||||
description: '${description}'
|
||||
website: 'https://william278.net'
|
||||
softdepend: [MysqlPlayerDataBridge]
|
||||
folia-supported: true
|
||||
softdepend:
|
||||
- 'packetevents'
|
||||
- 'ProtocolLib'
|
||||
- 'MysqlPlayerDataBridge'
|
||||
- 'Plan'
|
||||
libraries:
|
||||
- 'redis.clients:jedis:${jedis_version}'
|
||||
- 'com.mysql:mysql-connector-j:${mysql_driver_version}'
|
||||
- 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}'
|
||||
- 'org.postgresql:postgresql:${postgres_driver_version}'
|
||||
- 'org.mongodb:mongodb-driver-sync:${mongodb_driver_version}'
|
||||
- 'org.xerial.snappy:snappy-java:${snappy_version}'
|
||||
@@ -1,23 +0,0 @@
|
||||
dependencies {
|
||||
implementation project(path: ':common')
|
||||
|
||||
implementation 'com.zaxxer:HikariCP:5.0.1'
|
||||
implementation 'org.bstats:bstats-bungeecord:3.0.0'
|
||||
implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
|
||||
implementation 'net.byteflux:libby-bungee:1.1.5'
|
||||
|
||||
compileOnly 'net.md-5:bungeecord-api:1.16-R0.5-SNAPSHOT'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'net.byteflux', 'net.william278.husksync.libraries'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.apache', 'net.william278.husksync.libraries'
|
||||
|
||||
dependencies {
|
||||
//noinspection GroovyAssignabilityCheck
|
||||
exclude dependency(':slf4j-api')
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import net.byteflux.libby.BungeeLibraryManager;
|
||||
import net.byteflux.libby.Library;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.william278.husksync.bungeecord.command.BungeeCommand;
|
||||
import net.william278.husksync.bungeecord.config.ConfigLoader;
|
||||
import net.william278.husksync.bungeecord.config.ConfigManager;
|
||||
import net.william278.husksync.bungeecord.listener.BungeeEventListener;
|
||||
import net.william278.husksync.bungeecord.listener.BungeeRedisListener;
|
||||
import net.william278.husksync.bungeecord.util.BungeeLogger;
|
||||
import net.william278.husksync.bungeecord.util.BungeeUpdateChecker;
|
||||
import net.william278.husksync.migrator.MPDBMigrator;
|
||||
import net.william278.husksync.proxy.data.DataManager;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.william278.husksync.util.Logger;
|
||||
import org.bstats.bungeecord.Metrics;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class HuskSyncBungeeCord extends Plugin {
|
||||
|
||||
// BungeeCord bStats ID (different to Bukkit)
|
||||
private static final int METRICS_ID = 13141;
|
||||
|
||||
private static HuskSyncBungeeCord instance;
|
||||
|
||||
public static HuskSyncBungeeCord getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Whether the plugin is ready to accept redis messages
|
||||
public static boolean readyForRedis = false;
|
||||
|
||||
// Whether the plugin is in the process of disabling and should skip responding to handshake confirmations
|
||||
public static boolean isDisabling = false;
|
||||
|
||||
/**
|
||||
* Set of all the {@link Server}s that have completed the synchronisation handshake with HuskSync on the proxy
|
||||
*/
|
||||
public static HashSet<Server> synchronisedServers;
|
||||
|
||||
public static DataManager dataManager;
|
||||
|
||||
public static MPDBMigrator mpdbMigrator;
|
||||
|
||||
public static BungeeRedisListener redisListener;
|
||||
|
||||
private Logger logger;
|
||||
|
||||
public Logger getBungeeLogger() {
|
||||
return logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
instance = this;
|
||||
logger = new BungeeLogger(getLogger());
|
||||
fetchDependencies();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// Plugin startup logic
|
||||
synchronisedServers = new HashSet<>();
|
||||
|
||||
// Load config
|
||||
ConfigManager.loadConfig();
|
||||
|
||||
// Load settings from config
|
||||
ConfigLoader.loadSettings(Objects.requireNonNull(ConfigManager.getConfig()));
|
||||
|
||||
// Load messages
|
||||
ConfigManager.loadMessages();
|
||||
|
||||
// Load locales from messages
|
||||
ConfigLoader.loadMessageStrings(Objects.requireNonNull(ConfigManager.getMessages()));
|
||||
|
||||
// Do update checker
|
||||
if (Settings.automaticUpdateChecks) {
|
||||
new BungeeUpdateChecker(getDescription().getVersion()).logToConsole();
|
||||
}
|
||||
|
||||
// Setup data manager
|
||||
dataManager = new DataManager(getBungeeLogger(), getDataFolder());
|
||||
|
||||
// Ensure the data manager initialized correctly
|
||||
if (dataManager.hasFailedInitialization) {
|
||||
getBungeeLogger().severe("Failed to initialize the HuskSync database(s).\n" +
|
||||
"HuskSync will now abort loading itself (" + getProxy().getName() + ") v" + getDescription().getVersion());
|
||||
}
|
||||
|
||||
// Setup player data cache
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
dataManager.playerDataCache.put(cluster, new DataManager.PlayerDataCache());
|
||||
}
|
||||
|
||||
// Initialize the redis listener
|
||||
redisListener = new BungeeRedisListener();
|
||||
|
||||
// Register listener
|
||||
getProxy().getPluginManager().registerListener(this, new BungeeEventListener());
|
||||
|
||||
// Register command
|
||||
getProxy().getPluginManager().registerCommand(this, new BungeeCommand());
|
||||
|
||||
// Prepare the migrator for use if needed
|
||||
mpdbMigrator = new MPDBMigrator(getBungeeLogger());
|
||||
|
||||
// Initialize bStats metrics
|
||||
try {
|
||||
new Metrics(this, METRICS_ID);
|
||||
} catch (Exception e) {
|
||||
getBungeeLogger().info("Skipped metrics initialization");
|
||||
}
|
||||
|
||||
// Log to console
|
||||
getBungeeLogger().info("Enabled HuskSync (" + getProxy().getName() + ") v" + getDescription().getVersion());
|
||||
|
||||
// Mark as ready for redis message processing
|
||||
readyForRedis = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
// Plugin shutdown logic
|
||||
isDisabling = true;
|
||||
|
||||
// Send terminating handshake message
|
||||
for (Server server : synchronisedServers) {
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.TERMINATE_HANDSHAKE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, server.clusterId()),
|
||||
server.serverUUID().toString(),
|
||||
ProxyServer.getInstance().getName()).send();
|
||||
} catch (IOException e) {
|
||||
getBungeeLogger().log(Level.SEVERE, "Failed to serialize Redis message for handshake termination", e);
|
||||
}
|
||||
}
|
||||
|
||||
dataManager.closeDatabases();
|
||||
|
||||
// Log to console
|
||||
getBungeeLogger().info("Disabled HuskSync (" + getProxy().getName() + ") v" + getDescription().getVersion());
|
||||
}
|
||||
|
||||
// Load dependencies
|
||||
private void fetchDependencies() {
|
||||
BungeeLibraryManager manager = new BungeeLibraryManager(getInstance());
|
||||
|
||||
Library mySqlLib = Library.builder()
|
||||
.groupId("mysql")
|
||||
.artifactId("mysql-connector-java")
|
||||
.version("8.0.29")
|
||||
.build();
|
||||
|
||||
Library sqLiteLib = Library.builder()
|
||||
.groupId("org.xerial")
|
||||
.artifactId("sqlite-jdbc")
|
||||
.version("3.36.0.3")
|
||||
.build();
|
||||
|
||||
manager.addMavenCentral();
|
||||
manager.loadLibrary(mySqlLib);
|
||||
manager.loadLibrary(sqLiteLib);
|
||||
}
|
||||
}
|
||||
@@ -1,424 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.command;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Server;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bungeecord.config.ConfigLoader;
|
||||
import net.william278.husksync.bungeecord.config.ConfigManager;
|
||||
import net.william278.husksync.bungeecord.util.BungeeUpdateChecker;
|
||||
import net.william278.husksync.migrator.MPDBMigrator;
|
||||
import net.william278.husksync.proxy.command.HuskSyncCommand;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.william278.husksync.util.MessageManager;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
import net.md_5.bungee.api.plugin.TabExecutor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BungeeCommand extends Command implements TabExecutor, HuskSyncCommand {
|
||||
|
||||
private final static HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
public BungeeCommand() {
|
||||
super("husksync", null, "hs");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
if (sender instanceof ProxiedPlayer player) {
|
||||
if (HuskSyncBungeeCord.synchronisedServers.size() == 0) {
|
||||
player.sendMessage(new MineDown(MessageManager.getMessage("error_no_servers_proxied")).toComponent());
|
||||
return;
|
||||
}
|
||||
if (args.length >= 1) {
|
||||
switch (args[0].toLowerCase(Locale.ROOT)) {
|
||||
case "about", "info" -> sendAboutInformation(player);
|
||||
case "update" -> {
|
||||
if (!player.hasPermission("husksync.command.inventory")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(new MineDown("[Checking for HuskSync updates...](gray)").toComponent());
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
// Check Bukkit servers needing updates
|
||||
int updatesNeeded = 0;
|
||||
String bukkitBrand = "Spigot";
|
||||
String bukkitVersion = "1.0";
|
||||
for (Server server : HuskSyncBungeeCord.synchronisedServers) {
|
||||
BungeeUpdateChecker updateChecker = new BungeeUpdateChecker(server.huskSyncVersion());
|
||||
if (!updateChecker.isUpToDate()) {
|
||||
updatesNeeded++;
|
||||
bukkitBrand = server.serverBrand();
|
||||
bukkitVersion = server.huskSyncVersion();
|
||||
}
|
||||
}
|
||||
|
||||
// Check Bungee servers needing updates and send message
|
||||
BungeeUpdateChecker proxyUpdateChecker = new BungeeUpdateChecker(plugin.getDescription().getVersion());
|
||||
if (proxyUpdateChecker.isUpToDate() && updatesNeeded == 0) {
|
||||
sender.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date, running Version " + proxyUpdateChecker.getLatestVersion() + "](#00fb9a)").toComponent());
|
||||
} else {
|
||||
sender.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| Your server(s) are not up-to-date:](#00fb9a)").toComponent());
|
||||
if (!proxyUpdateChecker.isUpToDate()) {
|
||||
sender.sendMessage(new MineDown("[•](white) [HuskSync on the " + ProxyServer.getInstance().getName() + " proxy is outdated (Latest: " + proxyUpdateChecker.getLatestVersion() + ", Running: " + proxyUpdateChecker.getCurrentVersion() + ")](#00fb9a)").toComponent());
|
||||
}
|
||||
if (updatesNeeded > 0) {
|
||||
sender.sendMessage(new MineDown("[•](white) [HuskSync on " + updatesNeeded + " connected " + bukkitBrand + " server(s) are outdated (Latest: " + proxyUpdateChecker.getLatestVersion() + ", Running: " + bukkitVersion + ")](#00fb9a)").toComponent());
|
||||
}
|
||||
sender.sendMessage(new MineDown("[•](white) [Download links:](#00fb9a) [[⏩ Spigot]](gray open_url=https://www.spigotmc.org/resources/husktowns.92672/updates) [•](#262626) [[⏩ Polymart]](gray open_url=https://polymart.org/resource/husktowns.1056/updates)").toComponent());
|
||||
}
|
||||
});
|
||||
}
|
||||
case "invsee", "openinv", "inventory" -> {
|
||||
if (!player.hasPermission("husksync.command.inventory")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
String clusterId;
|
||||
if (Settings.clusters.size() > 1) {
|
||||
if (args.length == 3) {
|
||||
clusterId = args[2];
|
||||
} else {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
clusterId = "main";
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
clusterId = cluster.clusterId();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (args.length == 2 || args.length == 3) {
|
||||
String playerName = args[1];
|
||||
openInventory(player, playerName, clusterId);
|
||||
} else {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax").replaceAll("%1%",
|
||||
"/husksync invsee <player>")).toComponent());
|
||||
}
|
||||
}
|
||||
case "echest", "enderchest" -> {
|
||||
if (!player.hasPermission("husksync.command.ender_chest")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
String clusterId;
|
||||
if (Settings.clusters.size() > 1) {
|
||||
if (args.length == 3) {
|
||||
clusterId = args[2];
|
||||
} else {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
clusterId = "main";
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
clusterId = cluster.clusterId();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (args.length == 2 || args.length == 3) {
|
||||
String playerName = args[1];
|
||||
openEnderChest(player, playerName, clusterId);
|
||||
} else {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax")
|
||||
.replaceAll("%1%", "/husksync echest <player>")).toComponent());
|
||||
}
|
||||
}
|
||||
case "migrate" -> {
|
||||
if (!player.hasPermission("husksync.command.admin")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_console_command_only")
|
||||
.replaceAll("%1%", ProxyServer.getInstance().getName())).toComponent());
|
||||
}
|
||||
case "status" -> {
|
||||
if (!player.hasPermission("husksync.command.admin")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
int playerDataSize = 0;
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
playerDataSize += HuskSyncBungeeCord.dataManager.playerDataCache.get(cluster).playerData.size();
|
||||
}
|
||||
sender.sendMessage(new MineDown(MessageManager.PLUGIN_STATUS.toString()
|
||||
.replaceAll("%1%", String.valueOf(HuskSyncBungeeCord.synchronisedServers.size()))
|
||||
.replaceAll("%2%", String.valueOf(playerDataSize))).toComponent());
|
||||
}
|
||||
case "reload" -> {
|
||||
if (!player.hasPermission("husksync.command.admin")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
ConfigManager.loadConfig();
|
||||
ConfigLoader.loadSettings(Objects.requireNonNull(ConfigManager.getConfig()));
|
||||
|
||||
ConfigManager.loadMessages();
|
||||
ConfigLoader.loadMessageStrings(Objects.requireNonNull(ConfigManager.getMessages()));
|
||||
|
||||
// Send reload request to all bukkit servers
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.RELOAD_CONFIG,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null),
|
||||
"reload")
|
||||
.send();
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.WARNING, "Failed to serialize reload notification message data");
|
||||
}
|
||||
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("reload_complete")).toComponent());
|
||||
}
|
||||
default -> sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax").replaceAll("%1%",
|
||||
"/husksync <about/status/invsee/echest>")).toComponent());
|
||||
}
|
||||
} else {
|
||||
sendAboutInformation(player);
|
||||
}
|
||||
} else {
|
||||
// Database migration wizard
|
||||
if (args.length >= 1) {
|
||||
if (args[0].equalsIgnoreCase("migrate")) {
|
||||
MPDBMigrator migrator = HuskSyncBungeeCord.mpdbMigrator;
|
||||
if (args.length == 1) {
|
||||
sender.sendMessage(new MineDown(
|
||||
"""
|
||||
=== MySQLPlayerDataBridge Migration Wizard ==========
|
||||
This will migrate data from the MySQLPlayerDataBridge
|
||||
plugin to HuskSync.
|
||||
|
||||
Data that will be migrated:
|
||||
- Inventories
|
||||
- Ender Chests
|
||||
- Experience points
|
||||
|
||||
Other non-vital data, such as current health, hunger
|
||||
& potion effects will not be migrated to ensure that
|
||||
migration does not take an excessive amount of time.
|
||||
|
||||
To do this, you need to have MySqlPlayerDataBridge
|
||||
and HuskSync installed on one Spigot server as well
|
||||
as HuskSync installed on the proxy (which you have)
|
||||
|
||||
>To proceed, type: husksync migrate setup""").toComponent());
|
||||
} else {
|
||||
switch (args[1].toLowerCase()) {
|
||||
case "setup" -> sender.sendMessage(new MineDown(
|
||||
"""
|
||||
=== MySQLPlayerDataBridge Migration Wizard ==========
|
||||
The following database settings will be used.
|
||||
Please make sure they match the correct settings to
|
||||
access your MySQLPlayerDataBridge Data
|
||||
|
||||
sourceHost: %1%
|
||||
sourcePort: %2%
|
||||
sourceDatabase: %3%
|
||||
sourceUsername: %4%
|
||||
sourcePassword: %5%
|
||||
|
||||
sourceInventoryTableName: %6%
|
||||
sourceEnderChestTableName: %7%
|
||||
sourceExperienceTableName: %8%
|
||||
|
||||
targetCluster: %9%
|
||||
|
||||
To change a setting, type:
|
||||
husksync migrate setting <settingName> <value>
|
||||
|
||||
Please ensure no players are logged in to the network
|
||||
and that at least one Spigot server is online with
|
||||
both HuskSync AND MySqlPlayerDataBridge installed AND
|
||||
that the server has been configured with the correct
|
||||
Redis credentials.
|
||||
|
||||
Warning: Data will be saved to your configured data
|
||||
source, which is currently a %10% database.
|
||||
Please make sure you are happy with this, or stop
|
||||
the proxy server and edit this in config.yml
|
||||
|
||||
Warning: Migration will overwrite any current data
|
||||
saved by HuskSync. It will not, however, delete any
|
||||
data from the source MySQLPlayerDataBridge database.
|
||||
|
||||
>When done, type: husksync migrate start"""
|
||||
.replaceAll("%1%", migrator.migrationSettings.sourceHost)
|
||||
.replaceAll("%2%", String.valueOf(migrator.migrationSettings.sourcePort))
|
||||
.replaceAll("%3%", migrator.migrationSettings.sourceDatabase)
|
||||
.replaceAll("%4%", migrator.migrationSettings.sourceUsername)
|
||||
.replaceAll("%5%", migrator.migrationSettings.sourcePassword)
|
||||
.replaceAll("%6%", migrator.migrationSettings.inventoryDataTable)
|
||||
.replaceAll("%7%", migrator.migrationSettings.enderChestDataTable)
|
||||
.replaceAll("%8%", migrator.migrationSettings.expDataTable)
|
||||
.replaceAll("%9%", migrator.migrationSettings.targetCluster)
|
||||
.replaceAll("%10%", Settings.dataStorageType.toString())
|
||||
).toComponent());
|
||||
case "setting" -> {
|
||||
if (args.length == 4) {
|
||||
String value = args[3];
|
||||
switch (args[2]) {
|
||||
case "sourceHost", "host" -> migrator.migrationSettings.sourceHost = value;
|
||||
case "sourcePort", "port" -> {
|
||||
try {
|
||||
migrator.migrationSettings.sourcePort = Integer.parseInt(value);
|
||||
} catch (NumberFormatException e) {
|
||||
sender.sendMessage(new MineDown("Error: Invalid value; port must be a number").toComponent());
|
||||
return;
|
||||
}
|
||||
}
|
||||
case "sourceDatabase", "database" -> migrator.migrationSettings.sourceDatabase = value;
|
||||
case "sourceUsername", "username" -> migrator.migrationSettings.sourceUsername = value;
|
||||
case "sourcePassword", "password" -> migrator.migrationSettings.sourcePassword = value;
|
||||
case "sourceInventoryTableName", "inventoryTableName", "inventoryTable" -> migrator.migrationSettings.inventoryDataTable = value;
|
||||
case "sourceEnderChestTableName", "enderChestTableName", "enderChestTable" -> migrator.migrationSettings.enderChestDataTable = value;
|
||||
case "sourceExperienceTableName", "experienceTableName", "experienceTable" -> migrator.migrationSettings.expDataTable = value;
|
||||
case "targetCluster", "cluster" -> migrator.migrationSettings.targetCluster = value;
|
||||
default -> {
|
||||
sender.sendMessage(new MineDown("Error: Invalid setting; please use \"husksync migrate setup\" to view a list").toComponent());
|
||||
return;
|
||||
}
|
||||
}
|
||||
sender.sendMessage(new MineDown("Successfully updated setting: \"" + args[2] + "\" --> \"" + value + "\"").toComponent());
|
||||
} else {
|
||||
sender.sendMessage(new MineDown("Error: Invalid usage. Syntax: husksync migrate setting <settingName> <value>").toComponent());
|
||||
}
|
||||
}
|
||||
case "start" -> {
|
||||
sender.sendMessage(new MineDown("Starting MySQLPlayerDataBridge migration!...").toComponent());
|
||||
|
||||
// If the migrator is ready, execute the migration asynchronously
|
||||
if (HuskSyncBungeeCord.mpdbMigrator.readyToMigrate(ProxyServer.getInstance().getOnlineCount(),
|
||||
HuskSyncBungeeCord.synchronisedServers)) {
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () ->
|
||||
HuskSyncBungeeCord.mpdbMigrator.executeMigrationOperations(HuskSyncBungeeCord.dataManager,
|
||||
HuskSyncBungeeCord.synchronisedServers, HuskSyncBungeeCord.redisListener));
|
||||
}
|
||||
}
|
||||
default -> sender.sendMessage(new MineDown("Error: Invalid argument for migration. Use \"husksync migrate\" to start the process").toComponent());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
sender.sendMessage(new MineDown("Error: Invalid syntax. Usage: husksync migrate <args>").toComponent());
|
||||
}
|
||||
}
|
||||
|
||||
// View the inventory of a player specified by their name
|
||||
private void openInventory(ProxiedPlayer viewer, String targetPlayerName, String clusterId) {
|
||||
if (viewer.getName().equalsIgnoreCase(targetPlayerName)) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_inventory")).toComponent());
|
||||
return;
|
||||
}
|
||||
if (ProxyServer.getInstance().getPlayer(targetPlayerName) != null) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_inventory_online")).toComponent());
|
||||
return;
|
||||
}
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (!cluster.clusterId().equals(clusterId)) continue;
|
||||
PlayerData playerData = HuskSyncBungeeCord.dataManager.getPlayerDataByName(targetPlayerName, cluster.clusterId());
|
||||
if (playerData == null) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.OPEN_INVENTORY,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId(), null),
|
||||
targetPlayerName, RedisMessage.serialize(playerData))
|
||||
.send();
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_inventory_of").replaceAll("%1%",
|
||||
targetPlayerName)).toComponent());
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.WARNING, "Failed to serialize inventory-see player data", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
|
||||
});
|
||||
}
|
||||
|
||||
// View the ender chest of a player specified by their name
|
||||
public void openEnderChest(ProxiedPlayer viewer, String targetPlayerName, String clusterId) {
|
||||
if (viewer.getName().equalsIgnoreCase(targetPlayerName)) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_ender_chest")).toComponent());
|
||||
return;
|
||||
}
|
||||
if (ProxyServer.getInstance().getPlayer(targetPlayerName) != null) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_ender_chest_online")).toComponent());
|
||||
return;
|
||||
}
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (!cluster.clusterId().equals(clusterId)) continue;
|
||||
PlayerData playerData = HuskSyncBungeeCord.dataManager.getPlayerDataByName(targetPlayerName, cluster.clusterId());
|
||||
if (playerData == null) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.OPEN_ENDER_CHEST,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId(), null),
|
||||
targetPlayerName, RedisMessage.serialize(playerData))
|
||||
.send();
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_ender_chest_of").replaceAll("%1%",
|
||||
targetPlayerName)).toComponent());
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.WARNING, "Failed to serialize inventory-see player data", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send information about the plugin
|
||||
*
|
||||
* @param player The player to send it to
|
||||
*/
|
||||
private void sendAboutInformation(ProxiedPlayer player) {
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.SEND_PLUGIN_INFORMATION,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, player.getUniqueId(), null),
|
||||
plugin.getProxy().getName(), plugin.getDescription().getVersion()).send();
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.WARNING, "Failed to serialize plugin information to send", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Tab completion
|
||||
@Override
|
||||
public Iterable<String> onTabComplete(CommandSender sender, String[] args) {
|
||||
if (sender instanceof ProxiedPlayer player) {
|
||||
if (args.length == 1) {
|
||||
final ArrayList<String> subCommands = new ArrayList<>();
|
||||
for (SubCommand subCommand : SUB_COMMANDS) {
|
||||
if (subCommand.permission() != null) {
|
||||
if (!player.hasPermission(subCommand.permission())) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
subCommands.add(subCommand.command());
|
||||
}
|
||||
// Automatically filter the sub commands' order in tab completion by what the player has typed
|
||||
return subCommands.stream().filter(val -> val.startsWith(args[0]))
|
||||
.sorted().collect(Collectors.toList());
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.config;
|
||||
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.util.MessageManager;
|
||||
import net.md_5.bungee.config.Configuration;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class ConfigLoader {
|
||||
|
||||
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
private static Configuration copyDefaults(Configuration config) {
|
||||
// Get the config version and update if needed
|
||||
String configVersion = config.getString("config_file_version", "1.0");
|
||||
if (configVersion.contains("-dev")) {
|
||||
configVersion = configVersion.replaceAll("-dev", "");
|
||||
}
|
||||
if (!configVersion.equals(plugin.getDescription().getVersion())) {
|
||||
if (configVersion.equalsIgnoreCase("1.0")) {
|
||||
config.set("check_for_updates", true);
|
||||
}
|
||||
if (configVersion.equalsIgnoreCase("1.0") || configVersion.equalsIgnoreCase("1.0.1") || configVersion.equalsIgnoreCase("1.0.2") || configVersion.equalsIgnoreCase("1.0.3")) {
|
||||
config.set("clusters.main.player_table", "husksync_players");
|
||||
config.set("clusters.main.data_table", "husksync_data");
|
||||
}
|
||||
config.set("config_file_version", plugin.getDescription().getVersion());
|
||||
}
|
||||
// Save the config back
|
||||
ConfigManager.saveConfig(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
public static void loadSettings(Configuration loadedConfig) throws IllegalArgumentException {
|
||||
Configuration config = copyDefaults(loadedConfig);
|
||||
|
||||
Settings.language = config.getString("language", "en-gb");
|
||||
|
||||
Settings.serverType = Settings.ServerType.PROXY;
|
||||
Settings.automaticUpdateChecks = config.getBoolean("check_for_updates", true);
|
||||
Settings.redisHost = config.getString("redis_settings.host", "localhost");
|
||||
Settings.redisPort = config.getInt("redis_settings.port", 6379);
|
||||
Settings.redisPassword = config.getString("redis_settings.password", "");
|
||||
Settings.redisSSL = config.getBoolean("redis_settings.use_ssl", false);
|
||||
|
||||
Settings.dataStorageType = Settings.DataStorageType.valueOf(config.getString("data_storage_settings.database_type", "sqlite").toUpperCase());
|
||||
if (Settings.dataStorageType == Settings.DataStorageType.MYSQL) {
|
||||
Settings.mySQLHost = config.getString("data_storage_settings.mysql_settings.host", "localhost");
|
||||
Settings.mySQLPort = config.getInt("data_storage_settings.mysql_settings.port", 3306);
|
||||
Settings.mySQLDatabase = config.getString("data_storage_settings.mysql_settings.database", "HuskSync");
|
||||
Settings.mySQLUsername = config.getString("data_storage_settings.mysql_settings.username", "root");
|
||||
Settings.mySQLPassword = config.getString("data_storage_settings.mysql_settings.password", "pa55w0rd");
|
||||
Settings.mySQLParams = config.getString("data_storage_settings.mysql_settings.params", "?autoReconnect=true&useSSL=false");
|
||||
}
|
||||
|
||||
Settings.hikariMaximumPoolSize = config.getInt("data_storage_settings.hikari_pool_settings.maximum_pool_size", 10);
|
||||
Settings.hikariMinimumIdle = config.getInt("data_storage_settings.hikari_pool_settings.minimum_idle", 10);
|
||||
Settings.hikariMaximumLifetime = config.getLong("data_storage_settings.hikari_pool_settings.maximum_lifetime", 1800000);
|
||||
Settings.hikariKeepAliveTime = config.getLong("data_storage_settings.hikari_pool_settings.keepalive_time", 0);
|
||||
Settings.hikariConnectionTimeOut = config.getLong("data_storage_settings.hikari_pool_settings.connection_timeout", 5000);
|
||||
|
||||
Settings.bounceBackSynchronisation = config.getBoolean("bounce_back_synchronization", true);
|
||||
|
||||
// Read cluster data
|
||||
Configuration section = config.getSection("clusters");
|
||||
final String settingDatabaseName = Settings.mySQLDatabase != null ? Settings.mySQLDatabase : "HuskSync";
|
||||
for (String clusterId : section.getKeys()) {
|
||||
final String playerTableName = config.getString("clusters." + clusterId + ".player_table", "husksync_players");
|
||||
final String dataTableName = config.getString("clusters." + clusterId + ".data_table", "husksync_data");
|
||||
final String databaseName = config.getString("clusters." + clusterId + ".database", settingDatabaseName);
|
||||
Settings.clusters.add(new Settings.SynchronisationCluster(clusterId, databaseName, playerTableName, dataTableName));
|
||||
}
|
||||
}
|
||||
|
||||
public static void loadMessageStrings(Configuration config) {
|
||||
final HashMap<String,String> messages = new HashMap<>();
|
||||
for (String messageId : config.getKeys()) {
|
||||
messages.put(messageId, config.getString(messageId));
|
||||
}
|
||||
MessageManager.setMessages(messages);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.config;
|
||||
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.md_5.bungee.config.Configuration;
|
||||
import net.md_5.bungee.config.ConfigurationProvider;
|
||||
import net.md_5.bungee.config.YamlConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class ConfigManager {
|
||||
|
||||
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
public static void loadConfig() {
|
||||
try {
|
||||
if (!plugin.getDataFolder().exists()) {
|
||||
if (plugin.getDataFolder().mkdir()) {
|
||||
plugin.getBungeeLogger().info("Created HuskSync data folder");
|
||||
}
|
||||
}
|
||||
File configFile = new File(plugin.getDataFolder(), "config.yml");
|
||||
if (!configFile.exists()) {
|
||||
Files.copy(plugin.getResourceAsStream("proxy-config.yml"), configFile.toPath());
|
||||
plugin.getBungeeLogger().info("Created HuskSync config file");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getBungeeLogger().log(Level.CONFIG, "An exception occurred loading the configuration file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveConfig(Configuration config) {
|
||||
try {
|
||||
ConfigurationProvider.getProvider(YamlConfiguration.class).save(config, new File(plugin.getDataFolder(), "config.yml"));
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.CONFIG, "An exception occurred loading the configuration file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void loadMessages() {
|
||||
try {
|
||||
if (!plugin.getDataFolder().exists()) {
|
||||
if (plugin.getDataFolder().mkdir()) {
|
||||
plugin.getBungeeLogger().info("Created HuskSync data folder");
|
||||
}
|
||||
}
|
||||
File messagesFile = new File(plugin.getDataFolder(), "messages_" + Settings.language + ".yml");
|
||||
if (!messagesFile.exists()) {
|
||||
Files.copy(plugin.getResourceAsStream("languages/" + Settings.language + ".yml"), messagesFile.toPath());
|
||||
plugin.getBungeeLogger().info("Created HuskSync messages file");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getBungeeLogger().log(Level.CONFIG, "An exception occurred loading the messages file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Configuration getConfig() {
|
||||
try {
|
||||
File configFile = new File(plugin.getDataFolder(), "config.yml");
|
||||
return ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile);
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.CONFIG, "An IOException occurred fetching the configuration file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Configuration getMessages() {
|
||||
try {
|
||||
File configFile = new File(plugin.getDataFolder(), "messages_" + Settings.language + ".yml");
|
||||
return ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile);
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.CONFIG, "An IOException occurred fetching the messages file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.listener;
|
||||
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BungeeEventListener implements Listener {
|
||||
|
||||
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPostLogin(PostLoginEvent event) {
|
||||
final ProxiedPlayer player = event.getPlayer();
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
// Ensure the player has data on SQL and that it is up-to-date
|
||||
HuskSyncBungeeCord.dataManager.ensurePlayerExists(player.getUniqueId(), player.getName());
|
||||
|
||||
// Get the player's data from SQL
|
||||
final Map<Settings.SynchronisationCluster, PlayerData> data = HuskSyncBungeeCord.dataManager.getPlayerData(player.getUniqueId());
|
||||
|
||||
// Update the player's data from SQL onto the cache
|
||||
assert data != null;
|
||||
for (Settings.SynchronisationCluster cluster : data.keySet()) {
|
||||
HuskSyncBungeeCord.dataManager.playerDataCache.get(cluster).updatePlayer(data.get(cluster));
|
||||
}
|
||||
|
||||
// Send a message asking the bukkit to request data on join
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.REQUEST_DATA_ON_JOIN,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null),
|
||||
RedisMessage.RequestOnJoinUpdateType.ADD_REQUESTER.toString(), player.getUniqueId().toString()).send();
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.SEVERE, "Failed to serialize request data on join message data");
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.listener;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.Server;
|
||||
import net.william278.husksync.util.MessageManager;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.migrator.MPDBMigrator;
|
||||
import net.william278.husksync.redis.RedisListener;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.md_5.bungee.api.ChatMessageType;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BungeeRedisListener extends RedisListener {
|
||||
|
||||
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
// Initialize the listener on the bungee
|
||||
public BungeeRedisListener() {
|
||||
super();
|
||||
listen();
|
||||
}
|
||||
|
||||
private PlayerData getPlayerCachedData(UUID uuid, String clusterId) {
|
||||
PlayerData data = null;
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (cluster.clusterId().equals(clusterId)) {
|
||||
// Get the player data from the cache
|
||||
PlayerData cachedData = HuskSyncBungeeCord.dataManager.playerDataCache.get(cluster).getPlayer(uuid);
|
||||
if (cachedData != null) {
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
data = Objects.requireNonNull(HuskSyncBungeeCord.dataManager.getPlayerData(uuid)).get(cluster); // Get their player data from MySQL
|
||||
HuskSyncBungeeCord.dataManager.playerDataCache.get(cluster).updatePlayer(data); // Update the cache
|
||||
break;
|
||||
}
|
||||
}
|
||||
return data; // Return the data
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming {@link RedisMessage}
|
||||
*
|
||||
* @param message The {@link RedisMessage} to handle
|
||||
*/
|
||||
@Override
|
||||
public void handleMessage(RedisMessage message) {
|
||||
// Ignore messages destined for Bukkit servers
|
||||
if (message.getMessageTarget().targetServerType() != Settings.ServerType.PROXY) {
|
||||
return;
|
||||
}
|
||||
// Only process redis messages when ready
|
||||
if (!HuskSyncBungeeCord.readyForRedis) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (message.getMessageType()) {
|
||||
case PLAYER_DATA_REQUEST -> ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
// Get the UUID of the requesting player
|
||||
final UUID requestingPlayerUUID = UUID.fromString(message.getMessageData());
|
||||
try {
|
||||
// Send the reply, serializing the message data
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, requestingPlayerUUID, message.getMessageTarget().targetClusterId()),
|
||||
RedisMessage.serialize(getPlayerCachedData(requestingPlayerUUID, message.getMessageTarget().targetClusterId())))
|
||||
.send();
|
||||
|
||||
// Send an update to all bukkit servers removing the player from the requester cache
|
||||
new RedisMessage(RedisMessage.MessageType.REQUEST_DATA_ON_JOIN,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
|
||||
RedisMessage.RequestOnJoinUpdateType.REMOVE_REQUESTER.toString(), requestingPlayerUUID.toString())
|
||||
.send();
|
||||
|
||||
// Send synchronisation complete message
|
||||
ProxiedPlayer player = ProxyServer.getInstance().getPlayer(requestingPlayerUUID);
|
||||
if (player != null) {
|
||||
player.sendMessage(ChatMessageType.ACTION_BAR, new MineDown(MessageManager.getMessage("synchronisation_complete")).toComponent());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log(Level.SEVERE, "Failed to serialize data when replying to a data request");
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
case PLAYER_DATA_UPDATE -> ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
// Deserialize the PlayerData received
|
||||
PlayerData playerData;
|
||||
final String serializedPlayerData = message.getMessageDataElements()[0];
|
||||
final boolean bounceBack = Boolean.parseBoolean(message.getMessageDataElements()[1]);
|
||||
try {
|
||||
playerData = (PlayerData) RedisMessage.deserialize(serializedPlayerData);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to deserialize PlayerData when handling a player update request");
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the data in the cache and SQL
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (cluster.clusterId().equals(message.getMessageTarget().targetClusterId())) {
|
||||
HuskSyncBungeeCord.dataManager.updatePlayerData(playerData, cluster);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Reply with the player data if they are still online (switching server)
|
||||
if (Settings.bounceBackSynchronisation && bounceBack) {
|
||||
try {
|
||||
ProxiedPlayer player = ProxyServer.getInstance().getPlayer(playerData.getPlayerUUID());
|
||||
if (player != null) {
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, playerData.getPlayerUUID(), message.getMessageTarget().targetClusterId()),
|
||||
serializedPlayerData)
|
||||
.send();
|
||||
|
||||
// Send synchronisation complete message
|
||||
player.sendMessage(ChatMessageType.ACTION_BAR, new MineDown(MessageManager.getMessage("synchronisation_complete")).toComponent());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log(Level.SEVERE, "Failed to re-serialize PlayerData when handling a player update request");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
case CONNECTION_HANDSHAKE -> {
|
||||
// Reply to a Bukkit server's connection handshake to complete the process
|
||||
if (HuskSyncBungeeCord.isDisabling) return; // Return if the Proxy is disabling
|
||||
final UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
final boolean hasMySqlPlayerDataBridge = Boolean.parseBoolean(message.getMessageDataElements()[1]);
|
||||
final String bukkitBrand = message.getMessageDataElements()[2];
|
||||
final String huskSyncVersion = message.getMessageDataElements()[3];
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.CONNECTION_HANDSHAKE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
|
||||
serverUUID.toString(), plugin.getProxy().getName())
|
||||
.send();
|
||||
HuskSyncBungeeCord.synchronisedServers.add(
|
||||
new Server(serverUUID, hasMySqlPlayerDataBridge,
|
||||
huskSyncVersion, bukkitBrand, message.getMessageTarget().targetClusterId()));
|
||||
log(Level.INFO, "Completed handshake with " + bukkitBrand + " server (" + serverUUID + ")");
|
||||
} catch (IOException e) {
|
||||
log(Level.SEVERE, "Failed to serialize handshake message data");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
case TERMINATE_HANDSHAKE -> {
|
||||
// Terminate the handshake with a Bukkit server
|
||||
final UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
final String bukkitBrand = message.getMessageDataElements()[1];
|
||||
|
||||
// Remove a server from the synchronised server list
|
||||
Server serverToRemove = null;
|
||||
for (Server server : HuskSyncBungeeCord.synchronisedServers) {
|
||||
if (server.serverUUID().equals(serverUUID)) {
|
||||
serverToRemove = server;
|
||||
break;
|
||||
}
|
||||
}
|
||||
HuskSyncBungeeCord.synchronisedServers.remove(serverToRemove);
|
||||
log(Level.INFO, "Terminated the handshake with " + bukkitBrand + " server (" + serverUUID + ")");
|
||||
}
|
||||
case DECODED_MPDB_DATA_SET -> {
|
||||
// Deserialize the PlayerData received
|
||||
PlayerData playerData;
|
||||
final String serializedPlayerData = message.getMessageDataElements()[0];
|
||||
final String playerName = message.getMessageDataElements()[1];
|
||||
try {
|
||||
playerData = (PlayerData) RedisMessage.deserialize(serializedPlayerData);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to deserialize PlayerData when handling incoming decoded MPDB data");
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the migrator
|
||||
MPDBMigrator migrator = HuskSyncBungeeCord.mpdbMigrator;
|
||||
|
||||
// Add the incoming data to the data to be saved
|
||||
migrator.incomingPlayerData.put(playerData, playerName);
|
||||
|
||||
// Increment players migrated
|
||||
migrator.playersMigrated++;
|
||||
plugin.getBungeeLogger().log(Level.INFO, "Migrated " + migrator.playersMigrated + "/" + migrator.migratedDataSent + " players.");
|
||||
|
||||
// When all the data has been received, save it
|
||||
if (migrator.migratedDataSent == migrator.playersMigrated) {
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> migrator.loadIncomingData(migrator.incomingPlayerData,
|
||||
HuskSyncBungeeCord.dataManager));
|
||||
}
|
||||
}
|
||||
case API_DATA_REQUEST -> ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
final UUID playerUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[1]);
|
||||
try {
|
||||
final PlayerData data = getPlayerCachedData(playerUUID, message.getMessageTarget().targetClusterId());
|
||||
|
||||
if (data == null) {
|
||||
new RedisMessage(RedisMessage.MessageType.API_DATA_CANCEL,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
|
||||
requestUUID.toString())
|
||||
.send();
|
||||
} else {
|
||||
// Send the reply alongside the request UUID, serializing the requested message data
|
||||
new RedisMessage(RedisMessage.MessageType.API_DATA_RETURN,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
|
||||
requestUUID.toString(),
|
||||
RedisMessage.serialize(data))
|
||||
.send();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.SEVERE, "Failed to serialize PlayerData requested via the API");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log to console
|
||||
*
|
||||
* @param level The {@link Level} to log
|
||||
* @param message Message to log
|
||||
*/
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
plugin.getBungeeLogger().log(level, message);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.util;
|
||||
|
||||
import net.william278.husksync.util.Logger;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public record BungeeLogger(java.util.logging.Logger parent) implements Logger {
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message, Exception e) {
|
||||
parent.log(level, message, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
parent.log(level, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(String message) {
|
||||
parent.info(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void severe(String message) {
|
||||
parent.severe(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void config(String message) {
|
||||
parent.config(message);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.util;
|
||||
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.util.UpdateChecker;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BungeeUpdateChecker extends UpdateChecker {
|
||||
|
||||
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
public BungeeUpdateChecker(String versionToCheck) {
|
||||
super(versionToCheck);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
plugin.getBungeeLogger().log(level, message);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
name: HuskSync
|
||||
version: ${version}
|
||||
main: net.william278.husksync.HuskSyncBungeeCord
|
||||
author: William278
|
||||
description: 'A modern, cross-server player data synchronization system'
|
||||
@@ -1,11 +1,46 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'java-library'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly 'com.zaxxer:HikariCP:5.0.1'
|
||||
}
|
||||
api 'commons-io:commons-io:2.21.0'
|
||||
api 'org.apache.commons:commons-text:1.14.0'
|
||||
api 'net.william278:minedown:1.8.2'
|
||||
api 'net.william278:mapdataapi:2.0'
|
||||
api 'org.json:json:20250517'
|
||||
api 'com.google.code.gson:gson:2.13.2'
|
||||
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
|
||||
api 'de.exlll:configlib-yaml:4.6.4'
|
||||
api 'net.william278:paginedown:1.1.2'
|
||||
api 'net.william278:DesertWell:2.0.4'
|
||||
api('com.zaxxer:HikariCP:7.0.2') {
|
||||
exclude module: 'slf4j-api'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
compileOnlyApi 'net.william278.toilet:toilet-common:1.0.16'
|
||||
|
||||
compileOnly 'net.william278.uniform:uniform-common:1.3.9'
|
||||
compileOnly 'com.mojang:brigadier:1.1.8'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.42'
|
||||
compileOnly 'org.jetbrains:annotations:26.0.2-1'
|
||||
compileOnly 'net.kyori:adventure-api:4.25.0'
|
||||
compileOnly 'net.kyori:adventure-platform-api:4.4.0'
|
||||
compileOnly "net.kyori:adventure-text-serializer-plain:4.25.0"
|
||||
compileOnly 'com.google.guava:guava:33.5.0-jre'
|
||||
compileOnly 'com.github.plan-player-analytics:Plan:5.6.2965'
|
||||
compileOnly "redis.clients:jedis:$jedis_version"
|
||||
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
|
||||
compileOnly "org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version"
|
||||
compileOnly "org.postgresql:postgresql:$postgres_driver_version"
|
||||
compileOnly "org.mongodb:mongodb-driver-sync:$mongodb_driver_version"
|
||||
compileOnly "org.xerial.snappy:snappy-java:$snappy_version"
|
||||
|
||||
testImplementation "redis.clients:jedis:$jedis_version"
|
||||
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
|
||||
testImplementation 'com.google.guava:guava:33.5.0-jre'
|
||||
testImplementation 'com.github.plan-player-analytics:Plan:5.6.2965'
|
||||
testCompileOnly 'de.exlll:configlib-yaml:4.6.4'
|
||||
testCompileOnly 'org.jetbrains:annotations:26.0.2-1'
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.42'
|
||||
}
|
||||
362
common/src/main/java/net/william278/husksync/HuskSync.java
Normal file
362
common/src/main/java/net/william278/husksync/HuskSync.java
Normal file
@@ -0,0 +1,362 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync;
|
||||
|
||||
import com.fatboyindustrial.gsonjavatime.Converters;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.platform.AudienceProvider;
|
||||
import net.william278.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.desertwell.util.UpdateChecker;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.config.ConfigProvider;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.SerializerRegistry;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.event.EventDispatcher;
|
||||
import net.william278.husksync.listener.LockedHandler;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.sync.DataSyncer;
|
||||
import net.william278.husksync.user.ConsoleUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.util.*;
|
||||
import net.william278.uniform.Uniform;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Abstract implementation of the HuskSync plugin.
|
||||
*/
|
||||
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider, SerializerRegistry,
|
||||
CompatibilityChecker, DumpProvider, DataVersionSupplier {
|
||||
|
||||
int SPIGOT_RESOURCE_ID = 97144;
|
||||
|
||||
/**
|
||||
* Returns a set of online players.
|
||||
*
|
||||
* @return a set of online players as {@link OnlineUser}
|
||||
*/
|
||||
@NotNull
|
||||
Set<OnlineUser> getOnlineUsers();
|
||||
|
||||
/**
|
||||
* Returns an online user by UUID if they exist
|
||||
*
|
||||
* @param uuid the UUID of the user to get
|
||||
* @return an online user as {@link OnlineUser}
|
||||
*/
|
||||
@NotNull
|
||||
Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid);
|
||||
|
||||
/**
|
||||
* Returns the database implementation
|
||||
*
|
||||
* @return the {@link Database} implementation
|
||||
*/
|
||||
@NotNull
|
||||
Database getDatabase();
|
||||
|
||||
/**
|
||||
* Returns the redis manager implementation
|
||||
*
|
||||
* @return the {@link RedisManager} implementation
|
||||
*/
|
||||
@NotNull
|
||||
RedisManager getRedisManager();
|
||||
|
||||
/**
|
||||
* Returns the implementing adapter for serializing data
|
||||
*
|
||||
* @return the {@link DataAdapter}
|
||||
*/
|
||||
@NotNull
|
||||
DataAdapter getDataAdapter();
|
||||
|
||||
/**
|
||||
* Returns the data syncer implementation
|
||||
*
|
||||
* @return the {@link DataSyncer} implementation
|
||||
*/
|
||||
@NotNull
|
||||
DataSyncer getDataSyncer();
|
||||
|
||||
/**
|
||||
* Set the data syncer implementation
|
||||
*
|
||||
* @param dataSyncer the {@link DataSyncer} implementation
|
||||
*/
|
||||
void setDataSyncer(@NotNull DataSyncer dataSyncer);
|
||||
|
||||
/**
|
||||
* Get the uniform command provider
|
||||
*
|
||||
* @return the command provider
|
||||
*/
|
||||
@NotNull
|
||||
Uniform getUniform();
|
||||
|
||||
/**
|
||||
* Returns a list of available data {@link Migrator}s
|
||||
*
|
||||
* @return a list of {@link Migrator}s
|
||||
*/
|
||||
@NotNull
|
||||
List<Migrator> getAvailableMigrators();
|
||||
|
||||
@NotNull
|
||||
Map<UUID, Map<Identifier, Data>> getPlayerCustomDataStore();
|
||||
|
||||
@NotNull
|
||||
default Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
|
||||
if (getPlayerCustomDataStore().containsKey(user.getUuid())) {
|
||||
return getPlayerCustomDataStore().get(user.getUuid());
|
||||
}
|
||||
final Map<Identifier, Data> data = Maps.newHashMap();
|
||||
getPlayerCustomDataStore().put(user.getUuid(), data);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a faucet of the plugin.
|
||||
*
|
||||
* @param name the name of the faucet
|
||||
* @param runner a runnable for initializing the faucet
|
||||
*/
|
||||
default void initialize(@NotNull String name, @NotNull ThrowingConsumer<HuskSync> runner) {
|
||||
log(Level.INFO, "Initializing " + name + "...");
|
||||
try {
|
||||
runner.accept(this);
|
||||
} catch (Throwable e) {
|
||||
throw new FailedToLoadException("Failed to initialize " + name, e);
|
||||
}
|
||||
log(Level.INFO, "Successfully initialized " + name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if a dependency is loaded
|
||||
*
|
||||
* @param name the name of the dependency
|
||||
* @return {@code true} if the dependency is loaded, {@code false} otherwise
|
||||
*/
|
||||
boolean isDependencyLoaded(@NotNull String name);
|
||||
|
||||
/**
|
||||
* Get a resource as an {@link InputStream} from the plugin jar
|
||||
*
|
||||
* @param name the path to the resource
|
||||
* @return the {@link InputStream} of the resource
|
||||
*/
|
||||
InputStream getResource(@NotNull String name);
|
||||
|
||||
/**
|
||||
* Log a message to the console
|
||||
*
|
||||
* @param level the level of the message
|
||||
* @param message the message to log
|
||||
* @param throwable a throwable to log
|
||||
*/
|
||||
void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable);
|
||||
|
||||
/**
|
||||
* Send a debug message to the console, if debug logging is enabled
|
||||
*
|
||||
* @param message the message to log
|
||||
* @param throwable a throwable to log
|
||||
*/
|
||||
default void debug(@NotNull String message, @NotNull Throwable... throwable) {
|
||||
if (getSettings().isDebugLogging()) {
|
||||
log(Level.INFO, getDebugString(message), throwable);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the debug log message format
|
||||
@NotNull
|
||||
private String getDebugString(@NotNull String message) {
|
||||
return String.format("[DEBUG] [%s] %s", new SimpleDateFormat("mm:ss.SSS").format(new Date()), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link AudienceProvider} instance
|
||||
*
|
||||
* @return the {@link AudienceProvider} instance
|
||||
* @since 1.0
|
||||
*/
|
||||
@NotNull
|
||||
AudienceProvider getAudiences();
|
||||
|
||||
/**
|
||||
* Get the {@link Audience} instance for the given {@link OnlineUser}
|
||||
*
|
||||
* @param user the {@link OnlineUser} to get the {@link Audience} for
|
||||
* @return the {@link Audience} instance
|
||||
*/
|
||||
@NotNull
|
||||
default Audience getAudience(@NotNull UUID user) {
|
||||
return getAudiences().player(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link ConsoleUser} instance
|
||||
*
|
||||
* @return the {@link ConsoleUser} instance
|
||||
* @since 1.0
|
||||
*/
|
||||
@NotNull
|
||||
default ConsoleUser getConsole() {
|
||||
return new ConsoleUser(getAudiences());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plugin version
|
||||
*
|
||||
* @return the plugin {@link Version}
|
||||
*/
|
||||
@NotNull
|
||||
Version getPluginVersion();
|
||||
|
||||
/**
|
||||
* Returns the Minecraft version implementation
|
||||
*
|
||||
* @return the Minecraft {@link Version}
|
||||
*/
|
||||
@NotNull
|
||||
Version getMinecraftVersion();
|
||||
|
||||
/**
|
||||
* Returns the platform type
|
||||
*
|
||||
* @return the platform type
|
||||
*/
|
||||
@NotNull
|
||||
String getPlatformType();
|
||||
|
||||
/**
|
||||
* Returns the server software version
|
||||
*
|
||||
* @return the server software version string
|
||||
*/
|
||||
@NotNull
|
||||
String getServerVersion();
|
||||
|
||||
/**
|
||||
* Returns the legacy data converter if it exists
|
||||
*
|
||||
* @return the {@link LegacyConverter}
|
||||
*/
|
||||
Optional<LegacyConverter> getLegacyConverter();
|
||||
|
||||
@NotNull
|
||||
default UpdateChecker getUpdateChecker() {
|
||||
return UpdateChecker.builder()
|
||||
.currentVersion(getPluginVersion())
|
||||
.endpoint(UpdateChecker.Endpoint.SPIGOT)
|
||||
.resource(Integer.toString(SPIGOT_RESOURCE_ID))
|
||||
.build();
|
||||
}
|
||||
|
||||
default void checkForUpdates() {
|
||||
if (getSettings().isCheckForUpdates()) {
|
||||
getUpdateChecker().check().thenAccept(checked -> {
|
||||
if (!checked.isUpToDate()) {
|
||||
log(Level.WARNING, String.format(
|
||||
"A new version of HuskSync is available: v%s (running v%s)",
|
||||
checked.getLatestVersion(), getPluginVersion())
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
LockedHandler getLockedHandler();
|
||||
|
||||
/**
|
||||
* Get the set of UUIDs of "locked players", for which events will be canceled.
|
||||
* </p>
|
||||
* Players are locked while their items are being set (on join) or saved (on quit)
|
||||
*/
|
||||
@NotNull
|
||||
Set<UUID> getLockedPlayers();
|
||||
|
||||
/**
|
||||
* Get the set of UUIDs of players who are currently marked as disconnecting or disconnected
|
||||
*/
|
||||
@NotNull
|
||||
Set<UUID> getDisconnectingPlayers();
|
||||
|
||||
default boolean isLocked(@NotNull UUID uuid) {
|
||||
return getLockedPlayers().contains(uuid);
|
||||
}
|
||||
|
||||
default void lockPlayer(@NotNull UUID uuid) {
|
||||
getLockedPlayers().add(uuid);
|
||||
}
|
||||
|
||||
default void unlockPlayer(@NotNull UUID uuid) {
|
||||
getLockedPlayers().remove(uuid);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Gson getGson();
|
||||
|
||||
boolean isDisabling();
|
||||
|
||||
@NotNull
|
||||
default Gson createGson() {
|
||||
return Converters.registerOffsetDateTime(new GsonBuilder()).create();
|
||||
}
|
||||
|
||||
/**
|
||||
* An exception indicating the plugin has been accessed before it has been registered.
|
||||
*/
|
||||
final class FailedToLoadException extends IllegalStateException {
|
||||
|
||||
private static final String FORMAT = """
|
||||
HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized.
|
||||
Please make sure the plugin has been setup correctly (https://william278.net/docs/husksync/setup):
|
||||
|
||||
1) Make sure you've entered your MySQL, MariaDB or MongoDB database details correctly in config.yml
|
||||
2) Make sure your Redis server details are also correct in config.yml
|
||||
3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-file)
|
||||
4) Check the error below for more details
|
||||
|
||||
Caused by: %s""";
|
||||
|
||||
public FailedToLoadException(@NotNull String message) {
|
||||
super(String.format(FORMAT, message));
|
||||
}
|
||||
|
||||
public FailedToLoadException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(String.format(FORMAT, message), cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,533 +0,0 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import java.io.*;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Cross-platform class used to represent a player's data. Data from this can be deserialized using the DataSerializer class on Bukkit platforms.
|
||||
*/
|
||||
public class PlayerData implements Serializable {
|
||||
|
||||
/**
|
||||
* The UUID of the player who this data belongs to
|
||||
*/
|
||||
private final UUID playerUUID;
|
||||
|
||||
/**
|
||||
* The unique version UUID of this data
|
||||
*/
|
||||
private final UUID dataVersionUUID;
|
||||
|
||||
/**
|
||||
* Epoch time identifying when the data was last updated or created
|
||||
*/
|
||||
private long timestamp;
|
||||
|
||||
/**
|
||||
* A special flag that will be {@code true} if the player is new to the network and should not have their data set when joining the Bukkit
|
||||
*/
|
||||
public boolean useDefaultData = false;
|
||||
|
||||
/*
|
||||
* Player data records
|
||||
*/
|
||||
private String serializedInventory;
|
||||
private String serializedEnderChest;
|
||||
private double health;
|
||||
private double maxHealth;
|
||||
private double healthScale;
|
||||
private int hunger;
|
||||
private float saturation;
|
||||
private float saturationExhaustion;
|
||||
private int selectedSlot;
|
||||
private String serializedEffectData;
|
||||
private int totalExperience;
|
||||
private int expLevel;
|
||||
private float expProgress;
|
||||
private String gameMode;
|
||||
private String serializedStatistics;
|
||||
private boolean isFlying;
|
||||
private String serializedAdvancements;
|
||||
private String serializedLocation;
|
||||
|
||||
/**
|
||||
* Constructor to create new PlayerData from a bukkit {@code Player}'s data
|
||||
*
|
||||
* @param playerUUID The Player's UUID
|
||||
* @param serializedInventory Their serialized inventory
|
||||
* @param serializedEnderChest Their serialized ender chest
|
||||
* @param health Their health
|
||||
* @param maxHealth Their max health
|
||||
* @param healthScale Their health scale
|
||||
* @param hunger Their hunger
|
||||
* @param saturation Their saturation
|
||||
* @param saturationExhaustion Their saturation exhaustion
|
||||
* @param selectedSlot Their selected hot bar slot
|
||||
* @param serializedStatusEffects Their serialized status effects
|
||||
* @param totalExperience Their total experience points ("Score")
|
||||
* @param expLevel Their exp level
|
||||
* @param expProgress Their exp progress to the next level
|
||||
* @param gameMode Their game mode ({@code SURVIVAL}, {@code CREATIVE}, etc.)
|
||||
* @param serializedStatistics Their serialized statistics data (Displayed in Statistics menu in ESC menu)
|
||||
*/
|
||||
public PlayerData(UUID playerUUID, String serializedInventory, String serializedEnderChest, double health, double maxHealth,
|
||||
double healthScale, int hunger, float saturation, float saturationExhaustion, int selectedSlot,
|
||||
String serializedStatusEffects, int totalExperience, int expLevel, float expProgress, String gameMode,
|
||||
String serializedStatistics, boolean isFlying, String serializedAdvancements, String serializedLocation) {
|
||||
this.dataVersionUUID = UUID.randomUUID();
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
this.playerUUID = playerUUID;
|
||||
this.serializedInventory = serializedInventory;
|
||||
this.serializedEnderChest = serializedEnderChest;
|
||||
this.health = health;
|
||||
this.maxHealth = maxHealth;
|
||||
this.healthScale = healthScale;
|
||||
this.hunger = hunger;
|
||||
this.saturation = saturation;
|
||||
this.saturationExhaustion = saturationExhaustion;
|
||||
this.selectedSlot = selectedSlot;
|
||||
this.serializedEffectData = serializedStatusEffects;
|
||||
this.totalExperience = totalExperience;
|
||||
this.expLevel = expLevel;
|
||||
this.expProgress = expProgress;
|
||||
this.gameMode = gameMode;
|
||||
this.serializedStatistics = serializedStatistics;
|
||||
this.isFlying = isFlying;
|
||||
this.serializedAdvancements = serializedAdvancements;
|
||||
this.serializedLocation = serializedLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for a PlayerData object from an existing object that was stored in SQL
|
||||
*
|
||||
* @param playerUUID The player whose data this is' UUID
|
||||
* @param dataVersionUUID The PlayerData version UUID
|
||||
* @param serializedInventory Their serialized inventory
|
||||
* @param serializedEnderChest Their serialized ender chest
|
||||
* @param health Their health
|
||||
* @param maxHealth Their max health
|
||||
* @param healthScale Their health scale
|
||||
* @param hunger Their hunger
|
||||
* @param saturation Their saturation
|
||||
* @param saturationExhaustion Their saturation exhaustion
|
||||
* @param selectedSlot Their selected hot bar slot
|
||||
* @param serializedStatusEffects Their serialized status effects
|
||||
* @param totalExperience Their total experience points ("Score")
|
||||
* @param expLevel Their exp level
|
||||
* @param expProgress Their exp progress to the next level
|
||||
* @param gameMode Their game mode ({@code SURVIVAL}, {@code CREATIVE}, etc.)
|
||||
* @param serializedStatistics Their serialized statistics data (Displayed in Statistics menu in ESC menu)
|
||||
*/
|
||||
public PlayerData(UUID playerUUID, UUID dataVersionUUID, long timestamp, String serializedInventory, String serializedEnderChest,
|
||||
double health, double maxHealth, double healthScale, int hunger, float saturation, float saturationExhaustion,
|
||||
int selectedSlot, String serializedStatusEffects, int totalExperience, int expLevel, float expProgress,
|
||||
String gameMode, String serializedStatistics, boolean isFlying, String serializedAdvancements,
|
||||
String serializedLocation) {
|
||||
this.playerUUID = playerUUID;
|
||||
this.dataVersionUUID = dataVersionUUID;
|
||||
this.timestamp = timestamp;
|
||||
this.serializedInventory = serializedInventory;
|
||||
this.serializedEnderChest = serializedEnderChest;
|
||||
this.health = health;
|
||||
this.maxHealth = maxHealth;
|
||||
this.healthScale = healthScale;
|
||||
this.hunger = hunger;
|
||||
this.saturation = saturation;
|
||||
this.saturationExhaustion = saturationExhaustion;
|
||||
this.selectedSlot = selectedSlot;
|
||||
this.serializedEffectData = serializedStatusEffects;
|
||||
this.totalExperience = totalExperience;
|
||||
this.expLevel = expLevel;
|
||||
this.expProgress = expProgress;
|
||||
this.gameMode = gameMode;
|
||||
this.serializedStatistics = serializedStatistics;
|
||||
this.isFlying = isFlying;
|
||||
this.serializedAdvancements = serializedAdvancements;
|
||||
this.serializedLocation = serializedLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default PlayerData for a new user
|
||||
*
|
||||
* @param playerUUID The bukkit Player's UUID
|
||||
* @return Default {@link PlayerData}
|
||||
*/
|
||||
public static PlayerData DEFAULT_PLAYER_DATA(UUID playerUUID) {
|
||||
PlayerData data = new PlayerData(playerUUID, "", "", 20,
|
||||
20, 20, 20, 10, 1, 0,
|
||||
"", 0, 0, 0, "SURVIVAL",
|
||||
"", false, "", "");
|
||||
data.useDefaultData = true;
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link UUID} of the player whose data this is
|
||||
*
|
||||
* @return the player's {@link UUID}
|
||||
*/
|
||||
public UUID getPlayerUUID() {
|
||||
return playerUUID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique version {@link UUID} of the PlayerData
|
||||
*
|
||||
* @return The unique data version
|
||||
*/
|
||||
public UUID getDataVersionUUID() {
|
||||
return dataVersionUUID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timestamp when this data was created or last updated
|
||||
*
|
||||
* @return time since epoch of last data update or creation
|
||||
*/
|
||||
public long getDataTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized player {@code ItemStack[]} inventory
|
||||
*
|
||||
* @return The player's serialized inventory
|
||||
*/
|
||||
public String getSerializedInventory() {
|
||||
return serializedInventory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized player {@code ItemStack[]} ender chest
|
||||
*
|
||||
* @return The player's serialized ender chest
|
||||
*/
|
||||
public String getSerializedEnderChest() {
|
||||
return serializedEnderChest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's health value
|
||||
*
|
||||
* @return the player's health
|
||||
*/
|
||||
public double getHealth() {
|
||||
return health;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's max health value
|
||||
*
|
||||
* @return the player's max health
|
||||
*/
|
||||
public double getMaxHealth() {
|
||||
return maxHealth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's health scale value {@see https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/entity/Player.html#getHealthScale()}
|
||||
*
|
||||
* @return the player's health scaling value
|
||||
*/
|
||||
public double getHealthScale() {
|
||||
return healthScale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's hunger points
|
||||
*
|
||||
* @return the player's hunger level
|
||||
*/
|
||||
public int getHunger() {
|
||||
return hunger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's saturation points
|
||||
*
|
||||
* @return the player's saturation level
|
||||
*/
|
||||
public float getSaturation() {
|
||||
return saturation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's saturation exhaustion value {@see https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/entity/HumanEntity.html#getExhaustion()}
|
||||
*
|
||||
* @return the player's saturation exhaustion
|
||||
*/
|
||||
public float getSaturationExhaustion() {
|
||||
return saturationExhaustion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of the player's currently selected hotbar slot
|
||||
*
|
||||
* @return the player's selected hotbar slot
|
||||
*/
|
||||
public int getSelectedSlot() {
|
||||
return selectedSlot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of the player's current status effects
|
||||
*
|
||||
* @return the player's serialized status effect data
|
||||
*/
|
||||
public String getSerializedEffectData() {
|
||||
return serializedEffectData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's total experience score (used for presenting the death screen score value)
|
||||
*
|
||||
* @return the player's total experience score
|
||||
*/
|
||||
public int getTotalExperience() {
|
||||
return totalExperience;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of the player's statistics
|
||||
*
|
||||
* @return the player's serialized statistic records
|
||||
*/
|
||||
public String getSerializedStatistics() {
|
||||
return serializedStatistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's current experience level
|
||||
*
|
||||
* @return the player's exp level
|
||||
*/
|
||||
public int getExpLevel() {
|
||||
return expLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's progress to the next experience level
|
||||
*
|
||||
* @return the player's exp progress
|
||||
*/
|
||||
public float getExpProgress() {
|
||||
return expProgress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's current game mode as a string ({@code SURVIVAL}, {@code CREATIVE}, etc.)
|
||||
*
|
||||
* @return the player's game mode
|
||||
*/
|
||||
public String getGameMode() {
|
||||
return gameMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the player is currently flying
|
||||
*
|
||||
* @return {@code true} if the player is in flight; {@code false} otherwise
|
||||
*/
|
||||
public boolean isFlying() {
|
||||
return isFlying;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of the player's advancements
|
||||
*
|
||||
* @return the player's serialized advancement data
|
||||
*/
|
||||
public String getSerializedAdvancements() {
|
||||
return serializedAdvancements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of the player's current location
|
||||
*
|
||||
* @return the player's serialized location
|
||||
*/
|
||||
public String getSerializedLocation() {
|
||||
return serializedLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's inventory data
|
||||
*
|
||||
* @param serializedInventory A serialized {@code String}; new inventory data
|
||||
*/
|
||||
public void setSerializedInventory(String serializedInventory) {
|
||||
this.serializedInventory = serializedInventory;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's ender chest data
|
||||
*
|
||||
* @param serializedEnderChest A serialized {@code String}; new ender chest inventory data
|
||||
*/
|
||||
public void setSerializedEnderChest(String serializedEnderChest) {
|
||||
this.serializedEnderChest = serializedEnderChest;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's health
|
||||
*
|
||||
* @param health new health value
|
||||
*/
|
||||
public void setHealth(double health) {
|
||||
this.health = health;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's max health
|
||||
*
|
||||
* @param maxHealth new maximum health value
|
||||
*/
|
||||
public void setMaxHealth(double maxHealth) {
|
||||
this.maxHealth = maxHealth;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's health scale
|
||||
*
|
||||
* @param healthScale new health scaling value
|
||||
*/
|
||||
public void setHealthScale(double healthScale) {
|
||||
this.healthScale = healthScale;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's hunger meter
|
||||
*
|
||||
* @param hunger new hunger value
|
||||
*/
|
||||
public void setHunger(int hunger) {
|
||||
this.hunger = hunger;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's saturation level
|
||||
*
|
||||
* @param saturation new saturation value
|
||||
*/
|
||||
public void setSaturation(float saturation) {
|
||||
this.saturation = saturation;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's saturation exhaustion value
|
||||
*
|
||||
* @param saturationExhaustion new exhaustion value
|
||||
*/
|
||||
public void setSaturationExhaustion(float saturationExhaustion) {
|
||||
this.saturationExhaustion = saturationExhaustion;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's selected hotbar slot
|
||||
*
|
||||
* @param selectedSlot new hotbar slot number (0-9)
|
||||
*/
|
||||
public void setSelectedSlot(int selectedSlot) {
|
||||
this.selectedSlot = selectedSlot;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's status effect data
|
||||
*
|
||||
* @param serializedEffectData A serialized {@code String} of the player's new status effect data
|
||||
*/
|
||||
public void setSerializedEffectData(String serializedEffectData) {
|
||||
this.serializedEffectData = serializedEffectData;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the player's total experience points (used to display score on death screen)
|
||||
*
|
||||
* @param totalExperience the player's new total experience score
|
||||
*/
|
||||
public void setTotalExperience(int totalExperience) {
|
||||
this.totalExperience = totalExperience;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the player's exp level
|
||||
*
|
||||
* @param expLevel the player's new exp level
|
||||
*/
|
||||
public void setExpLevel(int expLevel) {
|
||||
this.expLevel = expLevel;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the player's progress to their next exp level
|
||||
*
|
||||
* @param expProgress the player's new experience progress
|
||||
*/
|
||||
public void setExpProgress(float expProgress) {
|
||||
this.expProgress = expProgress;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the player's game mode
|
||||
*
|
||||
* @param gameMode the player's new game mode ({@code SURVIVAL}, {@code CREATIVE}, etc.)
|
||||
*/
|
||||
public void setGameMode(String gameMode) {
|
||||
this.gameMode = gameMode;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's statistics data
|
||||
*
|
||||
* @param serializedStatistics A serialized {@code String}; new statistic data
|
||||
*/
|
||||
public void setSerializedStatistics(String serializedStatistics) {
|
||||
this.serializedStatistics = serializedStatistics;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if the player is flying
|
||||
*
|
||||
* @param flying whether the player is flying
|
||||
*/
|
||||
public void setFlying(boolean flying) {
|
||||
isFlying = flying;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's advancement data
|
||||
*
|
||||
* @param serializedAdvancements A serialized {@code String}; new advancement data
|
||||
*/
|
||||
public void setSerializedAdvancements(String serializedAdvancements) {
|
||||
this.serializedAdvancements = serializedAdvancements;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's location data
|
||||
*
|
||||
* @param serializedLocation A serialized {@code String}; new location data
|
||||
*/
|
||||
public void setSerializedLocation(String serializedLocation) {
|
||||
this.serializedLocation = serializedLocation;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A record representing a server synchronised on the network and whether it has MySqlPlayerDataBridge installed
|
||||
*/
|
||||
public record Server(UUID serverUUID, boolean hasMySqlPlayerDataBridge, String huskSyncVersion, String serverBrand,
|
||||
String clusterId) {
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Settings class, holds values loaded from the plugin config (either Bukkit or Bungee)
|
||||
*/
|
||||
public class Settings {
|
||||
|
||||
/*
|
||||
* General settings
|
||||
*/
|
||||
|
||||
// Whether to do automatic update checks on startup
|
||||
public static boolean automaticUpdateChecks;
|
||||
|
||||
// The type of THIS server (Bungee or Bukkit)
|
||||
public static ServerType serverType;
|
||||
|
||||
// Redis settings
|
||||
public static String redisHost;
|
||||
public static int redisPort;
|
||||
public static String redisPassword;
|
||||
public static boolean redisSSL;
|
||||
|
||||
/*
|
||||
* Bungee / Proxy server-only settings
|
||||
*/
|
||||
|
||||
// Messages language
|
||||
public static String language;
|
||||
|
||||
// Cluster IDs
|
||||
public static ArrayList<SynchronisationCluster> clusters = new ArrayList<>();
|
||||
|
||||
// SQL settings
|
||||
public static DataStorageType dataStorageType;
|
||||
|
||||
// Bounce-back synchronisation (default)
|
||||
public static boolean bounceBackSynchronisation;
|
||||
|
||||
// MySQL specific settings
|
||||
public static String mySQLHost;
|
||||
public static String mySQLDatabase;
|
||||
public static String mySQLUsername;
|
||||
public static String mySQLPassword;
|
||||
public static int mySQLPort;
|
||||
public static String mySQLParams;
|
||||
|
||||
// Hikari connection pooling settings
|
||||
public static int hikariMaximumPoolSize;
|
||||
public static int hikariMinimumIdle;
|
||||
public static long hikariMaximumLifetime;
|
||||
public static long hikariKeepAliveTime;
|
||||
public static long hikariConnectionTimeOut;
|
||||
|
||||
/*
|
||||
* Bukkit server-only settings
|
||||
*/
|
||||
|
||||
// Synchronisation options
|
||||
public static boolean syncInventories;
|
||||
public static boolean syncEnderChests;
|
||||
public static boolean syncHealth;
|
||||
public static boolean syncHunger;
|
||||
public static boolean syncExperience;
|
||||
public static boolean syncPotionEffects;
|
||||
public static boolean syncStatistics;
|
||||
public static boolean syncGameMode;
|
||||
public static boolean syncAdvancements;
|
||||
public static boolean syncLocation;
|
||||
public static boolean syncFlight;
|
||||
public static long synchronizationTimeoutRetryDelay;
|
||||
public static boolean saveOnWorldSave;
|
||||
public static boolean useNativeImplementation;
|
||||
|
||||
// This Cluster ID
|
||||
public static String cluster;
|
||||
|
||||
/*
|
||||
* Enum definitions
|
||||
*/
|
||||
|
||||
public enum ServerType {
|
||||
BUKKIT,
|
||||
PROXY,
|
||||
}
|
||||
|
||||
public enum DataStorageType {
|
||||
MYSQL,
|
||||
SQLITE
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines information for a synchronisation cluster as listed on the proxy
|
||||
*/
|
||||
public record SynchronisationCluster(String clusterId, String databaseName, String playerTableName, String dataTableName) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.adapter;
|
||||
|
||||
|
||||
public interface Adaptable {
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.adapter;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* An adapter that adapts data to and from a portable byte array.
|
||||
*/
|
||||
public interface DataAdapter {
|
||||
|
||||
/**
|
||||
* Converts an {@link Adaptable} to a string.
|
||||
*
|
||||
* @param data The {@link Adaptable} to adapt
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The string
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
@NotNull
|
||||
default <A extends Adaptable> String toString(@NotNull A data) throws AdaptionException {
|
||||
return new String(this.toBytes(data), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an {@link Adaptable} to a byte array.
|
||||
*
|
||||
* @param data The {@link Adaptable} to adapt
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The byte array
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
<A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException;
|
||||
|
||||
/**
|
||||
* Converts a JSON string to an {@link Adaptable}.
|
||||
*
|
||||
* @param data The JSON string to adapt.
|
||||
* @param type The class type of the {@link Adaptable} to adapt to.
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The {@link Adaptable}
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
@NotNull
|
||||
<A extends Adaptable> A fromJson(@NotNull String data, @NotNull Class<A> type) throws AdaptionException;
|
||||
|
||||
/**
|
||||
* Converts an {@link Adaptable} to a JSON string.
|
||||
*
|
||||
* @param data The {@link Adaptable} to adapt
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The JSON string
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
@NotNull
|
||||
<A extends Adaptable> String toJson(@NotNull A data) throws AdaptionException;
|
||||
|
||||
/**
|
||||
* Converts a byte array to an {@link Adaptable}.
|
||||
*
|
||||
* @param data The byte array to adapt.
|
||||
* @param type The class type of the {@link Adaptable} to adapt to.
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The {@link Adaptable}
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
<A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException;
|
||||
|
||||
/**
|
||||
* Converts a byte array to a string, including decompression if required.
|
||||
*
|
||||
* @param bytes The byte array to convert
|
||||
* @return the string form of the bytes
|
||||
*/
|
||||
@NotNull
|
||||
String bytesToString(byte[] bytes);
|
||||
|
||||
final class AdaptionException extends IllegalStateException {
|
||||
static final String FORMAT = "An exception occurred when adapting serialized/deserialized data: %s";
|
||||
|
||||
public AdaptionException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(String.format(FORMAT, message), cause);
|
||||
}
|
||||
|
||||
public AdaptionException(@NotNull String message) {
|
||||
super(String.format(FORMAT, message));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.adapter;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class GsonAdapter implements DataAdapter {
|
||||
|
||||
private final HuskSync plugin;
|
||||
|
||||
public GsonAdapter(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException {
|
||||
return this.toJson(data).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public <A extends Adaptable> String toJson(@NotNull A data) throws AdaptionException {
|
||||
try {
|
||||
return plugin.getGson().toJson(data);
|
||||
} catch (Throwable e) {
|
||||
throw new AdaptionException("Failed to adapt data to JSON via Gson", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public <A extends Adaptable> A fromBytes(byte[] data, @NotNull Class<A> type) throws AdaptionException {
|
||||
return this.fromJson(new String(data, StandardCharsets.UTF_8), type);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String bytesToString(byte[] bytes) {
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public <A extends Adaptable> A fromJson(@NotNull String data, @NotNull Class<A> type) throws AdaptionException {
|
||||
try {
|
||||
return plugin.getGson().fromJson(data, type);
|
||||
} catch (Throwable e) {
|
||||
throw new AdaptionException("Failed to adapt data from JSON via Gson", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.adapter;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.xerial.snappy.Snappy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SnappyGsonAdapter extends GsonAdapter {
|
||||
|
||||
public SnappyGsonAdapter(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException {
|
||||
try {
|
||||
return Snappy.compress(super.toBytes(data));
|
||||
} catch (IOException e) {
|
||||
throw new AdaptionException("Failed to compress data through Snappy", e);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public <A extends Adaptable> A fromBytes(byte[] data, @NotNull Class<A> type) throws AdaptionException {
|
||||
try {
|
||||
return super.fromBytes(decompressBytes(data), type);
|
||||
} catch (IOException e) {
|
||||
throw new AdaptionException("Failed to decompress data through Snappy", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String bytesToString(byte[] bytes) {
|
||||
try {
|
||||
return super.bytesToString(decompressBytes(bytes));
|
||||
} catch (IOException e) {
|
||||
throw new AdaptionException("Failed to decompress data through Snappy", e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] decompressBytes(byte[] bytes) throws IOException {
|
||||
return Snappy.uncompress(bytes);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.api;
|
||||
|
||||
import net.william278.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.Serializer;
|
||||
import net.william278.husksync.sync.DataSyncer;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* The common implementation of the HuskSync API, containing cross-platform API calls.
|
||||
* </p>
|
||||
* Retrieve an instance of the API class via {@link #getInstance()}.
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class HuskSyncAPI {
|
||||
|
||||
// Instance of the plugin
|
||||
protected static HuskSyncAPI instance;
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Instance of the implementing plugin.
|
||||
*/
|
||||
protected final HuskSync plugin;
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Constructor, instantiating the base API class.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
protected HuskSyncAPI(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint to the HuskSync API on the common platform - returns an instance of the API
|
||||
*
|
||||
* @return instance of the HuskSync API
|
||||
* @since 3.3
|
||||
*/
|
||||
@NotNull
|
||||
public static HuskSyncAPI getInstance() {
|
||||
if (instance == null) {
|
||||
throw new NotRegisteredException();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Unregister the API for this platform.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public static void unregister() {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link User} by their UUID
|
||||
*
|
||||
* @param uuid The UUID of the user to get
|
||||
* @return A future containing the user, or an empty optional if the user doesn't exist
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid) {
|
||||
return plugin.supplyAsync(() -> plugin.getDatabase().getUser(uuid));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an {@link OnlineUser} by their UUID
|
||||
*
|
||||
* @param uuid the UUID of the user to get
|
||||
* @return The {@link OnlineUser} wrapped in an optional, if they are online on <i>this</i> server.
|
||||
* @since 3.7.2
|
||||
*/
|
||||
@NotNull
|
||||
public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
|
||||
return plugin.getOnlineUser(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link User} by their username
|
||||
*
|
||||
* @param username The username of the user to get
|
||||
* @return A future containing the user, or an empty optional if the user doesn't exist
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public CompletableFuture<Optional<User>> getUser(@NotNull String username) {
|
||||
return plugin.supplyAsync(() -> plugin.getDatabase().getUserByName(username));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new data snapshot of an {@link OnlineUser}'s data.
|
||||
*
|
||||
* @param user The user to create the snapshot of
|
||||
* @return The snapshot of the user's data
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Packed createSnapshot(@NotNull OnlineUser user) {
|
||||
return snapshotBuilder().saveCause(DataSnapshot.SaveCause.API).buildAndPack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link User}'s current data, as a {@link DataSnapshot.Unpacked}
|
||||
* <p>
|
||||
* If the user is online, this will create a new snapshot of their data with the {@code API} data save cause.
|
||||
* </p>
|
||||
* If the user is offline, this will return the latest snapshot of their data if that exists
|
||||
* (an empty optional will be returned otherwise).
|
||||
*
|
||||
* @param user The user to get the data of
|
||||
* @return A future containing the user's current data, or an empty optional if the user has no data
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<DataSnapshot.Unpacked>> getCurrentData(@NotNull User user) {
|
||||
return plugin.getRedisManager()
|
||||
.getOnlineUserData(UUID.randomUUID(), user, DataSnapshot.SaveCause.API)
|
||||
.thenApply(data -> data.or(() -> plugin.getDatabase().getLatestSnapshot(user)))
|
||||
.thenApply(data -> data.map(snapshot -> snapshot.unpack(plugin)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a user's current data.
|
||||
* <p>
|
||||
* This will update the user's data in the database (creating a new snapshot) and send a data update,
|
||||
* updating the user if they are online.
|
||||
*
|
||||
* @param user The user to set the data of
|
||||
* @param data The data to set
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setCurrentData(@NotNull User user, @NotNull DataSnapshot data) {
|
||||
plugin.runAsync(() -> {
|
||||
final DataSnapshot.Packed packed = data instanceof DataSnapshot.Unpacked unpacked
|
||||
? unpacked.pack(plugin) : (DataSnapshot.Packed) data;
|
||||
addSnapshot(user, packed);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, packed);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a user's current data.
|
||||
* <p>
|
||||
* This will update the user's data in the database (creating a new snapshot) and send a data update,
|
||||
* updating the user if they are online.
|
||||
*
|
||||
* @param user The user to edit the data of
|
||||
* @param editor The editor function
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editCurrentData(@NotNull User user, @NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
getCurrentData(user).thenAccept(optional -> optional.ifPresent(data -> {
|
||||
editor.accept(data);
|
||||
data.setId(UUID.randomUUID());
|
||||
setCurrentData(user, data);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all saved data snapshots for a user
|
||||
*
|
||||
* @param user The user to get the data snapshots of
|
||||
* @return The user's data snapshots
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<List<DataSnapshot.Unpacked>> getSnapshots(@NotNull User user) {
|
||||
return plugin.supplyAsync(
|
||||
() -> plugin.getDatabase().getAllSnapshots(user).stream()
|
||||
.map(snapshot -> snapshot.unpack(plugin))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific data snapshot for a user
|
||||
*
|
||||
* @param user The user to get the data snapshot of
|
||||
* @param versionId The version ID of the snapshot to get
|
||||
* @return The user's data snapshot, or an empty optional if the user has no data
|
||||
* @see #getSnapshots(User)
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<List<DataSnapshot.Unpacked>> getSnapshot(@NotNull User user, @NotNull UUID versionId) {
|
||||
return plugin.supplyAsync(
|
||||
() -> plugin.getDatabase().getSnapshot(user, versionId).stream()
|
||||
.map(snapshot -> snapshot.unpack(plugin))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a data snapshot for a user
|
||||
*
|
||||
* @param user The user to edit the snapshot of
|
||||
* @param versionId The version ID of the snapshot to edit
|
||||
* @param editor The editor function
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editSnapshot(@NotNull User user, @NotNull UUID versionId,
|
||||
@NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().getSnapshot(user, versionId).ifPresent(snapshot -> {
|
||||
final DataSnapshot.Unpacked unpacked = snapshot.unpack(plugin);
|
||||
editor.accept(unpacked);
|
||||
plugin.getDatabase().updateSnapshot(user, unpacked.pack(plugin));
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest data snapshot for a user that has been saved in the database.
|
||||
* <p>
|
||||
* Not to be confused with {@link #getCurrentData(User)}, which will return the current data of a user
|
||||
* if they are online (this method will only return their latest <i>saved</i> snapshot).
|
||||
* </p>
|
||||
*
|
||||
* @param user The user to get the latest data snapshot of
|
||||
* @return The user's latest data snapshot, or an empty optional if the user has no data
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<DataSnapshot.Unpacked>> getLatestSnapshot(@NotNull User user) {
|
||||
return plugin.supplyAsync(
|
||||
() -> plugin.getDatabase().getLatestSnapshot(user).map(snapshot -> snapshot.unpack(plugin))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the latest data snapshot for a user
|
||||
*
|
||||
* @param user The user to edit the latest snapshot of
|
||||
* @param editor The editor function
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editLatestSnapshot(@NotNull User user, @NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().getLatestSnapshot(user).ifPresent(snapshot -> {
|
||||
final DataSnapshot.Unpacked unpacked = snapshot.unpack(plugin);
|
||||
editor.accept(unpacked);
|
||||
plugin.getDatabase().updateSnapshot(user, unpacked.pack(plugin));
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a data snapshot to the database
|
||||
*
|
||||
* @param user The user to save the data for
|
||||
* @param snapshot The snapshot to save
|
||||
* @param callback A callback to run after the data has been saved (if the DataSaveEvent was not canceled)
|
||||
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will be fired unless the
|
||||
* {@link DataSnapshot.SaveCause#fireDataSaveEvent()} is {@code false}
|
||||
* @since 3.3.2
|
||||
*/
|
||||
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot,
|
||||
@Nullable BiConsumer<User, DataSnapshot.Packed> callback) {
|
||||
plugin.runAsync(() -> plugin.getDataSyncer().saveData(
|
||||
user,
|
||||
snapshot instanceof DataSnapshot.Unpacked unpacked
|
||||
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot,
|
||||
callback
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a data snapshot to the database
|
||||
*
|
||||
* @param user The user to save the data for
|
||||
* @param snapshot The snapshot to save
|
||||
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will be fired unless the
|
||||
* {@link DataSnapshot.SaveCause#fireDataSaveEvent()} is {@code false}
|
||||
* @since 3.0
|
||||
*/
|
||||
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
|
||||
this.addSnapshot(user, snapshot, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an <i>existing</i> data snapshot in the database.
|
||||
* Not to be confused with {@link #addSnapshot(User, DataSnapshot)}, which will add a new snapshot if one
|
||||
* snapshot doesn't exist.
|
||||
*
|
||||
* @param user The user to update the snapshot of
|
||||
* @param snapshot The snapshot to update
|
||||
* @since 3.0
|
||||
*/
|
||||
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().updateSnapshot(
|
||||
user, snapshot instanceof DataSnapshot.Unpacked unpacked
|
||||
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a data snapshot, preventing it from being rotated
|
||||
*
|
||||
* @param user The user to pin the snapshot of
|
||||
* @param snapshotVersion The version ID of the snapshot to pin
|
||||
* @since 3.0
|
||||
*/
|
||||
public void pinSnapshot(@NotNull User user, @NotNull UUID snapshotVersion) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().pinSnapshot(user, snapshotVersion));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a data snapshot, allowing it to be rotated
|
||||
*
|
||||
* @param user The user to unpin the snapshot of
|
||||
* @param snapshotVersion The version ID of the snapshot to unpin
|
||||
* @since 3.0
|
||||
*/
|
||||
public void unpinSnapshot(@NotNull User user, @NotNull UUID snapshotVersion) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().unpinSnapshot(user, snapshotVersion));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a data snapshot from the database
|
||||
*
|
||||
* @param user The user to delete the snapshot of
|
||||
* @param versionId The version ID of the snapshot to delete
|
||||
* @return A future which will complete with true if the snapshot was deleted, or false if it wasn't
|
||||
* (e.g., if the snapshot didn't exist)
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Boolean> deleteSnapshot(@NotNull User user, @NotNull UUID versionId) {
|
||||
return plugin.supplyAsync(() -> plugin.getDatabase().deleteSnapshot(user, versionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a data snapshot from the database
|
||||
*
|
||||
* @param user The user to delete the snapshot of
|
||||
* @param snapshot The snapshot to delete
|
||||
* @return A future which will complete with true if the snapshot was deleted, or false if it wasn't
|
||||
* (e.g., if the snapshot hasn't been saved to the database yet)
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Boolean> deleteSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
|
||||
return deleteSnapshot(user, snapshot.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new custom data type serializer.
|
||||
* <p>
|
||||
* This allows for custom {@link Data} types to be persisted in {@link DataSnapshot}s. To register
|
||||
* a new data type, you must provide a {@link Serializer} for serializing and deserializing the data type
|
||||
* and invoke this method.
|
||||
* </p>
|
||||
* You'll need to do this on every server you wish to sync data between. On servers where the registered
|
||||
* data type is not present, the data will be ignored and snapshots created on that server will not
|
||||
* contain the data.
|
||||
*
|
||||
* @param identifier The identifier of the data type to register.
|
||||
* Create one using {@code Identifier.from(Key.of("your_plugin_name", "key"))}
|
||||
* @param serializer An implementation of {@link Serializer} for serializing and deserializing the {@link Data}
|
||||
* @param <T> A type extending {@link Data}; this will represent the data being held.
|
||||
*/
|
||||
public <T extends Data> void registerDataSerializer(@NotNull Identifier identifier,
|
||||
@NotNull Serializer<T> serializer) {
|
||||
plugin.registerSerializer(identifier, serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a registered data serializer by its identifier
|
||||
*
|
||||
* @param identifier The identifier of the data type to get the serializer for
|
||||
* @return The serializer for the given identifier, or an empty optional if the serializer isn't registered
|
||||
* @since 3.5.4
|
||||
*/
|
||||
public Optional<Serializer<Data>> getDataSerializer(@NotNull Identifier identifier) {
|
||||
return plugin.getSerializer(identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link DataSnapshot.Unpacked} from a {@link DataSnapshot.Packed}
|
||||
*
|
||||
* @param unpacked The unpacked snapshot
|
||||
* @return The packed snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Packed packSnapshot(@NotNull DataSnapshot.Unpacked unpacked) {
|
||||
return unpacked.pack(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link DataSnapshot.Unpacked} from a {@link DataSnapshot.Packed}
|
||||
*
|
||||
* @param packed The packed snapshot
|
||||
* @return The unpacked snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Unpacked unpackSnapshot(@NotNull DataSnapshot.Packed packed) {
|
||||
return packed.unpack(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpack, edit, and repack a data snapshot.
|
||||
* </p>
|
||||
* This won't save the snapshot to the database; it'll just edit the data snapshot in place.
|
||||
*
|
||||
* @param packed The packed snapshot
|
||||
* @param editor An editor function for editing the unpacked snapshot
|
||||
* @return The edited packed snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Packed editPackedSnapshot(@NotNull DataSnapshot.Packed packed,
|
||||
@NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
final DataSnapshot.Unpacked unpacked = packed.unpack(plugin);
|
||||
editor.accept(unpacked);
|
||||
return unpacked.pack(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the estimated size of a {@link DataSnapshot} in bytes
|
||||
*
|
||||
* @param snapshot The snapshot to get the size of
|
||||
* @return The size of the snapshot in bytes
|
||||
* @since 3.0
|
||||
*/
|
||||
public int getSnapshotFileSize(@NotNull DataSnapshot snapshot) {
|
||||
return (snapshot instanceof DataSnapshot.Packed packed)
|
||||
? packed.getFileSize(plugin)
|
||||
: ((DataSnapshot.Unpacked) snapshot).pack(plugin).getFileSize(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a builder for creating a new data snapshot
|
||||
*
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Builder snapshotBuilder() {
|
||||
return DataSnapshot.builder(plugin).saveCause(DataSnapshot.SaveCause.API);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a JSON string to an {@link Adaptable}
|
||||
*
|
||||
* @param serialized The serialized JSON string
|
||||
* @param type The type of the element
|
||||
* @param <T> The type of the element
|
||||
* @return The deserialized element
|
||||
* @throws Serializer.DeserializationException If the element could not be deserialized
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public <T extends Adaptable> T deserializeData(@NotNull String serialized, Class<T> type)
|
||||
throws Serializer.DeserializationException {
|
||||
return plugin.getDataAdapter().fromJson(serialized, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize an {@link Adaptable} to a JSON string
|
||||
*
|
||||
* @param element The element to serialize
|
||||
* @param <T> The type of the element
|
||||
* @return The serialized JSON string
|
||||
* @throws Serializer.SerializationException If the element could not be serialized
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public <T extends Adaptable> String serializeData(@NotNull T element)
|
||||
throws Serializer.SerializationException {
|
||||
return plugin.getDataAdapter().toJson(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link DataSyncer} to be used to sync data
|
||||
*
|
||||
* @param syncer The data syncer to use for synchronizing user data
|
||||
* @since 3.1
|
||||
*/
|
||||
public void setDataSyncer(@NotNull DataSyncer syncer) {
|
||||
plugin.setDataSyncer(syncer);
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Get the plugin instance
|
||||
*
|
||||
* @return The plugin instance
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* An exception indicating the plugin has been accessed before it has been registered.
|
||||
*/
|
||||
static final class NotRegisteredException extends IllegalStateException {
|
||||
|
||||
private static final String REASONS = """
|
||||
This may be because:
|
||||
1) HuskSync has failed to enable successfully
|
||||
2) Your plugin isn't set to load after HuskSync has
|
||||
(Check if it set as a (soft)depend in plugin.yml or to load: BEFORE in paper-plugin.yml?)
|
||||
3) You are attempting to access HuskSync on plugin construction/before your plugin has enabled.""";
|
||||
|
||||
NotRegisteredException(@NotNull String reasons) {
|
||||
super("Could not access the HuskSync API as it has not yet been registered. %s".formatted(reasons));
|
||||
}
|
||||
|
||||
NotRegisteredException() {
|
||||
this(REASONS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class EnderChestCommand extends ItemsCommand {
|
||||
|
||||
public EnderChestCommand(@NotNull HuskSync plugin) {
|
||||
super("enderchest", List.of("echest", "openechest"), DataSnapshot.SaveCause.ENDERCHEST_COMMAND, plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
|
||||
@NotNull User user, boolean allowEdit) {
|
||||
final Optional<Data.Items.EnderChest> optionalEnderChest = snapshot.getEnderChest();
|
||||
if (optionalEnderChest.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Display opening message
|
||||
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getName(),
|
||||
snapshot.getTimestamp().format(DateTimeFormatter
|
||||
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
|
||||
// Show GUI
|
||||
final Data.Items.EnderChest enderChest = optionalEnderChest.get();
|
||||
viewer.showGui(
|
||||
enderChest,
|
||||
plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getName())
|
||||
.orElse(new MineDown(String.format("%s's Ender Chest", user.getName()))),
|
||||
allowEdit,
|
||||
enderChest.getSlotCount(),
|
||||
(itemsOnClose) -> {
|
||||
if (allowEdit && !enderChest.equals(itemsOnClose)) {
|
||||
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Creates a new snapshot with the updated enderChest
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
|
||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
|
||||
if (latestData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and pack the snapshot with the updated enderChest
|
||||
final DataSnapshot.Packed snapshot = latestData.get().copy();
|
||||
boolean pin = plugin.getSettings().getSynchronization().doAutoPin(saveCause);
|
||||
snapshot.edit(plugin, (data) -> {
|
||||
data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items));
|
||||
data.setSaveCause(saveCause);
|
||||
data.setPinned(pin);
|
||||
});
|
||||
|
||||
// Save data
|
||||
final RedisManager redis = plugin.getRedisManager();
|
||||
plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
|
||||
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot));
|
||||
redis.sendUserDataUpdate(user, data);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.JoinConfiguration;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import net.william278.desertwell.about.AboutMenu;
|
||||
import net.william278.desertwell.util.UpdateChecker;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.util.LegacyConverter;
|
||||
import net.william278.husksync.util.StatusLine;
|
||||
import net.william278.uniform.BaseCommand;
|
||||
import net.william278.uniform.CommandProvider;
|
||||
import net.william278.uniform.Permission;
|
||||
import net.william278.uniform.element.ArgumentElement;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class HuskSyncCommand extends PluginCommand {
|
||||
|
||||
private final UpdateChecker updateChecker;
|
||||
private final AboutMenu aboutMenu;
|
||||
|
||||
public HuskSyncCommand(@NotNull HuskSync plugin) {
|
||||
super("husksync", List.of(), Permission.Default.TRUE, ExecutionScope.ALL, plugin);
|
||||
this.updateChecker = plugin.getUpdateChecker();
|
||||
this.aboutMenu = AboutMenu.builder()
|
||||
.title(Component.text("HuskSync"))
|
||||
.description(Component.text("A modern, cross-server player data synchronization system"))
|
||||
.version(plugin.getPluginVersion())
|
||||
.credits("Author",
|
||||
AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net"))
|
||||
.credits("Contributors",
|
||||
AboutMenu.Credit.of("HarvelsX").description("Code"),
|
||||
AboutMenu.Credit.of("HookWoods").description("Code"),
|
||||
AboutMenu.Credit.of("Preva1l").description("Code"),
|
||||
AboutMenu.Credit.of("hanbings").description("Code (Fabric porting)"),
|
||||
AboutMenu.Credit.of("Stampede2011").description("Code (Fabric mixins)"),
|
||||
AboutMenu.Credit.of("VinerDream").description("Code"))
|
||||
.credits("Translators",
|
||||
AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"),
|
||||
AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"),
|
||||
AboutMenu.Credit.of("Melonzio").description("Spanish (es-es)"),
|
||||
AboutMenu.Credit.of("Ceddix").description("German (de-de)"),
|
||||
AboutMenu.Credit.of("Pukejoy_1").description("Bulgarian (bg-bg)"),
|
||||
AboutMenu.Credit.of("mateusneresrb").description("Brazilian Portuguese (pt-br)"),
|
||||
AboutMenu.Credit.of("小蔡").description("Traditional Chinese (zh-tw)"),
|
||||
AboutMenu.Credit.of("Ghost-chu").description("Simplified Chinese (zh-cn)"),
|
||||
AboutMenu.Credit.of("DJelly4K").description("Simplified Chinese (zh-cn)"),
|
||||
AboutMenu.Credit.of("Thourgard").description("Ukrainian (uk-ua)"),
|
||||
AboutMenu.Credit.of("xF3d3").description("Italian (it-it)"),
|
||||
AboutMenu.Credit.of("cada3141").description("Korean (ko-kr)"),
|
||||
AboutMenu.Credit.of("Wirayuda5620").description("Indonesian (id-id)"),
|
||||
AboutMenu.Credit.of("WinTone01").description("Turkish (tr-tr)"),
|
||||
AboutMenu.Credit.of("IbanEtchep").description("French (fr-fr)"))
|
||||
.buttons(
|
||||
AboutMenu.Link.of("https://william278.net/docs/husksync").text("Documentation").icon("⛏"),
|
||||
AboutMenu.Link.of("https://github.com/WiIIiam278/HuskSync/issues").text("Issues").icon("❌").color(TextColor.color(0xff9f0f)),
|
||||
AboutMenu.Link.of("https://discord.gg/tVYhJfyDWG").text("Discord").icon("⭐").color(TextColor.color(0x6773f5)))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void provide(@NotNull BaseCommand<?> command) {
|
||||
command.setDefaultExecutor((ctx) -> about(command, ctx));
|
||||
command.addSubCommand("about", (sub) -> sub.setDefaultExecutor((ctx) -> about(command, ctx)));
|
||||
command.addSubCommand("status", needsOp("status"), status());
|
||||
command.addSubCommand("dump", needsOp("dump"), dump());
|
||||
command.addSubCommand("reload", needsOp("reload"), reload());
|
||||
command.addSubCommand("update", needsOp("update"), update());
|
||||
command.addSubCommand("forceupgrade", forceUpgrade());
|
||||
command.addSubCommand("migrate", migrate());
|
||||
}
|
||||
|
||||
private void about(@NotNull BaseCommand<?> c, @NotNull CommandContext<?> ctx) {
|
||||
user(c, ctx).getAudience().sendMessage(aboutMenu.toComponent());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider status() {
|
||||
return (sub) -> sub.setDefaultExecutor((ctx) -> {
|
||||
final CommandUser user = user(sub, ctx);
|
||||
plugin.getLocales().getLocale("system_status_header").ifPresent(user::sendMessage);
|
||||
user.sendMessage(Component.join(
|
||||
JoinConfiguration.newlines(),
|
||||
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList()
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider dump() {
|
||||
return (sub) -> {
|
||||
sub.setDefaultExecutor((ctx) -> {
|
||||
final CommandUser user = user(sub, ctx);
|
||||
plugin.getLocales().getLocale("system_dump_confirm").ifPresent(user::sendMessage);
|
||||
});
|
||||
sub.addSubCommand("confirm", (con) -> con.setDefaultExecutor((ctx) -> {
|
||||
final CommandUser user = user(sub, ctx);
|
||||
plugin.getLocales().getLocale("system_dump_started").ifPresent(user::sendMessage);
|
||||
plugin.runAsync(() -> {
|
||||
final String url = plugin.createDump(user);
|
||||
plugin.getLocales().getLocale("system_dump_ready").ifPresent(user::sendMessage);
|
||||
user.sendMessage(Component.text(url).clickEvent(ClickEvent.openUrl(url))
|
||||
.decorate(TextDecoration.UNDERLINED).color(NamedTextColor.GRAY));
|
||||
});
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider reload() {
|
||||
return (sub) -> sub.setDefaultExecutor((ctx) -> {
|
||||
final CommandUser user = user(sub, ctx);
|
||||
try {
|
||||
plugin.loadSettings();
|
||||
plugin.loadLocales();
|
||||
plugin.loadServer();
|
||||
plugin.getLocales().getLocale("reload_complete").ifPresent(user::sendMessage);
|
||||
} catch (Throwable e) {
|
||||
user.sendMessage(new MineDown(
|
||||
"[Error:](#ff3300) [Failed to reload the plugin. Check console for errors.](#ff7e5e)"
|
||||
));
|
||||
plugin.log(Level.SEVERE, "Failed to reload the plugin", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider update() {
|
||||
return (sub) -> sub.setDefaultExecutor((ctx) -> updateChecker.check().thenAccept(checked -> {
|
||||
final CommandUser user = user(sub, ctx);
|
||||
if (checked.isUpToDate()) {
|
||||
plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getLocales().getLocale("update_available", checked.getLatestVersion().toString(),
|
||||
plugin.getPluginVersion().toString()).ifPresent(user::sendMessage);
|
||||
}));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider migrate() {
|
||||
return (sub) -> {
|
||||
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
|
||||
sub.setDefaultExecutor((ctx) -> {
|
||||
plugin.log(Level.INFO, "Please choose a migrator, then run \"husksync migrate start <migrator>\"");
|
||||
plugin.log(Level.INFO, String.format(
|
||||
"List of available migrators:\nMigrator ID / Migrator Name:\n%s",
|
||||
plugin.getAvailableMigrators().stream()
|
||||
.map(migrator -> String.format("%s - %s", migrator.getIdentifier(), migrator.getName()))
|
||||
.collect(Collectors.joining("\n"))
|
||||
));
|
||||
});
|
||||
sub.addSubCommand("help", (help) -> help.addSyntax((cmd) -> {
|
||||
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
|
||||
plugin.log(Level.INFO, migrator.getHelpMenu());
|
||||
}, migrator()));
|
||||
sub.addSubCommand("start", (start) -> start.addSyntax((cmd) -> {
|
||||
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
|
||||
migrator.start().thenAccept(succeeded -> {
|
||||
if (succeeded) {
|
||||
plugin.log(Level.INFO, "Migration completed successfully!");
|
||||
} else {
|
||||
plugin.log(Level.WARNING, "Migration failed!");
|
||||
}
|
||||
});
|
||||
}, migrator()));
|
||||
sub.addSubCommand("set", (set) -> set.addSyntax((cmd) -> {
|
||||
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
|
||||
final String[] args = cmd.getArgument("args", String.class).split(" ");
|
||||
migrator.handleConfigurationCommand(args);
|
||||
}, migrator(), BaseCommand.greedyString("args")));
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider forceUpgrade() {
|
||||
return (sub) -> {
|
||||
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
|
||||
sub.setDefaultExecutor((ctx) -> {
|
||||
final LegacyConverter converter = plugin.getLegacyConverter().orElse(null);
|
||||
if (converter == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.runAsync(() -> {
|
||||
final Database database = plugin.getDatabase();
|
||||
plugin.log(Level.INFO, "Beginning forced legacy data upgrade for all users...");
|
||||
database.getAllUsers().forEach(user -> database.getLatestSnapshot(user).ifPresent(snapshot -> {
|
||||
final DataSnapshot.Packed upgraded = converter.convert(
|
||||
snapshot.asBytes(plugin),
|
||||
UUID.randomUUID(),
|
||||
OffsetDateTime.now()
|
||||
);
|
||||
upgraded.setSaveCause(DataSnapshot.SaveCause.CONVERTED_FROM_V2);
|
||||
plugin.getDatabase().addSnapshot(user, upgraded);
|
||||
plugin.getRedisManager().clearUserData(user);
|
||||
}));
|
||||
plugin.log(Level.INFO, "Legacy data upgrade complete!");
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private <S> ArgumentElement<S, Migrator> migrator() {
|
||||
return new ArgumentElement<>("migrator", reader -> {
|
||||
final String id = reader.readString();
|
||||
final Migrator migrator = plugin.getAvailableMigrators().stream()
|
||||
.filter(m -> m.getIdentifier().equalsIgnoreCase(id)).findFirst().orElse(null);
|
||||
if (migrator == null) {
|
||||
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
|
||||
}
|
||||
return migrator;
|
||||
}, (context, builder) -> {
|
||||
for (Migrator material : plugin.getAvailableMigrators()) {
|
||||
builder.suggest(material.getIdentifier());
|
||||
}
|
||||
return builder.buildFuture();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class InventoryCommand extends ItemsCommand {
|
||||
|
||||
public InventoryCommand(@NotNull HuskSync plugin) {
|
||||
super("inventory", List.of("invsee", "openinv"), DataSnapshot.SaveCause.INVENTORY_COMMAND, plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
|
||||
@NotNull User user, boolean allowEdit) {
|
||||
final Optional<Data.Items.Inventory> optionalInventory = snapshot.getInventory();
|
||||
if (optionalInventory.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Display opening message
|
||||
plugin.getLocales().getLocale("inventory_viewer_opened", user.getName(),
|
||||
snapshot.getTimestamp().format(DateTimeFormatter
|
||||
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
|
||||
// Show GUI
|
||||
final Data.Items.Inventory inventory = optionalInventory.get();
|
||||
viewer.showGui(
|
||||
inventory,
|
||||
plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getName())
|
||||
.orElse(new MineDown(String.format("%s's Inventory", user.getName()))),
|
||||
allowEdit,
|
||||
inventory.getSlotCount(),
|
||||
(itemsOnClose) -> {
|
||||
if (allowEdit && !inventory.equals(itemsOnClose)) {
|
||||
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Creates a new snapshot with the updated inventory
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
|
||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
|
||||
if (latestData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and pack the snapshot with the updated inventory
|
||||
final DataSnapshot.Packed snapshot = latestData.get().copy();
|
||||
boolean pin = plugin.getSettings().getSynchronization().doAutoPin(saveCause);
|
||||
snapshot.edit(plugin, (data) -> {
|
||||
data.getInventory().ifPresent(inventory -> inventory.setContents(items));
|
||||
data.setSaveCause(saveCause);
|
||||
data.setPinned(pin);
|
||||
});
|
||||
|
||||
// Save data
|
||||
final RedisManager redis = plugin.getRedisManager();
|
||||
plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
|
||||
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot));
|
||||
redis.sendUserDataUpdate(user, data);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import net.william278.uniform.BaseCommand;
|
||||
import net.william278.uniform.Permission;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class ItemsCommand extends PluginCommand {
|
||||
|
||||
protected final DataSnapshot.SaveCause saveCause;
|
||||
|
||||
protected ItemsCommand(@NotNull String name, @NotNull List<String> aliases,
|
||||
@NotNull DataSnapshot.SaveCause saveCause, @NotNull HuskSync plugin) {
|
||||
super(name, aliases, Permission.Default.IF_OP, ExecutionScope.IN_GAME, plugin);
|
||||
this.saveCause = saveCause;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void provide(@NotNull BaseCommand<?> command) {
|
||||
command.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final UUID version = ctx.getArgument("version", UUID.class);
|
||||
final CommandUser executor = user(command, ctx);
|
||||
if (!(executor instanceof OnlineUser online)) {
|
||||
plugin.getLocales().getLocale("error_in_game_command_only")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
this.showSnapshotItems(online, user, version);
|
||||
}, user("username"), versionUuid());
|
||||
command.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final CommandUser executor = user(command, ctx);
|
||||
if (!(executor instanceof OnlineUser online)) {
|
||||
plugin.getLocales().getLocale("error_in_game_command_only")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
this.showLatestItems(online, user);
|
||||
}, user("username"));
|
||||
}
|
||||
|
||||
// View (and edit) the latest user data
|
||||
private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) {
|
||||
plugin.getRedisManager().getOnlineUserData(user.getUuid(), user, saveCause).thenAccept(d -> d
|
||||
.or(() -> plugin.getDatabase().getLatestSnapshot(user))
|
||||
.or(() -> {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return Optional.empty();
|
||||
})
|
||||
.flatMap(packed -> {
|
||||
if (packed.isInvalid()) {
|
||||
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(packed.unpack(plugin));
|
||||
})
|
||||
.ifPresent(snapshot -> this.showItems(
|
||||
viewer, snapshot, user, viewer.hasPermission(getPermission("edit"))
|
||||
)));
|
||||
}
|
||||
|
||||
// View a specific version of the user data
|
||||
private void showSnapshotItems(@NotNull OnlineUser viewer, @NotNull User user, @NotNull UUID version) {
|
||||
plugin.getDatabase().getSnapshot(user, version)
|
||||
.or(() -> {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return Optional.empty();
|
||||
})
|
||||
.flatMap(packed -> {
|
||||
if (packed.isInvalid()) {
|
||||
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(packed.unpack(plugin));
|
||||
})
|
||||
.ifPresent(snapshot -> this.showItems(
|
||||
viewer, snapshot, user, false
|
||||
));
|
||||
}
|
||||
|
||||
// Show a GUI menu with the correct item data from the snapshot
|
||||
protected abstract void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
|
||||
@NotNull User user, boolean allowEdit);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import net.william278.uniform.BaseCommand;
|
||||
import net.william278.uniform.Command;
|
||||
import net.william278.uniform.Permission;
|
||||
import net.william278.uniform.element.ArgumentElement;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
public abstract class PluginCommand extends Command {
|
||||
|
||||
protected final HuskSync plugin;
|
||||
|
||||
protected PluginCommand(@NotNull String name, @NotNull List<String> aliases, @NotNull Permission.Default defPerm,
|
||||
@NotNull ExecutionScope scope, @NotNull HuskSync plugin) {
|
||||
super(name, aliases, getDescription(plugin, name), new Permission(createPermission(name), defPerm), scope);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
private static String getDescription(@NotNull HuskSync plugin, @NotNull String name) {
|
||||
return plugin.getLocales().getRawLocale("%s_command_description".formatted(name)).orElse("");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String createPermission(@NotNull String name, @NotNull String... sub) {
|
||||
return "husksync.command." + name + (sub.length > 0 ? "." + String.join(".", sub) : "");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected String getPermission(@NotNull String... sub) {
|
||||
return createPermission(this.getName(), sub);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@SuppressWarnings("rawtypes")
|
||||
protected CommandUser user(@NotNull BaseCommand base, @NotNull CommandContext context) {
|
||||
return adapt(base.getUser(context.getSource()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected Permission needsOp(@NotNull String... nodes) {
|
||||
return new Permission(getPermission(nodes), Permission.Default.IF_OP);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected CommandUser adapt(net.william278.uniform.CommandUser user) {
|
||||
return user.getUuid() == null ? plugin.getConsole() : plugin.getOnlineUser(user.getUuid()).orElseThrow();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
protected <S> ArgumentElement<S, OnlineUser> onlineUser(@NotNull String name) {
|
||||
return new ArgumentElement<>(name, reader -> {
|
||||
final String username = reader.readString();
|
||||
return plugin.getOnlineUsers().stream()
|
||||
.filter(user -> username.equals(user.getName()))
|
||||
.findFirst().orElse(null);
|
||||
}, (context, builder) -> {
|
||||
plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getName()));
|
||||
return builder.buildFuture();
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected <S> ArgumentElement<S, User> user(@NotNull String name) {
|
||||
return new ArgumentElement<>(name, reader -> {
|
||||
final String username = reader.readString();
|
||||
return plugin.getDatabase().getUserByName(username).orElseThrow(
|
||||
() -> CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader)
|
||||
);
|
||||
}, (context, builder) -> {
|
||||
plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getName()));
|
||||
return builder.buildFuture();
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected <S> ArgumentElement<S, UUID> versionUuid() {
|
||||
return new ArgumentElement<>("version", reader -> {
|
||||
try {
|
||||
return UUID.fromString(reader.readString());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
|
||||
}
|
||||
}, (context, builder) -> {
|
||||
try {
|
||||
plugin.getDatabase().getAllSnapshots(context.getArgument("username", User.class))
|
||||
.stream().sorted(Comparator.comparing(d -> d.getTimestamp().toEpochSecond()))
|
||||
.forEach(id -> builder.suggest(id.getId().toString()));
|
||||
return builder.buildFuture();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return builder.buildFuture();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
|
||||
HUSKSYNC_COMMAND(HuskSyncCommand::new),
|
||||
USERDATA_COMMAND(UserDataCommand::new),
|
||||
INVENTORY_COMMAND(InventoryCommand::new),
|
||||
ENDER_CHEST_COMMAND(EnderChestCommand::new);
|
||||
|
||||
public final Function<HuskSync, PluginCommand> commandSupplier;
|
||||
|
||||
Type(@NotNull Function<HuskSync, PluginCommand> supplier) {
|
||||
this.commandSupplier = supplier;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public PluginCommand supply(@NotNull HuskSync plugin) {
|
||||
return commandSupplier.apply(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static PluginCommand[] create(@NotNull HuskSync plugin) {
|
||||
return Arrays.stream(values()).map(type -> type.supply(plugin))
|
||||
.filter(command -> !plugin.getSettings().isCommandDisabled(command))
|
||||
.toArray(PluginCommand[]::new);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import net.william278.husksync.util.DataSnapshotList;
|
||||
import net.william278.husksync.util.DataSnapshotOverview;
|
||||
import net.william278.husksync.util.UserDataDumper;
|
||||
import net.william278.uniform.BaseCommand;
|
||||
import net.william278.uniform.CommandProvider;
|
||||
import net.william278.uniform.Permission;
|
||||
import net.william278.uniform.element.ArgumentElement;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class UserDataCommand extends PluginCommand {
|
||||
|
||||
public UserDataCommand(@NotNull HuskSync plugin) {
|
||||
super("userdata", List.of("playerdata"), Permission.Default.IF_OP, ExecutionScope.ALL, plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void provide(@NotNull BaseCommand<?> command) {
|
||||
command.addSubCommand("view", needsOp("view"), view());
|
||||
command.addSubCommand("list", needsOp("list"), list());
|
||||
command.addSubCommand("delete", needsOp("delete"), delete());
|
||||
command.addSubCommand("save", needsOp("save"), save());
|
||||
command.addSubCommand("restore", needsOp("restore"), restore());
|
||||
command.addSubCommand("pin", needsOp("pin"), pin());
|
||||
command.addSubCommand("dump", needsOp("dump"), dump());
|
||||
}
|
||||
|
||||
// Show the latest snapshot
|
||||
private void viewLatestSnapshot(@NotNull CommandUser executor, @NotNull User user) {
|
||||
plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse(
|
||||
data -> {
|
||||
if (data.isInvalid()) {
|
||||
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
DataSnapshotOverview.of(data.unpack(plugin), data.getFileSize(plugin), user, plugin)
|
||||
.show(executor);
|
||||
},
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(executor::sendMessage)
|
||||
);
|
||||
}
|
||||
|
||||
// Show the specified snapshot
|
||||
private void viewSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
||||
plugin.getDatabase().getSnapshot(user, version).ifPresentOrElse(
|
||||
data -> {
|
||||
if (data.isInvalid()) {
|
||||
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
DataSnapshotOverview.of(data.unpack(plugin), data.getFileSize(plugin), user, plugin)
|
||||
.show(executor);
|
||||
},
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage)
|
||||
);
|
||||
}
|
||||
|
||||
// View a list of snapshots
|
||||
private void listSnapshots(@NotNull CommandUser executor, @NotNull User user, int page) {
|
||||
final List<DataSnapshot.Packed> dataList = plugin.getDatabase().getAllSnapshots(user);
|
||||
if (dataList.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
DataSnapshotList.create(dataList, user, plugin).displayPage(executor, page);
|
||||
}
|
||||
|
||||
// Create and save a snapshot of a user's current data
|
||||
private void createAndSaveSnapshot(@NotNull CommandUser executor, @NotNull OnlineUser onlineUser) {
|
||||
plugin.getDataSyncer().saveCurrentUserData(onlineUser, DataSnapshot.SaveCause.SAVE_COMMAND);
|
||||
plugin.getLocales().getLocale("data_saved", onlineUser.getName())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
|
||||
// Delete a snapshot
|
||||
private void deleteSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
||||
if (!plugin.getDatabase().deleteSnapshot(user, version)) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getRedisManager().clearUserData(user);
|
||||
plugin.getLocales().getLocale("data_deleted",
|
||||
version.toString().split("-")[0],
|
||||
version.toString(),
|
||||
user.getName(),
|
||||
user.getUuid().toString())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
|
||||
// Restore a snapshot
|
||||
private void restoreSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
||||
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, version);
|
||||
if (optionalData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore users with a minimum of one health (prevent restoring players with <= 0 health)
|
||||
final DataSnapshot.Packed data = optionalData.get().copy();
|
||||
if (data.isInvalid()) {
|
||||
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
data.edit(plugin, (unpacked -> {
|
||||
unpacked.getHealth().ifPresent(status -> status.setHealth(Math.max(1, status.getHealth())));
|
||||
unpacked.setSaveCause(DataSnapshot.SaveCause.BACKUP_RESTORE);
|
||||
unpacked.setPinned(
|
||||
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.BACKUP_RESTORE)
|
||||
);
|
||||
}));
|
||||
|
||||
// Save data
|
||||
final RedisManager redis = plugin.getRedisManager();
|
||||
plugin.getDataSyncer().saveData(user, data, (u, s) -> {
|
||||
redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s));
|
||||
redis.sendUserDataUpdate(u, s);
|
||||
plugin.getLocales().getLocale("data_restored", u.getName(), u.getUuid().toString(),
|
||||
s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage);
|
||||
});
|
||||
}
|
||||
|
||||
// Pin a snapshot
|
||||
private void pinSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
||||
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, version);
|
||||
if (optionalData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pin or unpin the data
|
||||
final DataSnapshot.Packed data = optionalData.get();
|
||||
if (data.isPinned()) {
|
||||
plugin.getDatabase().unpinSnapshot(user, data.getId());
|
||||
} else {
|
||||
plugin.getDatabase().pinSnapshot(user, data.getId());
|
||||
}
|
||||
plugin.getLocales().getLocale(data.isPinned() ? "data_unpinned" : "data_pinned", data.getShortId(),
|
||||
data.getId().toString(), user.getName(), user.getUuid().toString())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
|
||||
// Lookup a snapshot by UUID and dump
|
||||
private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version,
|
||||
@NotNull DumpType type) {
|
||||
final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, version);
|
||||
if (data.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
this.dumpSnapshot(executor, user, data.get(), type);
|
||||
}
|
||||
|
||||
// Dump a snapshot
|
||||
private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user,
|
||||
@NotNull DataSnapshot.Packed userData, @NotNull DumpType type) {
|
||||
final UserDataDumper dumper = UserDataDumper.create(userData, user, plugin);
|
||||
try {
|
||||
final String url = type == DumpType.WEB ? dumper.toWeb() : dumper.toFile();
|
||||
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getName())
|
||||
.ifPresent(executor::sendMessage);
|
||||
executor.sendMessage(Component.text(url)
|
||||
.clickEvent(type == DumpType.WEB ? ClickEvent.openUrl(url) : ClickEvent.copyToClipboard(url))
|
||||
.decorate(TextDecoration.UNDERLINED).color(NamedTextColor.GRAY));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "Failed to dump user data", e);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider view() {
|
||||
return (sub) -> {
|
||||
sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final UUID version = ctx.getArgument("version", UUID.class);
|
||||
viewSnapshot(user(sub, ctx), user, version);
|
||||
}, user("username"), versionUuid());
|
||||
sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
viewLatestSnapshot(user(sub, ctx), user);
|
||||
}, user("username"));
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider list() {
|
||||
return (sub) -> {
|
||||
sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
listSnapshots(user(sub, ctx), user, 1);
|
||||
}, user("username"));
|
||||
sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final int page = ctx.getArgument("page", Integer.class);
|
||||
listSnapshots(user(sub, ctx), user, page);
|
||||
}, user("username"), BaseCommand.intNum("page", 1));
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider delete() {
|
||||
return (sub) -> sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final UUID version = ctx.getArgument("version", UUID.class);
|
||||
deleteSnapshot(user(sub, ctx), user, version);
|
||||
}, user("username"), versionUuid());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider save() {
|
||||
return (sub) -> sub.addSyntax((ctx) -> {
|
||||
final OnlineUser user = ctx.getArgument("username", OnlineUser.class);
|
||||
createAndSaveSnapshot(user(sub, ctx), user);
|
||||
}, onlineUser("username"));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider restore() {
|
||||
return (sub) -> sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final UUID version = ctx.getArgument("version", UUID.class);
|
||||
restoreSnapshot(user(sub, ctx), user, version);
|
||||
}, user("username"), versionUuid());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider pin() {
|
||||
return (sub) -> sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final UUID version = ctx.getArgument("version", UUID.class);
|
||||
pinSnapshot(user(sub, ctx), user, version);
|
||||
}, user("username"), versionUuid());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider dump() {
|
||||
return (sub) -> {
|
||||
sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final CommandUser executor = user(sub, ctx);
|
||||
plugin.getRedisManager()
|
||||
.getOnlineUserData(UUID.randomUUID(), user, DataSnapshot.SaveCause.DUMP_COMMAND)
|
||||
.thenAccept((data) -> data
|
||||
.or(() -> plugin.getDatabase().getLatestSnapshot(user))
|
||||
.ifPresentOrElse(
|
||||
(s) -> dumpSnapshot(executor, user, s, DumpType.WEB),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(executor::sendMessage)
|
||||
));
|
||||
}, user("username"));
|
||||
sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final UUID version = ctx.getArgument("version", UUID.class);
|
||||
final DumpType type = ctx.getArgument("type", DumpType.class);
|
||||
dumpSnapshot(user(sub, ctx), user, version, type);
|
||||
}, user("username"), versionUuid(), dumpType());
|
||||
};
|
||||
}
|
||||
|
||||
private <S> ArgumentElement<S, DumpType> dumpType() {
|
||||
return new ArgumentElement<>("type", reader -> {
|
||||
final String type = reader.readString();
|
||||
return switch (type.toLowerCase(Locale.ENGLISH)) {
|
||||
case "web" -> DumpType.WEB;
|
||||
case "file" -> DumpType.FILE;
|
||||
default -> throw CommandSyntaxException.BUILT_IN_EXCEPTIONS
|
||||
.dispatcherUnknownArgument().createWithContext(reader);
|
||||
};
|
||||
}, (context, builder) -> {
|
||||
builder.suggest("web");
|
||||
builder.suggest("file");
|
||||
return builder.buildFuture();
|
||||
});
|
||||
}
|
||||
|
||||
enum DumpType {
|
||||
WEB,
|
||||
FILE
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user