mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-22 08:09:20 +00:00
Compare commits
308 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 |
66
.github/workflows/ci.yml
vendored
66
.github/workflows/ci.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: CI Tests
|
||||
name: CI Tests & Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -17,11 +17,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Set up JDK 17 📦'
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Set up JDK 21 📦'
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
- name: 'Build with Gradle 🏗️'
|
||||
uses: gradle/gradle-build-action@v3
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
uses: mikepenz/action-junit-report@v6
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
@@ -41,4 +41,56 @@ jobs:
|
||||
id: fetch-version
|
||||
- name: Get Version
|
||||
run: |
|
||||
echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV
|
||||
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
|
||||
10
.github/workflows/pr_tests.yml
vendored
10
.github/workflows/pr_tests.yml
vendored
@@ -13,18 +13,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout for CI 🛎'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Set up JDK 17 📦'
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Set up JDK 21 📦'
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
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@v4
|
||||
uses: mikepenz/action-junit-report@v6
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
62
.github/workflows/release.yml
vendored
62
.github/workflows/release.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Release Tests
|
||||
name: Release Tests & Publish
|
||||
|
||||
on:
|
||||
release:
|
||||
@@ -13,11 +13,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Set up JDK 17 📦'
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Set up JDK 21 📦'
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
- name: 'Build with Gradle 🏗️'
|
||||
uses: gradle/gradle-build-action@v3
|
||||
@@ -27,7 +27,55 @@ jobs:
|
||||
RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
RELEASES_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
uses: mikepenz/action-junit-report@v6
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
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
|
||||
7
.github/workflows/update_docs.yml
vendored
7
.github/workflows/update_docs.yml
vendored
@@ -13,12 +13,13 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deploy-wiki:
|
||||
update-docs:
|
||||
name: 'Update Docs'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Push Docs to Github Wiki 📄️'
|
||||
uses: Andrew-Chen-Wang/github-wiki-action@v4
|
||||
uses: Andrew-Chen-Wang/github-wiki-action@v5
|
||||
with:
|
||||
path: 'docs'
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -121,5 +121,4 @@ run/
|
||||
|
||||
# Don't include generated test suite files
|
||||
/test/servers/
|
||||
/test/HuskSync
|
||||
/test/config.yml
|
||||
/test/HuskSync
|
||||
33
README.md
33
README.md
@@ -43,8 +43,33 @@
|
||||
|
||||
**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
|
||||
Requires a MySQL/Mongo/PostgreSQL database, a Redis (v5.0+) server and a network of Spigot (1.17.1+) or Fabric (1.20.1) Minecraft servers, running Java 17+.
|
||||
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).
|
||||
|
||||
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.
|
||||
@@ -52,12 +77,14 @@ Requires a MySQL/Mongo/PostgreSQL database, a Redis (v5.0+) server and a network
|
||||
4. Start every server again and synchronization will begin.
|
||||
|
||||
## Development
|
||||
To build HuskSync, simply run the following in the root of the repository (building requires Java 17). Builds will be output in `/target`:
|
||||
To build HuskSync, simply run the following in the root of the repository (building requires Java 21). Builds will be output in `/target`:
|
||||
|
||||
```bash
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### License
|
||||
HuskSync is licensed under the Apache 2.0 license.
|
||||
|
||||
@@ -82,4 +109,4 @@ Translations of the plugin locales are welcome to help make the plugin more acce
|
||||
- [bStats](https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140) — View plugin metrics
|
||||
|
||||
---
|
||||
© [William278](https://william278.net/), 2023. Licensed under the Apache-2.0 License.
|
||||
© [William278](https://william278.net/), 2025. Licensed under the Apache-2.0 License.
|
||||
|
||||
82
build.gradle
82
build.gradle
@@ -1,10 +1,11 @@
|
||||
import org.apache.tools.ant.filters.ReplaceTokens
|
||||
|
||||
plugins {
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||
id 'com.gradleup.shadow' version '9.2.2'
|
||||
id 'org.cadixdev.licenser' version '0.6.1' apply false
|
||||
id 'fabric-loom' version '1.7-SNAPSHOT' apply false
|
||||
id 'org.ajoberstar.grgit' version '5.2.2'
|
||||
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'
|
||||
}
|
||||
@@ -58,7 +59,12 @@ publishing {
|
||||
}
|
||||
|
||||
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'
|
||||
|
||||
@@ -73,7 +79,7 @@ allprojects {
|
||||
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.dmulloy2.net/repository/public/" }
|
||||
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.alessiodp.com/releases/' }
|
||||
@@ -83,13 +89,10 @@ allprojects {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
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 {
|
||||
@@ -98,21 +101,45 @@ allprojects {
|
||||
newLine = true
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
processResources {
|
||||
def tokenMap = rootProject.ext.properties
|
||||
tokenMap.merge("grgit", '', (s, s2) -> s)
|
||||
filesMatching(['**/*.json', '**/*.yml']) {
|
||||
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
|
||||
tokens: rootProject.ext.properties
|
||||
tokens: tokenMap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
if (['fabric'].contains(project.name)) {
|
||||
apply plugin: 'fabric-loom'
|
||||
// Ignore parent projects (no jars)
|
||||
if (['fabric', 'bukkit'].contains(project.name)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Project naming
|
||||
version rootProject.version
|
||||
archivesBaseName = "${rootProject.name}-${project.name.capitalize()}"
|
||||
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'
|
||||
@@ -123,13 +150,8 @@ subprojects {
|
||||
archiveClassifier.set('')
|
||||
}
|
||||
|
||||
// Append the Minecraft to the version for Fabric projects
|
||||
if (project.name == 'fabric') {
|
||||
version += "+mc.${fabric_minecraft_version}"
|
||||
}
|
||||
|
||||
// API publishing
|
||||
if (['common', 'bukkit', 'fabric'].contains(project.name)) {
|
||||
if (project.name == 'common' || ['fabric', 'bukkit'].contains(project.parent?.name)) {
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
@@ -156,12 +178,12 @@ subprojects {
|
||||
}
|
||||
}
|
||||
|
||||
if (['bukkit'].contains(project.name)) {
|
||||
if (project.parent?.name?.equals('bukkit')) {
|
||||
publications {
|
||||
mavenJavaBukkit(MavenPublication) {
|
||||
"mavenJavaBukkit_${project.name.replace('.', '_')}"(MavenPublication) {
|
||||
groupId = 'net.william278.husksync'
|
||||
artifactId = 'husksync-bukkit'
|
||||
version = "$rootProject.version"
|
||||
version = "$rootProject.version+$project.name"
|
||||
artifact shadowJar
|
||||
artifact sourcesJar
|
||||
artifact javadocJar
|
||||
@@ -169,12 +191,12 @@ subprojects {
|
||||
}
|
||||
}
|
||||
|
||||
if (['fabric'].contains(project.name)) {
|
||||
if (project.parent?.name?.equals('fabric')) {
|
||||
publications {
|
||||
mavenJavaFabric(MavenPublication) {
|
||||
"mavenJavaFabric_${project.name.replace('.', '_')}"(MavenPublication) {
|
||||
groupId = 'net.william278.husksync'
|
||||
artifactId = 'husksync-fabric'
|
||||
version = "$rootProject.version+${fabric_minecraft_version}"
|
||||
version = "$rootProject.version+$project.name"
|
||||
artifact remapJar
|
||||
artifact sourcesJar
|
||||
artifact javadocJar
|
||||
@@ -184,7 +206,7 @@ subprojects {
|
||||
}
|
||||
}
|
||||
|
||||
jar.dependsOn(shadowJar)
|
||||
jar.dependsOn shadowJar
|
||||
clean.delete "$rootDir/target"
|
||||
}
|
||||
|
||||
@@ -199,7 +221,7 @@ def versionMetadata() {
|
||||
|
||||
// If unclean, return the last commit hash with -indev
|
||||
if (!grgit.status().clean) {
|
||||
return '-' + grgit.head().abbreviatedId + '-indev'
|
||||
return '-' + grgit.head().abbreviatedId + '-indev'
|
||||
}
|
||||
|
||||
// Otherwise if this matches a tag, return nothing
|
||||
|
||||
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,37 +1,68 @@
|
||||
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 'net.william278.uniform:uniform-bukkit:1.1.5'
|
||||
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:1.0.3'
|
||||
implementation 'net.william278:andjam:1.0.2'
|
||||
implementation 'org.bstats:bstats-bukkit:3.0.2'
|
||||
implementation 'net.kyori:adventure-platform-bukkit:4.3.3'
|
||||
implementation 'dev.triumphteam:triumph-gui:3.1.10'
|
||||
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.13.1-SNAPSHOT'
|
||||
implementation 'de.tr7zw:item-nbt-api:2.15.5'
|
||||
|
||||
compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT'
|
||||
compileOnly 'com.github.retrooper.packetevents:spigot:2.3.0'
|
||||
compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||
compileOnly 'commons-io:commons-io:2.16.1'
|
||||
compileOnly 'org.json:json:20240303'
|
||||
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.5.0'
|
||||
compileOnly 'com.zaxxer:HikariCP:5.1.0'
|
||||
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.32'
|
||||
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'
|
||||
@@ -43,10 +74,10 @@ shadowJar {
|
||||
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.andjam', 'net.william278.husksync.libraries.andjam'
|
||||
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'
|
||||
@@ -58,4 +89,14 @@ shadowJar {
|
||||
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
@@ -23,6 +23,7 @@ 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;
|
||||
@@ -46,6 +47,8 @@ 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;
|
||||
@@ -54,9 +57,10 @@ 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.BukkitMapPersister;
|
||||
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;
|
||||
@@ -78,8 +82,9 @@ import java.util.stream.Collectors;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("unchecked")
|
||||
public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier,
|
||||
BukkitEventDispatcher, BukkitMapPersister {
|
||||
BukkitEventDispatcher, BukkitMapHandler {
|
||||
|
||||
/**
|
||||
* Metrics ID for <a href="https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140">HuskSync on Bukkit</a>.
|
||||
@@ -87,17 +92,17 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
private static final int METRICS_ID = 13140;
|
||||
private static final String PLATFORM_TYPE_ID = "bukkit";
|
||||
|
||||
private final TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap(
|
||||
SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR
|
||||
);
|
||||
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;
|
||||
@@ -127,6 +132,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
loadSettings();
|
||||
loadLocales();
|
||||
loadServer();
|
||||
validateConfigFiles();
|
||||
});
|
||||
|
||||
this.eventListener = createEventListener();
|
||||
@@ -136,6 +142,16 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
@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)));
|
||||
@@ -290,7 +306,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
@Override
|
||||
public boolean isDependencyLoaded(@NotNull String name) {
|
||||
final Plugin plugin = getServer().getPluginManager().getPlugin(name);
|
||||
return plugin != null && plugin.isEnabled();
|
||||
return plugin != null;
|
||||
}
|
||||
|
||||
// Register bStats metrics
|
||||
@@ -333,11 +349,23 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
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();
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
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;
|
||||
@@ -29,7 +30,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@SuppressWarnings({"unused"})
|
||||
public class PaperHuskSync extends BukkitHuskSync {
|
||||
|
||||
@NotNull
|
||||
@@ -45,6 +46,12 @@ public class PaperHuskSync extends BukkitHuskSync {
|
||||
return player == null || !player.isOnline() ? Audience.empty() : player;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Version getMinecraftVersion() {
|
||||
return Version.fromString(getServer().getMinecraftVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Uniform getUniform() {
|
||||
@@ -34,6 +34,7 @@ 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")
|
||||
@@ -46,13 +47,20 @@ public class PaperHuskSyncLoader implements PluginLoader {
|
||||
resolveLibraries(classpathBuilder).stream()
|
||||
.map(DefaultArtifact::new)
|
||||
.forEach(artifact -> resolver.addDependency(new Dependency(artifact, null)));
|
||||
resolver.addRepository(new RemoteRepository.Builder(
|
||||
"maven", "default", "https://repo.maven.apache.org/maven2/"
|
||||
).build());
|
||||
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()) {
|
||||
@@ -62,7 +62,7 @@ public class BukkitHuskSyncAPI extends HuskSyncAPI {
|
||||
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.");
|
||||
"and need to fix your maven/gradle/build script so that it *compiles against* HuskSync instead.");
|
||||
}
|
||||
if (instance == null) {
|
||||
throw new NotRegisteredException();
|
||||
|
||||
@@ -26,28 +26,28 @@ import de.tr7zw.changeme.nbtapi.NBTCompound;
|
||||
import de.tr7zw.changeme.nbtapi.NBTPersistentDataContainer;
|
||||
import lombok.*;
|
||||
import net.william278.desertwell.util.ThrowingConsumer;
|
||||
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.config.Settings.SynchronizationSettings.AttributeSettings;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.Statistic;
|
||||
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.EquipmentSlot;
|
||||
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;
|
||||
@@ -156,20 +156,17 @@ public abstract class BukkitData implements Data {
|
||||
this.clearInventoryCraftingSlots(player);
|
||||
player.setItemOnCursor(null);
|
||||
player.getInventory().setContents(plugin.setMapViews(getContents()));
|
||||
player.updateInventory();
|
||||
player.getInventory().setHeldItemSlot(heldItemSlot);
|
||||
//noinspection UnstableApiUsage
|
||||
player.updateInventory();
|
||||
}
|
||||
|
||||
private void clearInventoryCraftingSlots(@NotNull Player player) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
// Ignore any exceptions
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +195,9 @@ public abstract class BukkitData implements Data {
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
user.getPlayer().getEnderChest().setContents(plugin.setMapViews(getContents()));
|
||||
ItemStack[] fullContents = plugin.setMapViews(getContents());
|
||||
ItemStack[] enderChestContents = Arrays.copyOf(fullContents, Math.min(fullContents.length, user.getPlayer().getEnderChest().getSize()));
|
||||
user.getPlayer().getEnderChest().setContents(enderChestContents);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -236,8 +235,9 @@ public abstract class BukkitData implements Data {
|
||||
private final Collection<PotionEffect> effects;
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> effects) {
|
||||
return new BukkitData.PotionEffects(effects);
|
||||
public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> sei) {
|
||||
return new BukkitData.PotionEffects(Lists.newArrayList(sei.stream().filter(e -> !e.isAmbient()).toList()));
|
||||
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -261,7 +261,7 @@ public abstract class BukkitData implements Data {
|
||||
@NotNull
|
||||
@SuppressWarnings("unused")
|
||||
public static BukkitData.PotionEffects empty() {
|
||||
return new BukkitData.PotionEffects(List.of());
|
||||
return new BukkitData.PotionEffects(Lists.newArrayList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -277,10 +277,11 @@ public abstract class BukkitData implements Data {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
@Unmodifiable
|
||||
public List<Effect> getActiveEffects() {
|
||||
return effects.stream()
|
||||
.map(potionEffect -> new Effect(
|
||||
potionEffect.getType().getName().toLowerCase(Locale.ENGLISH),
|
||||
potionEffect.getType().getKey().toString(),
|
||||
potionEffect.getAmplifier(),
|
||||
potionEffect.getDuration(),
|
||||
potionEffect.isAmbient(),
|
||||
@@ -455,9 +456,10 @@ public abstract class BukkitData implements Data {
|
||||
Registry.STATISTIC.forEach(id -> {
|
||||
switch (id.getType()) {
|
||||
case UNTYPED -> addStatistic(player, id, generic);
|
||||
case BLOCK -> addMaterialStatistic(player, id, blocks, true);
|
||||
case ITEM -> addMaterialStatistic(player, id, items, false);
|
||||
case ENTITY -> addEntityStatistic(player, id, entities);
|
||||
// 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);
|
||||
@@ -478,43 +480,36 @@ public abstract class BukkitData implements Data {
|
||||
}
|
||||
}
|
||||
|
||||
private static void addMaterialStatistic(@NotNull Player p, @NotNull Statistic id,
|
||||
@NotNull Map<String, Map<String, Integer>> map, boolean isBlock) {
|
||||
Registry.MATERIAL.forEach(material -> {
|
||||
if ((material.isBlock() && !isBlock) || (material.isItem() && isBlock)) {
|
||||
return;
|
||||
}
|
||||
final int stat = p.getStatistic(id, material);
|
||||
if (stat != 0) {
|
||||
map.computeIfAbsent(id.getKey().getKey(), k -> Maps.newHashMap())
|
||||
.put(material.getKey().getKey(), stat);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void addEntityStatistic(@NotNull Player p, @NotNull Statistic id,
|
||||
@NotNull Map<String, Map<String, Integer>> map) {
|
||||
Registry.ENTITY_TYPE.forEach(entity -> {
|
||||
if (!entity.isAlive()) {
|
||||
return;
|
||||
}
|
||||
final int stat = p.getStatistic(id, entity);
|
||||
if (stat != 0) {
|
||||
map.computeIfAbsent(id.getKey().getKey(), k -> Maps.newHashMap())
|
||||
.put(entity.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 plugin) {
|
||||
genericStatistics.forEach((id, v) -> applyStat(user, id, Statistic.Type.UNTYPED, v));
|
||||
blockStatistics.forEach((id, m) -> m.forEach((b, v) -> applyStat(user, id, Statistic.Type.BLOCK, v, b)));
|
||||
itemStatistics.forEach((id, m) -> m.forEach((i, v) -> applyStat(user, id, Statistic.Type.ITEM, v, i)));
|
||||
entityStatistics.forEach((id, m) -> m.forEach((e, v) -> applyStat(user, id, Statistic.Type.ENTITY, v, e)));
|
||||
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 UserDataHolder user, @NotNull String id,
|
||||
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);
|
||||
@@ -525,10 +520,21 @@ public abstract class BukkitData implements Data {
|
||||
try {
|
||||
switch (type) {
|
||||
case UNTYPED -> player.setStatistic(stat, value);
|
||||
case BLOCK, ITEM -> player.setStatistic(stat, Objects.requireNonNull(matchMaterial(key[0])), value);
|
||||
case ENTITY -> player.setStatistic(stat, Objects.requireNonNull(matchEntityType(key[0])), 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 ignored) {
|
||||
} catch (Throwable a) {
|
||||
plugin.log(Level.WARNING, "Failed to apply statistic " + id, a);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,21 +569,29 @@ public abstract class BukkitData implements Data {
|
||||
@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 (instance == null || instance.getValue() == instance.getDefaultValue() || plugin
|
||||
.getSettings().getSynchronization().isIgnoredAttribute(id.getKey().toString())) {
|
||||
// We don't sync unmodified or disabled attributes
|
||||
return;
|
||||
if (settings.isIgnoredAttribute(id.getKey().toString()) || instance == null) {
|
||||
return; // We don't sync attributes not marked as to be synced
|
||||
}
|
||||
attributes.add(adapt(instance, plugin.getMinecraftVersion()));
|
||||
attributes.add(adapt(instance, settings));
|
||||
});
|
||||
return new BukkitData.Attributes(attributes);
|
||||
}
|
||||
@@ -596,47 +610,72 @@ public abstract class BukkitData implements Data {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Attribute adapt(@NotNull AttributeInstance instance, @NotNull Version version) {
|
||||
private static Attribute adapt(@NotNull AttributeInstance instance, @NotNull AttributeSettings settings) {
|
||||
return new Attribute(
|
||||
instance.getAttribute().getKey().toString(),
|
||||
instance.getBaseValue(),
|
||||
instance.getModifiers().stream().map(m -> adapt(m, version)).collect(Collectors.toSet())
|
||||
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, @NotNull Version version) {
|
||||
private static Modifier adapt(@NotNull AttributeModifier modifier) {
|
||||
return new Modifier(
|
||||
version.compareTo(Version.fromString("1.21")) >= 0 ? null : modifier.getUniqueId(),
|
||||
modifier.getName(),
|
||||
modifier.getKey().toString(),
|
||||
modifier.getAmount(),
|
||||
modifier.getOperation().ordinal(),
|
||||
modifier.getSlot() != null ? modifier.getSlot().ordinal() : -1
|
||||
modifier.getSlotGroup().toString()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
Registry.ATTRIBUTE.forEach(id -> applyAttribute(user.getPlayer().getAttribute(id), getAttribute(id).orElse(null)));
|
||||
}
|
||||
|
||||
private static void applyAttribute(@Nullable AttributeInstance instance, @Nullable Attribute attribute) {
|
||||
if (instance == null) {
|
||||
return;
|
||||
}
|
||||
instance.setBaseValue(attribute == null ? instance.getDefaultValue() : attribute.baseValue());
|
||||
instance.getModifiers().forEach(instance::removeModifier);
|
||||
instance.setBaseValue(attribute == null ? instance.getValue() : attribute.baseValue());
|
||||
if (attribute != null) {
|
||||
attribute.modifiers().forEach(modifier -> instance.addModifier(new AttributeModifier(
|
||||
modifier.uuid(),
|
||||
modifier.name(),
|
||||
modifier.amount(),
|
||||
AttributeModifier.Operation.values()[modifier.operationType()],
|
||||
modifier.equipmentSlot() != -1 ? EquipmentSlot.values()[modifier.equipmentSlot()] : 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
|
||||
@@ -696,11 +735,12 @@ public abstract class BukkitData implements Data {
|
||||
}
|
||||
|
||||
// Set health scale
|
||||
double scale = healthScale <= 0 ? player.getMaxHealth() : healthScale;
|
||||
try {
|
||||
player.setHealthScale(healthScale);
|
||||
player.setHealthScale(scale);
|
||||
player.setHealthScaled(isHealthScaled);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.WARNING, "Error setting %s's health scale to %s".formatted(player.getName(), healthScale), e);
|
||||
plugin.log(Level.WARNING, "Error setting %s's health scale to %s".formatted(player.getName(), scale), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -153,20 +153,11 @@ public class BukkitSerializer {
|
||||
@NotNull
|
||||
private ReadWriteNBT upgradeItemData(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion)
|
||||
throws NoSuchFieldException, IllegalAccessException {
|
||||
return DataFixerUtil.fixUpItemData(tag, getDataVersion(mcVersion), DataFixerUtil.getCurrentVersion());
|
||||
}
|
||||
|
||||
private int getDataVersion(@NotNull Version mcVersion) {
|
||||
return switch (mcVersion.toStringWithoutMetadata()) {
|
||||
case "1.16", "1.16.1", "1.16.2", "1.16.3", "1.16.4", "1.16.5" -> DataFixerUtil.VERSION1_16_5;
|
||||
case "1.17", "1.17.1" -> DataFixerUtil.VERSION1_17_1;
|
||||
case "1.18", "1.18.1", "1.18.2" -> DataFixerUtil.VERSION1_18_2;
|
||||
case "1.19", "1.19.1", "1.19.2" -> DataFixerUtil.VERSION1_19_2;
|
||||
case "1.20", "1.20.1", "1.20.2" -> DataFixerUtil.VERSION1_20_2;
|
||||
case "1.20.3", "1.20.4" -> DataFixerUtil.VERSION1_20_4;
|
||||
case "1.20.5", "1.20.6" -> DataFixerUtil.VERSION1_20_5;
|
||||
default -> DataFixerUtil.getCurrentVersion();
|
||||
};
|
||||
return DataFixerUtil.fixUpItemData(
|
||||
tag,
|
||||
getPlugin().getDataVersion(mcVersion),
|
||||
DataFixerUtil.getCurrentVersion()
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -228,7 +219,7 @@ public class BukkitSerializer {
|
||||
|
||||
@Override
|
||||
public BukkitData.PersistentData deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
return BukkitData.PersistentData.from(new NBTContainer(serialized));
|
||||
return BukkitData.PersistentData.from((NBTContainer) NBT.parseNBT(serialized));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.util.BukkitMapPersister;
|
||||
import net.william278.husksync.maps.BukkitMapHandler;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -31,7 +31,11 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
|
||||
@Override
|
||||
default Optional<? extends Data> getData(@NotNull Identifier id) {
|
||||
if (!id.isCustom()) {
|
||||
if (id.isCustom()) {
|
||||
return Optional.ofNullable(getCustomDataStore().get(id));
|
||||
}
|
||||
|
||||
try {
|
||||
return switch (id.getKeyValue()) {
|
||||
case "inventory" -> getInventory();
|
||||
case "ender_chest" -> getEnderChest();
|
||||
@@ -48,8 +52,10 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
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();
|
||||
}
|
||||
return Optional.ofNullable(getCustomDataStore().get(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -163,7 +169,7 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default BukkitMapPersister getMapPersister() {
|
||||
default BukkitMapHandler getMapPersister() {
|
||||
return (BukkitHuskSync) getPlugin();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
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;
|
||||
@@ -36,6 +37,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Getter
|
||||
public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
|
||||
BukkitDeathEventListener, Listener {
|
||||
|
||||
@@ -133,7 +135,7 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onMapInitialize(@NotNull MapInitializeEvent event) {
|
||||
if (plugin.getSettings().getSynchronization().isPersistLockedMaps() && event.getMap().isLocked()) {
|
||||
getPlugin().runAsync(() -> ((BukkitHuskSync) plugin).renderMapFromFile(event.getMap()));
|
||||
getPlugin().runAsync(() -> ((BukkitHuskSync) plugin).renderInitializingLockedMap(event.getMap()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@@ -124,7 +123,6 @@ public class BukkitLockedEventListener implements LockedHandler, Listener {
|
||||
private void cancelPlayerEvent(@NotNull UUID uuid, @NotNull Cancellable event) {
|
||||
if (cancelPlayerEvent(uuid)) {
|
||||
event.setCancelled(true);
|
||||
plugin.debug("Cancelled event " + event.getClass().getSimpleName() + " from " + Objects.requireNonNull(plugin.getServer().getPlayer(uuid)).getName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ 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;
|
||||
@@ -39,12 +40,11 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public void onLoad() {
|
||||
super.onLoad();
|
||||
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(getPlugin()));
|
||||
PacketEvents.getAPI().getSettings().reEncodeByDefault(false)
|
||||
.checkForUpdates(false)
|
||||
.bStats(true);
|
||||
PacketEvents.getAPI().getSettings().reEncodeByDefault(false).checkForUpdates(false);
|
||||
PacketEvents.getAPI().load();
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
|
||||
|
||||
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
|
||||
@@ -78,7 +79,20 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
|
||||
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
if(!(event.getPacketType() instanceof PacketType.Play.Client client)) {
|
||||
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)) {
|
||||
|
||||
@@ -49,7 +49,7 @@ public class BukkitProtocolLibLockedPacketListener extends BukkitLockedEventList
|
||||
|
||||
private static class PlayerPacketAdapter extends PacketAdapter {
|
||||
|
||||
// Packets we want the player to still be able to send/receiver to/from the server
|
||||
// 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
|
||||
|
||||
@@ -43,6 +43,13 @@ public class PaperEventListener extends BukkitEventListener {
|
||||
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
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -146,7 +146,7 @@ public class LegacyMigrator extends Migrator {
|
||||
try {
|
||||
plugin.getDatabase().addSnapshot(data.user(), convertedData);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().getUsername() + ": " + e.getMessage());
|
||||
plugin.log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().getName() + ": " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -204,10 +204,10 @@ public class LegacyMigrator extends Migrator {
|
||||
}) {
|
||||
plugin.log(Level.INFO, getHelpMenu());
|
||||
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]));
|
||||
obfuscateDataString(args[1]));
|
||||
} else {
|
||||
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||
}
|
||||
} else {
|
||||
plugin.log(Level.INFO, getHelpMenu());
|
||||
|
||||
@@ -201,10 +201,10 @@ public class MpdbMigrator extends Migrator {
|
||||
}) {
|
||||
plugin.log(Level.INFO, getHelpMenu());
|
||||
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]));
|
||||
obfuscateDataString(args[1]));
|
||||
} else {
|
||||
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||
}
|
||||
} else {
|
||||
plugin.log(Level.INFO, getHelpMenu());
|
||||
@@ -255,7 +255,7 @@ public class MpdbMigrator extends Migrator {
|
||||
If any of these are not correct, please correct them
|
||||
using the command:
|
||||
"husksync migrate mpdb set <parameter> <value>"
|
||||
(e.g.: "husksync migrate mpdb set host 1.2.3.4")
|
||||
(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
|
||||
@@ -263,7 +263,7 @@ public class MpdbMigrator extends Migrator {
|
||||
before proceeding.
|
||||
|
||||
STEP 4] To start the migration, please run:
|
||||
"husksync migrate mpdb start"
|
||||
"husksync migrate start mpdb"
|
||||
|
||||
NOTE: This migrator currently WORKS WITH MPDB version
|
||||
v4.9.2 and below!
|
||||
|
||||
@@ -23,14 +23,10 @@ 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.roxeez.advancement.display.FrameType;
|
||||
import net.william278.andjam.Toast;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
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.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -40,8 +36,6 @@ import java.util.Arrays;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static net.william278.husksync.util.BukkitKeyedAdapter.matchMaterial;
|
||||
|
||||
/**
|
||||
* Bukkit platform implementation of an {@link OnlineUser}
|
||||
*/
|
||||
@@ -63,25 +57,18 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOffline() {
|
||||
return player == null || !player.isOnline();
|
||||
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) {
|
||||
try {
|
||||
final Material material = matchMaterial(iconMaterial);
|
||||
Toast.builder((BukkitHuskSync) plugin)
|
||||
.setTitle(title.toComponent())
|
||||
.setDescription(description.toComponent())
|
||||
.setIcon(material != null ? material : Material.BARRIER)
|
||||
.setFrameType(FrameType.valueOf(backgroundType))
|
||||
.build()
|
||||
.show(player);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.WARNING, "Failed to send toast to player " + player.getName(), e);
|
||||
}
|
||||
plugin.log(Level.WARNING, "Toast notifications are deprecated. " +
|
||||
"Please change your notification display slot to CHAT, ACTION_BAR or NONE.");
|
||||
this.sendActionBar(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -92,7 +79,7 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
||||
if (!editable) {
|
||||
builder.disableAllInteractions();
|
||||
}
|
||||
final StorageGui gui = builder.enableOtherActions()
|
||||
final StorageGui gui = builder
|
||||
.apply(a -> a.getInventory().setContents(contents))
|
||||
.title(title.toComponent()).create();
|
||||
gui.setCloseGuiAction((close) -> onClose.accept(BukkitData.Items.ItemArray.adapt(
|
||||
|
||||
@@ -51,7 +51,7 @@ public final class BukkitKeyedAdapter {
|
||||
|
||||
@Nullable
|
||||
public static PotionEffectType matchEffectType(@NotNull String key) {
|
||||
return PotionEffectType.getByName(key); // No registry for this in 1.17 API
|
||||
return getRegistryValue(Registry.EFFECT, key);
|
||||
}
|
||||
|
||||
private static <T extends Keyed> T getRegistryValue(@NotNull Registry<T> registry, @NotNull String keyString) {
|
||||
|
||||
@@ -1,452 +0,0 @@
|
||||
/*
|
||||
* 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 de.tr7zw.changeme.nbtapi.NBT;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadableNBT;
|
||||
import net.querz.nbt.io.NBTUtil;
|
||||
import net.querz.nbt.tag.CompoundTag;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
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.ShulkerBox;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.BlockStateMeta;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.map.*;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public interface BukkitMapPersister {
|
||||
|
||||
// The map used to store HuskSync data in ItemStack NBT
|
||||
String MAP_DATA_KEY = "husksync:persisted_locked_map";
|
||||
// The key used to store the serialized map data in NBT
|
||||
String MAP_PIXEL_DATA_KEY = "canvas_data";
|
||||
// The key used to store the map of World UIDs to MapView IDs in NBT
|
||||
String MAP_VIEW_ID_MAPPINGS_KEY = "id_mappings";
|
||||
|
||||
/**
|
||||
* 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 ShulkerBox box) {
|
||||
forEachMap(box.getInventory().getContents(), function);
|
||||
b.setBlockState(box);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@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 PersistentMapCanvas canvas = new PersistentMapCanvas(view);
|
||||
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 worldUid = view.getWorld().getUID().toString();
|
||||
mapData.setByteArray(MAP_PIXEL_DATA_KEY, canvas.extractMapData().toBytes());
|
||||
nbt.getOrCreateCompound(MAP_VIEW_ID_MAPPINGS_KEY).setInteger(worldUid, view.getId());
|
||||
getPlugin().debug(String.format("Saved data for locked map (#%s, UID: %s)", view.getId(), worldUid));
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
@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);
|
||||
final ReadableNBT mapIds = nbt.getCompound(MAP_VIEW_ID_MAPPINGS_KEY);
|
||||
if (mapData == null || mapIds == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Search for an existing map view
|
||||
Optional<String> world = Optional.empty();
|
||||
for (String worldUid : mapIds.getKeys()) {
|
||||
world = getPlugin().getServer().getWorlds().stream()
|
||||
.map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid))
|
||||
.findFirst();
|
||||
if (world.isPresent()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (world.isPresent()) {
|
||||
final String uid = world.get();
|
||||
final Optional<MapView> existingView = this.getMapView(mapIds.getInteger(uid));
|
||||
if (existingView.isPresent()) {
|
||||
final MapView view = existingView.get();
|
||||
view.setLocked(true);
|
||||
meta.setMapView(view);
|
||||
map.setItemMeta(meta);
|
||||
getPlugin().debug(String.format("View exists (#%s); updated map (UID: %s)", view.getId(), uid));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Read the pixel data and generate a map view otherwise
|
||||
final MapData canvasData;
|
||||
try {
|
||||
getPlugin().debug("Deserializing map data from NBT and generating view...");
|
||||
canvasData = MapData.fromByteArray(Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY),
|
||||
"Map pixel data is null"));
|
||||
} catch (Throwable e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to deserialize map data from NBT", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a renderer to the map with the data and save to file
|
||||
final MapView view = generateRenderedMap(canvasData);
|
||||
final String worldUid = getDefaultMapWorld().getUID().toString();
|
||||
meta.setMapView(view);
|
||||
map.setItemMeta(meta);
|
||||
saveMapToFile(canvasData, view.getId());
|
||||
|
||||
// Set the map view ID in NBT
|
||||
NBT.modify(map, editable -> {
|
||||
Objects.requireNonNull(editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY),
|
||||
"Map view ID mappings compound is null")
|
||||
.setInteger(worldUid, view.getId());
|
||||
});
|
||||
getPlugin().debug(String.format("Generated view (#%s) and updated map (UID: %s)", view.getId(), worldUid));
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
default void renderMapFromFile(@NotNull MapView view) {
|
||||
final File mapFile = new File(getMapCacheFolder(), view.getId() + ".dat");
|
||||
if (!mapFile.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final MapData canvasData;
|
||||
try {
|
||||
canvasData = MapData.fromNbt(mapFile);
|
||||
} catch (Throwable e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to deserialize map data from file", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new map view renderer with the map data color at each pixel
|
||||
// use view.removeRenderer() to remove all this maps renderers
|
||||
view.getRenderers().forEach(view::removeRenderer);
|
||||
view.addRenderer(new PersistentMapRenderer(canvasData));
|
||||
view.setLocked(true);
|
||||
view.setScale(MapView.Scale.NORMAL);
|
||||
view.setTrackingPosition(false);
|
||||
view.setUnlimitedTracking(false);
|
||||
|
||||
// Set the view to the map
|
||||
setMapView(view);
|
||||
}
|
||||
|
||||
default void saveMapToFile(@NotNull MapData data, int id) {
|
||||
getPlugin().runAsync(() -> {
|
||||
final File mapFile = new File(getMapCacheFolder(), id + ".dat");
|
||||
if (mapFile.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final CompoundTag rootTag = new CompoundTag();
|
||||
rootTag.put("data", data.toNBT().getTag());
|
||||
NBTUtil.write(rootTag, mapFile);
|
||||
} catch (Throwable e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to serialize map data to file", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private File getMapCacheFolder() {
|
||||
final File mapCache = new File(getPlugin().getDataFolder(), "maps");
|
||||
if (!mapCache.exists() && !mapCache.mkdirs()) {
|
||||
getPlugin().log(Level.WARNING, "Failed to create maps folder");
|
||||
}
|
||||
return mapCache;
|
||||
}
|
||||
|
||||
// Sets the renderer of a map, and returns the generated MapView
|
||||
@NotNull
|
||||
private MapView generateRenderedMap(@NotNull MapData canvasData) {
|
||||
final MapView view = Bukkit.createMap(getDefaultMapWorld());
|
||||
view.getRenderers().clear();
|
||||
|
||||
// Create a new map view renderer with the map data color at each pixel
|
||||
view.addRenderer(new PersistentMapRenderer(canvasData));
|
||||
view.setLocked(true);
|
||||
view.setScale(MapView.Scale.NORMAL);
|
||||
view.setTrackingPosition(false);
|
||||
view.setUnlimitedTracking(false);
|
||||
|
||||
// Set the view to the map and return it
|
||||
setMapView(view);
|
||||
return 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}
|
||||
*/
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link MapCanvas} implementation used for pre-rendering maps to be converted into {@link MapData}
|
||||
*/
|
||||
class PersistentMapCanvas implements MapCanvas {
|
||||
|
||||
private final MapView mapView;
|
||||
private final int[][] pixels = new int[128][128];
|
||||
private MapCursorCollection cursors;
|
||||
|
||||
private PersistentMapCanvas(@NotNull MapView mapView) {
|
||||
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
|
||||
public void setPixel(int x, int y, byte color) {
|
||||
pixels[x][y] = color;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getPixel(int x, int y) {
|
||||
return (byte) pixels[x][y];
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getBasePixel(int x, int y) {
|
||||
return getPixel(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();
|
||||
final String BANNER_PREFIX = "banner_";
|
||||
for (int i = 0; i < getCursors().size(); i++) {
|
||||
final MapCursor cursor = getCursors().getCursor(i);
|
||||
final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH);
|
||||
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(pixels, getDimension(), (byte) 2, banners, List.of());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Map<Integer, MapView> getMapViews();
|
||||
|
||||
@ApiStatus.Internal
|
||||
@NotNull
|
||||
BukkitHuskSync getPlugin();
|
||||
|
||||
}
|
||||
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}'
|
||||
@@ -5,7 +5,7 @@ website: 'https://william278.net/'
|
||||
main: 'net.william278.husksync.PaperHuskSync'
|
||||
loader: 'net.william278.husksync.PaperHuskSyncLoader'
|
||||
version: '${version}'
|
||||
api-version: '1.19'
|
||||
api-version: '${minecraft_api_version}'
|
||||
folia-supported: true
|
||||
dependencies:
|
||||
server:
|
||||
@@ -1,7 +1,7 @@
|
||||
name: 'HuskSync'
|
||||
version: '${version}'
|
||||
main: 'net.william278.husksync.BukkitHuskSync'
|
||||
api-version: 1.17
|
||||
api-version: '${minecraft_api_version}'
|
||||
author: 'William278'
|
||||
description: '${description}'
|
||||
website: 'https://william278.net'
|
||||
|
||||
@@ -3,27 +3,31 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'commons-io:commons-io:2.16.1'
|
||||
api 'org.apache.commons:commons-text:1.12.0'
|
||||
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 'org.json:json:20240303'
|
||||
api 'com.google.code.gson:gson:2.11.0'
|
||||
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.5.0'
|
||||
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:5.1.0') {
|
||||
api('com.zaxxer:HikariCP:7.0.2') {
|
||||
exclude module: 'slf4j-api'
|
||||
}
|
||||
|
||||
compileOnly 'net.william278.uniform:uniform-common:1.1.5'
|
||||
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.32'
|
||||
compileOnly 'org.jetbrains:annotations:24.1.0'
|
||||
compileOnly 'net.kyori:adventure-api:4.17.0'
|
||||
compileOnly 'net.kyori:adventure-platform-api:4.3.3'
|
||||
compileOnly 'com.google.guava:guava:33.2.1-jre'
|
||||
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
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"
|
||||
@@ -33,10 +37,10 @@ dependencies {
|
||||
|
||||
testImplementation "redis.clients:jedis:$jedis_version"
|
||||
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
|
||||
testImplementation 'com.google.guava:guava:33.2.1-jre'
|
||||
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
testCompileOnly 'de.exlll:configlib-yaml:4.5.0'
|
||||
testCompileOnly 'org.jetbrains:annotations:24.1.0'
|
||||
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.32'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.42'
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
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;
|
||||
@@ -34,13 +35,13 @@ 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.LegacyConverter;
|
||||
import net.william278.husksync.util.Task;
|
||||
import net.william278.husksync.util.*;
|
||||
import net.william278.uniform.Uniform;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -52,7 +53,8 @@ import java.util.logging.Level;
|
||||
/**
|
||||
* Abstract implementation of the HuskSync plugin.
|
||||
*/
|
||||
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider, SerializerRegistry {
|
||||
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider, SerializerRegistry,
|
||||
CompatibilityChecker, DumpProvider, DataVersionSupplier {
|
||||
|
||||
int SPIGOT_RESOURCE_ID = 97144;
|
||||
|
||||
@@ -136,7 +138,7 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
if (getPlayerCustomDataStore().containsKey(user.getUuid())) {
|
||||
return getPlayerCustomDataStore().get(user.getUuid());
|
||||
}
|
||||
final Map<Identifier, Data> data = new HashMap<>();
|
||||
final Map<Identifier, Data> data = Maps.newHashMap();
|
||||
getPlayerCustomDataStore().put(user.getUuid(), data);
|
||||
return data;
|
||||
}
|
||||
@@ -255,6 +257,14 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
@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
|
||||
*
|
||||
@@ -265,10 +275,10 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
@NotNull
|
||||
default UpdateChecker getUpdateChecker() {
|
||||
return UpdateChecker.builder()
|
||||
.currentVersion(getPluginVersion())
|
||||
.endpoint(UpdateChecker.Endpoint.SPIGOT)
|
||||
.resource(Integer.toString(SPIGOT_RESOURCE_ID))
|
||||
.build();
|
||||
.currentVersion(getPluginVersion())
|
||||
.endpoint(UpdateChecker.Endpoint.SPIGOT)
|
||||
.resource(Integer.toString(SPIGOT_RESOURCE_ID))
|
||||
.build();
|
||||
}
|
||||
|
||||
default void checkForUpdates() {
|
||||
@@ -276,14 +286,17 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
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())
|
||||
"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>
|
||||
@@ -292,6 +305,12 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
@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);
|
||||
}
|
||||
@@ -320,17 +339,21 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
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""";
|
||||
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""";
|
||||
|
||||
FailedToLoadException(@NotNull String message, @NotNull Throwable cause) {
|
||||
public FailedToLoadException(@NotNull String message) {
|
||||
super(String.format(FORMAT, message));
|
||||
}
|
||||
|
||||
public FailedToLoadException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(String.format(FORMAT, message), cause);
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,18 @@ public class HuskSyncAPI {
|
||||
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
|
||||
*
|
||||
@@ -137,7 +149,7 @@ public class HuskSyncAPI {
|
||||
*/
|
||||
public CompletableFuture<Optional<DataSnapshot.Unpacked>> getCurrentData(@NotNull User user) {
|
||||
return plugin.getRedisManager()
|
||||
.getUserData(UUID.randomUUID(), user)
|
||||
.getOnlineUserData(UUID.randomUUID(), user, DataSnapshot.SaveCause.API)
|
||||
.thenApply(data -> data.or(() -> plugin.getDatabase().getLatestSnapshot(user)))
|
||||
.thenApply(data -> data.map(snapshot -> snapshot.unpack(plugin)));
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ 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.RedisKeyType;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
@@ -37,7 +36,7 @@ import java.util.Optional;
|
||||
public class EnderChestCommand extends ItemsCommand {
|
||||
|
||||
public EnderChestCommand(@NotNull HuskSync plugin) {
|
||||
super("enderchest", List.of("echest", "openechest"), plugin);
|
||||
super("enderchest", List.of("echest", "openechest"), DataSnapshot.SaveCause.ENDERCHEST_COMMAND, plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -46,29 +45,29 @@ public class EnderChestCommand extends ItemsCommand {
|
||||
final Optional<Data.Items.EnderChest> optionalEnderChest = snapshot.getEnderChest();
|
||||
if (optionalEnderChest.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Display opening message
|
||||
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(),
|
||||
snapshot.getTimestamp().format(DateTimeFormatter
|
||||
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
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.getUsername())
|
||||
.orElse(new MineDown(String.format("%s's Ender Chest", user.getUsername()))),
|
||||
allowEdit,
|
||||
enderChest.getSlotCount(),
|
||||
(itemsOnClose) -> {
|
||||
if (allowEdit && !enderChest.equals(itemsOnClose)) {
|
||||
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,24 +77,23 @@ public class EnderChestCommand extends ItemsCommand {
|
||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
|
||||
if (latestData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
.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(DataSnapshot.SaveCause.ENDERCHEST_COMMAND);
|
||||
data.setPinned(
|
||||
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND)
|
||||
);
|
||||
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, RedisKeyType.TTL_1_YEAR));
|
||||
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot));
|
||||
redis.sendUserDataUpdate(user, data);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,25 +24,29 @@ 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.HoverEvent;
|
||||
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.apache.commons.text.WordUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -52,41 +56,42 @@ public class HuskSyncCommand extends PluginCommand {
|
||||
private final AboutMenu aboutMenu;
|
||||
|
||||
public HuskSyncCommand(@NotNull HuskSync plugin) {
|
||||
super("husksync", List.of(), Permission.Default.TRUE, 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)"))
|
||||
.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();
|
||||
.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
|
||||
@@ -94,8 +99,10 @@ public class HuskSyncCommand extends PluginCommand {
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -109,12 +116,32 @@ public class HuskSyncCommand extends PluginCommand {
|
||||
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()
|
||||
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) -> {
|
||||
@@ -126,7 +153,7 @@ public class HuskSyncCommand extends PluginCommand {
|
||||
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)"
|
||||
"[Error:](#ff3300) [Failed to reload the plugin. Check console for errors.](#ff7e5e)"
|
||||
));
|
||||
plugin.log(Level.SEVERE, "Failed to reload the plugin", e);
|
||||
}
|
||||
@@ -139,11 +166,11 @@ public class HuskSyncCommand extends PluginCommand {
|
||||
final CommandUser user = user(sub, ctx);
|
||||
if (checked.isUpToDate()) {
|
||||
plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
.ifPresent(user::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getLocales().getLocale("update_available", checked.getLatestVersion().toString(),
|
||||
plugin.getPluginVersion().toString()).ifPresent(user::sendMessage);
|
||||
plugin.getPluginVersion().toString()).ifPresent(user::sendMessage);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -152,14 +179,18 @@ public class HuskSyncCommand extends PluginCommand {
|
||||
return (sub) -> {
|
||||
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
|
||||
sub.setDefaultExecutor((ctx) -> {
|
||||
plugin.log(Level.INFO, "Please choose a migrator, then run \"husksync migrate <migrator>\"");
|
||||
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"))
|
||||
"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 -> {
|
||||
@@ -173,17 +204,46 @@ public class HuskSyncCommand extends PluginCommand {
|
||||
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(Arrays.copyOfRange(args, 3, args.length));
|
||||
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);
|
||||
.filter(m -> m.getIdentifier().equalsIgnoreCase(id)).findFirst().orElse(null);
|
||||
if (migrator == null) {
|
||||
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
|
||||
}
|
||||
@@ -196,86 +256,4 @@ public class HuskSyncCommand extends PluginCommand {
|
||||
});
|
||||
}
|
||||
|
||||
private enum StatusLine {
|
||||
PLUGIN_VERSION(plugin -> Component.text("v" + plugin.getPluginVersion().toStringWithoutMetadata())
|
||||
.appendSpace().append(plugin.getPluginVersion().getMetadata().isBlank() ? Component.empty()
|
||||
: Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))),
|
||||
PLATFORM_TYPE(plugin -> Component.text(WordUtils.capitalizeFully(plugin.getPlatformType()))),
|
||||
LANGUAGE(plugin -> Component.text(plugin.getSettings().getLanguage())),
|
||||
MINECRAFT_VERSION(plugin -> Component.text(plugin.getMinecraftVersion().toString())),
|
||||
JAVA_VERSION(plugin -> Component.text(System.getProperty("java.version"))),
|
||||
JAVA_VENDOR(plugin -> Component.text(System.getProperty("java.vendor"))),
|
||||
SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully(
|
||||
plugin.getSettings().getSynchronization().getMode().toString()
|
||||
))),
|
||||
DELAY_LATENCY(plugin -> Component.text(
|
||||
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms"
|
||||
)),
|
||||
SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
|
||||
CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())),
|
||||
DATABASE_TYPE(plugin ->
|
||||
Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() +
|
||||
(plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ?
|
||||
(plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : ""))
|
||||
),
|
||||
IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())),
|
||||
USING_REDIS_SENTINEL(plugin -> getBoolean(
|
||||
!plugin.getSettings().getRedis().getSentinel().getMaster().isBlank()
|
||||
)),
|
||||
USING_REDIS_PASSWORD(plugin -> getBoolean(
|
||||
!plugin.getSettings().getRedis().getCredentials().getPassword().isBlank()
|
||||
)),
|
||||
REDIS_USING_SSL(plugin -> getBoolean(
|
||||
plugin.getSettings().getRedis().getCredentials().isUseSsl()
|
||||
)),
|
||||
IS_REDIS_LOCAL(plugin -> getLocalhostBoolean(
|
||||
plugin.getSettings().getRedis().getCredentials().getHost()
|
||||
)),
|
||||
DATA_TYPES(plugin -> Component.join(
|
||||
JoinConfiguration.commas(true),
|
||||
plugin.getRegisteredDataTypes().stream().map(i -> Component.textOfChildren(Component.text(i.toString())
|
||||
.appendSpace().append(Component.text(i.isEnabled() ? '✔' : '❌')))
|
||||
.color(i.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED)
|
||||
.hoverEvent(HoverEvent.showText(
|
||||
Component.text(i.isEnabled() ? "Enabled" : "Disabled")
|
||||
.append(Component.newline())
|
||||
.append(Component.text("Dependencies: %s".formatted(i.getDependencies()
|
||||
.isEmpty() ? "(None)" : i.getDependencies().stream()
|
||||
.map(d -> "%s (%s)".formatted(
|
||||
d.getKey().value(), d.isRequired() ? "Required" : "Optional"
|
||||
)).collect(Collectors.joining(", ")))
|
||||
).color(NamedTextColor.GRAY))
|
||||
))).toList()
|
||||
));
|
||||
|
||||
private final Function<HuskSync, Component> supplier;
|
||||
|
||||
StatusLine(@NotNull Function<HuskSync, Component> supplier) {
|
||||
this.supplier = supplier;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Component get(@NotNull HuskSync plugin) {
|
||||
return Component
|
||||
.text("•").appendSpace()
|
||||
.append(Component.text(
|
||||
WordUtils.capitalizeFully(name().replaceAll("_", " ")),
|
||||
TextColor.color(0x848484)
|
||||
))
|
||||
.append(Component.text(':')).append(Component.space().color(NamedTextColor.WHITE))
|
||||
.append(supplier.apply(plugin));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Component getBoolean(boolean value) {
|
||||
return Component.text(value ? "Yes" : "No", value ? NamedTextColor.GREEN : NamedTextColor.RED);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Component getLocalhostBoolean(@NotNull String value) {
|
||||
return getBoolean(value.equals("127.0.0.1") || value.equals("0.0.0.0")
|
||||
|| value.equals("localhost") || value.equals("::1"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ 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.RedisKeyType;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
@@ -37,7 +36,7 @@ import java.util.Optional;
|
||||
public class InventoryCommand extends ItemsCommand {
|
||||
|
||||
public InventoryCommand(@NotNull HuskSync plugin) {
|
||||
super("inventory", List.of("invsee", "openinv"), plugin);
|
||||
super("inventory", List.of("invsee", "openinv"), DataSnapshot.SaveCause.INVENTORY_COMMAND, plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -45,31 +44,30 @@ public class InventoryCommand extends ItemsCommand {
|
||||
@NotNull User user, boolean allowEdit) {
|
||||
final Optional<Data.Items.Inventory> optionalInventory = snapshot.getInventory();
|
||||
if (optionalInventory.isEmpty()) {
|
||||
viewer.sendMessage(new MineDown("what the FUCK is happening"));
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Display opening message
|
||||
plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(),
|
||||
snapshot.getTimestamp().format(DateTimeFormatter
|
||||
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
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.getUsername())
|
||||
.orElse(new MineDown(String.format("%s's Inventory", user.getUsername()))),
|
||||
allowEdit,
|
||||
inventory.getSlotCount(),
|
||||
(itemsOnClose) -> {
|
||||
if (allowEdit && !inventory.equals(itemsOnClose)) {
|
||||
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,24 +77,23 @@ public class InventoryCommand extends ItemsCommand {
|
||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
|
||||
if (latestData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
.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(DataSnapshot.SaveCause.INVENTORY_COMMAND);
|
||||
data.setPinned(
|
||||
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND)
|
||||
);
|
||||
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, RedisKeyType.TTL_1_YEAR));
|
||||
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot));
|
||||
redis.sendUserDataUpdate(user, data);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,8 +34,12 @@ import java.util.UUID;
|
||||
|
||||
public abstract class ItemsCommand extends PluginCommand {
|
||||
|
||||
protected ItemsCommand(@NotNull String name, @NotNull List<String> aliases, @NotNull HuskSync plugin) {
|
||||
super(name, aliases, Permission.Default.IF_OP, plugin);
|
||||
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
|
||||
@@ -46,17 +50,17 @@ public abstract class ItemsCommand extends PluginCommand {
|
||||
final CommandUser executor = user(command, ctx);
|
||||
if (!(executor instanceof OnlineUser online)) {
|
||||
plugin.getLocales().getLocale("error_in_game_command_only")
|
||||
.ifPresent(executor::sendMessage);
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
this.showSnapshotItems(online, user, version);
|
||||
}, user("username"), uuid("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);
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
this.showLatestItems(online, user);
|
||||
@@ -65,45 +69,45 @@ public abstract class ItemsCommand extends PluginCommand {
|
||||
|
||||
// View (and edit) the latest user data
|
||||
private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) {
|
||||
plugin.getRedisManager().getUserData(user.getUuid(), user).thenAccept(data -> data
|
||||
.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);
|
||||
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();
|
||||
}
|
||||
return Optional.of(packed.unpack(plugin));
|
||||
})
|
||||
.ifPresent(snapshot -> this.showItems(
|
||||
viewer, snapshot, user, viewer.hasPermission(getPermission("edit"))
|
||||
)));
|
||||
})
|
||||
.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);
|
||||
.or(() -> {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(packed.unpack(plugin));
|
||||
})
|
||||
.ifPresent(snapshot -> this.showItems(
|
||||
viewer, snapshot, user, false
|
||||
));
|
||||
})
|
||||
.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
|
||||
|
||||
@@ -23,6 +23,7 @@ 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;
|
||||
@@ -31,6 +32,7 @@ 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;
|
||||
@@ -39,9 +41,9 @@ public abstract class PluginCommand extends Command {
|
||||
|
||||
protected final HuskSync plugin;
|
||||
|
||||
protected PluginCommand(@NotNull String name, @NotNull List<String> aliases,
|
||||
@NotNull Permission.Default permissionDefault, @NotNull HuskSync plugin) {
|
||||
super(name, aliases, getDescription(plugin, name), new Permission(createPermission(name), permissionDefault));
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -75,6 +77,20 @@ public abstract class PluginCommand extends Command {
|
||||
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 -> {
|
||||
@@ -83,20 +99,29 @@ public abstract class PluginCommand extends Command {
|
||||
() -> CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader)
|
||||
);
|
||||
}, (context, builder) -> {
|
||||
plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getUsername()));
|
||||
plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getName()));
|
||||
return builder.buildFuture();
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected <S> ArgumentElement<S, UUID> uuid(@NotNull String name) {
|
||||
return new ArgumentElement<>(name, reader -> {
|
||||
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) -> builder.buildFuture());
|
||||
}, (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 {
|
||||
|
||||
@@ -20,15 +20,19 @@
|
||||
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.RedisKeyType;
|
||||
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.DataDumper;
|
||||
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;
|
||||
@@ -44,7 +48,7 @@ import java.util.logging.Level;
|
||||
public class UserDataCommand extends PluginCommand {
|
||||
|
||||
public UserDataCommand(@NotNull HuskSync plugin) {
|
||||
super("userdata", List.of("playerdata"), Permission.Default.IF_OP, plugin);
|
||||
super("userdata", List.of("playerdata"), Permission.Default.IF_OP, ExecutionScope.ALL, plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -52,6 +56,7 @@ public class UserDataCommand extends PluginCommand {
|
||||
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());
|
||||
@@ -102,6 +107,13 @@ public class UserDataCommand extends PluginCommand {
|
||||
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)) {
|
||||
@@ -113,7 +125,7 @@ public class UserDataCommand extends PluginCommand {
|
||||
plugin.getLocales().getLocale("data_deleted",
|
||||
version.toString().split("-")[0],
|
||||
version.toString(),
|
||||
user.getUsername(),
|
||||
user.getName(),
|
||||
user.getUuid().toString())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
@@ -145,9 +157,9 @@ public class UserDataCommand extends PluginCommand {
|
||||
// Save data
|
||||
final RedisManager redis = plugin.getRedisManager();
|
||||
plugin.getDataSyncer().saveData(user, data, (u, s) -> {
|
||||
redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s, RedisKeyType.TTL_1_YEAR));
|
||||
redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s));
|
||||
redis.sendUserDataUpdate(u, s);
|
||||
plugin.getLocales().getLocale("data_restored", u.getUsername(), u.getUuid().toString(),
|
||||
plugin.getLocales().getLocale("data_restored", u.getName(), u.getUuid().toString(),
|
||||
s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage);
|
||||
});
|
||||
}
|
||||
@@ -169,11 +181,11 @@ public class UserDataCommand extends PluginCommand {
|
||||
plugin.getDatabase().pinSnapshot(user, data.getId());
|
||||
}
|
||||
plugin.getLocales().getLocale(data.isPinned() ? "data_unpinned" : "data_pinned", data.getShortId(),
|
||||
data.getId().toString(), user.getUsername(), user.getUuid().toString())
|
||||
data.getId().toString(), user.getName(), user.getUuid().toString())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
|
||||
// Dump a snapshot
|
||||
// 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);
|
||||
@@ -182,14 +194,20 @@ public class UserDataCommand extends PluginCommand {
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
this.dumpSnapshot(executor, user, data.get(), type);
|
||||
}
|
||||
|
||||
// Dump the data
|
||||
final DataSnapshot.Packed userData = data.get();
|
||||
final DataDumper dumper = DataDumper.create(userData, user, plugin);
|
||||
// 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 {
|
||||
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getUsername(),
|
||||
(type == DumpType.WEB ? dumper.toWeb() : dumper.toFile()))
|
||||
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);
|
||||
}
|
||||
@@ -198,15 +216,15 @@ public class UserDataCommand extends PluginCommand {
|
||||
@NotNull
|
||||
private CommandProvider view() {
|
||||
return (sub) -> {
|
||||
sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
viewLatestSnapshot(user(sub, ctx), user);
|
||||
}, user("username"));
|
||||
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"), uuid("version"));
|
||||
}, user("username"), versionUuid());
|
||||
sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
viewLatestSnapshot(user(sub, ctx), user);
|
||||
}, user("username"));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -231,7 +249,15 @@ public class UserDataCommand extends PluginCommand {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final UUID version = ctx.getArgument("version", UUID.class);
|
||||
deleteSnapshot(user(sub, ctx), user, version);
|
||||
}, user("username"), uuid("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
|
||||
@@ -240,7 +266,7 @@ public class UserDataCommand extends PluginCommand {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final UUID version = ctx.getArgument("version", UUID.class);
|
||||
restoreSnapshot(user(sub, ctx), user, version);
|
||||
}, user("username"), uuid("version"));
|
||||
}, user("username"), versionUuid());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -249,17 +275,32 @@ public class UserDataCommand extends PluginCommand {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final UUID version = ctx.getArgument("version", UUID.class);
|
||||
pinSnapshot(user(sub, ctx), user, version);
|
||||
}, user("username"), uuid("version"));
|
||||
}, user("username"), versionUuid());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider dump() {
|
||||
return (sub) -> 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"), uuid("version"), dumpType());
|
||||
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() {
|
||||
|
||||
@@ -131,6 +131,15 @@ public interface ConfigProvider {
|
||||
));
|
||||
}
|
||||
|
||||
default void validateConfigFiles() {
|
||||
// Validate server name is default
|
||||
if (getServerName().equals("server")) {
|
||||
getPlugin().log(Level.WARNING, "The server name set in ~/plugins/HuskSync/server.yml appears to" +
|
||||
"be unchanged from the default (currently set to: \"server\"). Please check that this value has" +
|
||||
"been updated to match the case-sensitive ID of this server in your proxy config file!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a plugin resource
|
||||
*
|
||||
|
||||
@@ -193,14 +193,20 @@ public class Locales {
|
||||
* Displays the notification in the action bar
|
||||
*/
|
||||
ACTION_BAR,
|
||||
|
||||
/**
|
||||
* Displays the notification in the chat
|
||||
*/
|
||||
CHAT,
|
||||
|
||||
/**
|
||||
* Displays the notification in an Advancement Toast
|
||||
*
|
||||
* @deprecated No longer supported
|
||||
*/
|
||||
@Deprecated(since = "3.6.7")
|
||||
TOAST,
|
||||
|
||||
/**
|
||||
* Does not display the notification
|
||||
*/
|
||||
|
||||
@@ -22,13 +22,11 @@ package net.william278.husksync.config;
|
||||
import de.exlll.configlib.Configuration;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Getter
|
||||
@Configuration
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@@ -66,5 +64,9 @@ public class Server {
|
||||
}
|
||||
return super.equals(other);
|
||||
}
|
||||
|
||||
|
||||
public String getName() {
|
||||
final String envServerName = System.getenv("HUSKSYNC_SERVER_NAME");
|
||||
return envServerName == null ? name : envServerName;
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ public class Settings {
|
||||
private boolean checkForUpdates = true;
|
||||
|
||||
@Comment("Specify a common ID for grouping servers running HuskSync. "
|
||||
+ "Don't modify this unless you know what you're doing!")
|
||||
+ "Don't modify this unless you know what you're doing!")
|
||||
private String clusterId = "";
|
||||
|
||||
@Comment("Enable development debug logging")
|
||||
@@ -141,13 +141,16 @@ public class Settings {
|
||||
@Getter(AccessLevel.NONE)
|
||||
private Map<String, String> tableNames = Database.TableName.getDefaults();
|
||||
|
||||
@Comment("Whether to run the creation SQL on the database when the server starts. Don't modify this unless you know what you're doing!")
|
||||
private boolean createTables = true;
|
||||
|
||||
@NotNull
|
||||
public String getTableName(@NotNull Database.TableName tableName) {
|
||||
return tableNames.getOrDefault(tableName.name().toLowerCase(Locale.ENGLISH), tableName.getDefaultName());
|
||||
}
|
||||
}
|
||||
|
||||
// 𝓡𝓮𝓭𝓲𝓼 settings
|
||||
// Redis settings
|
||||
@Comment("Redis settings")
|
||||
private RedisSettings redis = new RedisSettings();
|
||||
|
||||
@@ -156,7 +159,9 @@ public class Settings {
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class RedisSettings {
|
||||
|
||||
@Comment("Specify the credentials of your Redis database here. Set \"password\" to '' if you don't have one")
|
||||
@Comment({"Specify the credentials of your Redis server here.",
|
||||
"Set \"user\" to '' if you don't have one or would like to use the default user.",
|
||||
"Set \"password\" to '' if you don't have one."})
|
||||
private RedisCredentials credentials = new RedisCredentials();
|
||||
|
||||
@Getter
|
||||
@@ -165,8 +170,49 @@ public class Settings {
|
||||
public static class RedisCredentials {
|
||||
private String host = "localhost";
|
||||
private int port = 6379;
|
||||
@Comment("Only change the database if you know what you are doing. The default is 0.")
|
||||
private int database = 0;
|
||||
private String user = "";
|
||||
private String password = "";
|
||||
|
||||
@Comment("Use SSL/TLS for encrypted connections.")
|
||||
private boolean useSsl = false;
|
||||
|
||||
@Comment("Connection timeout in milliseconds.")
|
||||
private int connectionTimeout = 2000;
|
||||
|
||||
@Comment("Socket (read/write) timeout in milliseconds.")
|
||||
private int socketTimeout = 2000;
|
||||
|
||||
@Comment("Max number of connections in the pool.")
|
||||
private int maxTotalConnections = 50;
|
||||
|
||||
@Comment("Max number of idle connections in the pool.")
|
||||
private int maxIdleConnections = 8;
|
||||
|
||||
@Comment("Min number of idle connections in the pool.")
|
||||
private int minIdleConnections = 2;
|
||||
|
||||
@Comment("Enable health checks when borrowing connections from the pool.")
|
||||
private boolean testOnBorrow = true;
|
||||
|
||||
@Comment("Enable health checks when returning connections to the pool.")
|
||||
private boolean testOnReturn = true;
|
||||
|
||||
@Comment("Enable periodic idle connection health checks.")
|
||||
private boolean testWhileIdle = true;
|
||||
|
||||
@Comment("Min evictable idle time (ms) before a connection is eligible for eviction.")
|
||||
private long minEvictableIdleTimeMillis = 60000;
|
||||
|
||||
@Comment("Time (ms) between eviction runs.")
|
||||
private long timeBetweenEvictionRunsMillis = 30000;
|
||||
|
||||
@Comment("Number of retries for commands when connection fails.")
|
||||
private int maxRetries = 3;
|
||||
|
||||
@Comment("Base backoff time in ms for retries (exponential backoff multiplier).")
|
||||
private int retryBackoffMillis = 200;
|
||||
}
|
||||
|
||||
@Comment("Options for if you're using Redis sentinel. Don't modify this unless you know what you're doing!")
|
||||
@@ -229,7 +275,7 @@ public class Settings {
|
||||
private boolean enabled = false;
|
||||
|
||||
@Comment("What items to save in death snapshots? (DROPS or ITEMS_TO_KEEP). "
|
||||
+ "Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server.")
|
||||
+ "Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server.")
|
||||
private DeathItemsMode itemsToSave = DeathItemsMode.DROPS;
|
||||
|
||||
@Comment("Should a death snapshot still be created even if the items to save on the player's death are empty?")
|
||||
@@ -250,14 +296,14 @@ public class Settings {
|
||||
@Comment("Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing")
|
||||
private boolean compressData = true;
|
||||
|
||||
@Comment("Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)")
|
||||
@Comment("Where to display sync notifications (ACTION_BAR, CHAT or NONE)")
|
||||
private Locales.NotificationSlot notificationDisplaySlot = Locales.NotificationSlot.ACTION_BAR;
|
||||
|
||||
@Comment("Persist maps locked in a Cartography Table to let them be viewed on any server")
|
||||
private boolean persistLockedMaps = true;
|
||||
|
||||
@Comment("If using the DELAY sync method, how long should this server listen for Redis key data updates before "
|
||||
+ "pulling data from the database instead (i.e., if the user did not change servers).")
|
||||
+ "pulling data from the database instead (i.e., if the user did not change servers).")
|
||||
private int networkLatencyMilliseconds = 500;
|
||||
|
||||
@Comment({"Which data types to synchronize.", "Docs: https://william278.net/docs/husksync/sync-features"})
|
||||
@@ -267,15 +313,60 @@ public class Settings {
|
||||
@Comment("Commands which should be blocked before a player has finished syncing (Use * to block all commands)")
|
||||
private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*"));
|
||||
|
||||
@Comment({"For attribute syncing, which attributes should be ignored/skipped when syncing",
|
||||
"(e.g. ['minecraft:generic.max_health', 'minecraft:generic.attack_damage'])"})
|
||||
@Getter(AccessLevel.NONE)
|
||||
private List<String> ignoredAttributes = new ArrayList<>(List.of(""));
|
||||
@Comment("Configuration for how to sync attributes")
|
||||
private AttributeSettings attributes = new AttributeSettings();
|
||||
|
||||
@Getter
|
||||
@Configuration
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class AttributeSettings {
|
||||
|
||||
@Comment({"Which attribute types should be saved as part of attribute syncing. Supports wildcard matching.",
|
||||
"(e.g. ['minecraft:generic.max_health', 'minecraft:generic.*'])"})
|
||||
@Getter(AccessLevel.NONE)
|
||||
private List<String> syncedAttributes = new ArrayList<>(List.of(
|
||||
"minecraft:generic.max_health", "minecraft:max_health",
|
||||
"minecraft:generic.max_absorption", "minecraft:max_absorption",
|
||||
"minecraft:generic.luck", "minecraft:luck",
|
||||
"minecraft:generic.scale", "minecraft:scale",
|
||||
"minecraft:generic.step_height", "minecraft:step_height",
|
||||
"minecraft:generic.gravity", "minecraft:gravity"
|
||||
));
|
||||
|
||||
@Comment({"Which attribute modifiers should be saved. Supports wildcard matching.",
|
||||
"(e.g. ['minecraft:effect.speed', 'minecraft:effect.*'])"})
|
||||
@Getter(AccessLevel.NONE)
|
||||
private List<String> ignoredModifiers = new ArrayList<>(List.of(
|
||||
"minecraft:effect.*", "minecraft:creative_mode_*"
|
||||
));
|
||||
|
||||
private boolean matchesWildcard(@NotNull String pat, @NotNull String value) {
|
||||
if (!pat.contains(":")) {
|
||||
pat = "minecraft:%s".formatted(pat);
|
||||
}
|
||||
if (!value.contains(":")) {
|
||||
value = "minecraft:%s".formatted(value);
|
||||
}
|
||||
return pat.contains("*") ? value.matches(pat.replace("*", ".*")) : pat.equals(value);
|
||||
}
|
||||
|
||||
public boolean isIgnoredAttribute(@NotNull String attribute) {
|
||||
return syncedAttributes.stream().noneMatch(wildcard -> matchesWildcard(wildcard, attribute));
|
||||
}
|
||||
|
||||
public boolean isIgnoredModifier(@NotNull String modifier) {
|
||||
return ignoredModifiers.stream().anyMatch(wildcard -> matchesWildcard(wildcard, modifier));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Comment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts")
|
||||
@Getter(AccessLevel.NONE)
|
||||
private Map<String, String> eventPriorities = EventListener.ListenerType.getDefaults();
|
||||
|
||||
@Comment("Enable check-in petitions for data syncing (don't change this unless you know what you're doing)")
|
||||
private boolean checkinPetitions = false;
|
||||
|
||||
public boolean doAutoPin(@NotNull DataSnapshot.SaveCause cause) {
|
||||
return autoPinnedSaveCauses.contains(cause.name());
|
||||
}
|
||||
@@ -284,10 +375,6 @@ public class Settings {
|
||||
return id.isCustom() || features.getOrDefault(id.getKeyValue(), id.isEnabledByDefault());
|
||||
}
|
||||
|
||||
public boolean isIgnoredAttribute(@NotNull String attribute) {
|
||||
return ignoredAttributes.contains(attribute);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) {
|
||||
try {
|
||||
|
||||
@@ -22,9 +22,8 @@ package net.william278.husksync.data;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.william278.husksync.HuskSync;
|
||||
@@ -132,7 +131,7 @@ public interface Data {
|
||||
/**
|
||||
* Represents a potion effect
|
||||
*
|
||||
* @param type the type of potion effect
|
||||
* @param type the key of potion effect
|
||||
* @param amplifier the amplifier of the potion effect
|
||||
* @param duration the duration of the potion effect
|
||||
* @param isAmbient whether the potion effect is ambient
|
||||
@@ -155,14 +154,14 @@ public interface Data {
|
||||
*/
|
||||
interface Advancements extends Data {
|
||||
|
||||
String RECIPE_ADVANCEMENT = "minecraft:recipe";
|
||||
|
||||
@NotNull
|
||||
List<Advancement> getCompleted();
|
||||
|
||||
@NotNull
|
||||
default List<Advancement> getCompletedExcludingRecipes() {
|
||||
return getCompleted().stream()
|
||||
.filter(advancement -> !advancement.getKey().startsWith("minecraft:recipe"))
|
||||
.collect(Collectors.toList());
|
||||
return getCompleted().stream().filter(adv -> !adv.getKey().startsWith(RECIPE_ADVANCEMENT)).toList();
|
||||
}
|
||||
|
||||
void setCompleted(@NotNull List<Advancement> completed);
|
||||
@@ -191,13 +190,13 @@ public interface Data {
|
||||
@NotNull
|
||||
private static Map<String, Long> adaptDateMap(@NotNull Map<String, Date> dateMap) {
|
||||
return dateMap.entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getTime()));
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getTime()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Map<String, Date> adaptLongMap(@NotNull Map<String, Long> dateMap) {
|
||||
return dateMap.entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> new Date(e.getValue())));
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> new Date(e.getValue())));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -250,9 +249,9 @@ public interface Data {
|
||||
void setWorld(@NotNull World world);
|
||||
|
||||
record World(
|
||||
@SerializedName("name") @NotNull String name,
|
||||
@SerializedName("uuid") @NotNull UUID uuid,
|
||||
@SerializedName("environment") @NotNull String environment
|
||||
@SerializedName("name") @NotNull String name,
|
||||
@SerializedName("uuid") @NotNull UUID uuid,
|
||||
@SerializedName("environment") @NotNull String environment
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -324,9 +323,9 @@ public interface Data {
|
||||
List<Attribute> getAttributes();
|
||||
|
||||
record Attribute(
|
||||
@NotNull String name,
|
||||
double baseValue,
|
||||
@NotNull Set<Modifier> modifiers
|
||||
@NotNull String name,
|
||||
double baseValue,
|
||||
@NotNull Set<Modifier> modifiers
|
||||
) {
|
||||
|
||||
public double getValue() {
|
||||
@@ -341,36 +340,60 @@ public interface Data {
|
||||
|
||||
@Getter
|
||||
@Accessors(fluent = true)
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@RequiredArgsConstructor
|
||||
final class Modifier {
|
||||
final static String ANY_EQUIPMENT_SLOT_GROUP = "any";
|
||||
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Nullable
|
||||
@SerializedName("uuid")
|
||||
private UUID uuid;
|
||||
private UUID uuid = null;
|
||||
|
||||
// Since 1.21.1: Name, amount, operation, slotGroup
|
||||
@SerializedName("name")
|
||||
private String name;
|
||||
|
||||
@SerializedName("amount")
|
||||
private double amount;
|
||||
|
||||
@SerializedName("operation")
|
||||
private int operationType;
|
||||
private int operation;
|
||||
|
||||
@SerializedName("equipment_slot")
|
||||
@Deprecated(since = "3.7")
|
||||
private int equipmentSlot;
|
||||
|
||||
public Modifier(@NotNull String name, double amount, int operationType, int equipmentSlot) {
|
||||
@SerializedName("equipment_slot_group")
|
||||
private String slotGroup = ANY_EQUIPMENT_SLOT_GROUP;
|
||||
|
||||
public Modifier(@NotNull String name, double amount, int operation, @NotNull String slotGroup) {
|
||||
this.name = name;
|
||||
this.amount = amount;
|
||||
this.operationType = operationType;
|
||||
this.operation = operation;
|
||||
this.slotGroup = slotGroup;
|
||||
}
|
||||
|
||||
@Deprecated(since = "3.7")
|
||||
public Modifier(@NotNull UUID uuid, @NotNull String name, double amount, int operation, int equipmentSlot) {
|
||||
this.name = name;
|
||||
this.amount = amount;
|
||||
this.operation = operation;
|
||||
this.equipmentSlot = equipmentSlot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof Modifier modifier && modifier.uuid().equals(uuid());
|
||||
if (obj instanceof Modifier other) {
|
||||
if (uuid != null && other.uuid != null) {
|
||||
return uuid.equals(other.uuid);
|
||||
}
|
||||
return name.equals(other.name);
|
||||
}
|
||||
return super.equals(obj);
|
||||
}
|
||||
|
||||
public double modify(double value) {
|
||||
return switch (operationType) {
|
||||
return switch (operation) {
|
||||
case 0 -> value + amount;
|
||||
case 1 -> value * amount;
|
||||
case 2 -> value * (1 + amount);
|
||||
@@ -378,6 +401,10 @@ public interface Data {
|
||||
};
|
||||
}
|
||||
|
||||
public boolean hasUuid() {
|
||||
return uuid != null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public UUID uuid() {
|
||||
return uuid != null ? uuid : UUID.nameUUIDFromBytes(name.getBytes());
|
||||
@@ -387,8 +414,8 @@ public interface Data {
|
||||
|
||||
default Optional<Attribute> getAttribute(@NotNull Key key) {
|
||||
return getAttributes().stream()
|
||||
.filter(attribute -> attribute.name().equals(key.asString()))
|
||||
.findFirst();
|
||||
.filter(attribute -> attribute.name().equals(key.asString()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
default void removeAttribute(@NotNull Key key) {
|
||||
@@ -397,8 +424,8 @@ public interface Data {
|
||||
|
||||
default double getMaxHealth() {
|
||||
return getAttribute(MAX_HEALTH_KEY)
|
||||
.map(Attribute::getValue)
|
||||
.orElse(20.0);
|
||||
.map(Attribute::getValue)
|
||||
.orElse(20.0);
|
||||
}
|
||||
|
||||
default void setMaxHealth(double maxHealth) {
|
||||
|
||||
@@ -45,13 +45,13 @@ public class DataException extends IllegalStateException {
|
||||
@AllArgsConstructor
|
||||
public enum Reason {
|
||||
INVALID_MINECRAFT_VERSION((plugin, snapshot) -> String.format("The Minecraft version of the snapshot (%s) is " +
|
||||
"newer than the server's version (%s). Ensure each server is on the same version of Minecraft.",
|
||||
"newer than the server's version (%s). Ensure each server is on the same version of Minecraft.",
|
||||
snapshot.getMinecraftVersion(), plugin.getMinecraftVersion())),
|
||||
INVALID_FORMAT_VERSION((plugin, snapshot) -> String.format("The format version of the snapshot (%s) is newer " +
|
||||
"than the server's version (%s). Ensure each server is running the same version of HuskSync.",
|
||||
"than the server's version (%s). Ensure each server is running the same version of HuskSync.",
|
||||
snapshot.getFormatVersion(), DataSnapshot.CURRENT_FORMAT_VERSION)),
|
||||
INVALID_PLATFORM_TYPE((plugin, snapshot) -> String.format("The platform type of the snapshot (%s) does " +
|
||||
"not match the server's platform type (%s). Ensure each server has the same platform type.",
|
||||
"not match the server's platform type (%s). Ensure each server has the same platform type.",
|
||||
snapshot.getPlatformType(), plugin.getPlatformType())),
|
||||
NO_LEGACY_CONVERTER((plugin, snapshot) -> String.format("No legacy converter to convert format version: %s",
|
||||
snapshot.getFormatVersion()));
|
||||
|
||||
@@ -31,7 +31,10 @@ public interface DataHolder {
|
||||
Map<Identifier, Data> getData();
|
||||
|
||||
default Optional<? extends Data> getData(@NotNull Identifier id) {
|
||||
return getData().entrySet().stream().filter(e -> e.getKey().equals(id)).map(Map.Entry::getValue).findFirst();
|
||||
if (getData().containsKey(id)) {
|
||||
return Optional.of(getData().get(id));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
default void setData(@NotNull Identifier identifier, @NotNull Data data) {
|
||||
|
||||
@@ -370,7 +370,7 @@ public class DataSnapshot {
|
||||
public static class Unpacked extends DataSnapshot implements DataHolder {
|
||||
|
||||
@Expose(serialize = false, deserialize = false)
|
||||
private final TreeMap<Identifier, Data> deserialized;
|
||||
private final Map<Identifier, Data> deserialized;
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@@ -381,7 +381,7 @@ public class DataSnapshot {
|
||||
}
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull TreeMap<Identifier, Data> data,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<Identifier, Data> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
super(id, pinned, timestamp, saveCause, serverName, Map.of(), minecraftVersion, platformType, formatVersion);
|
||||
this.deserialized = data;
|
||||
@@ -389,14 +389,15 @@ public class DataSnapshot {
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
private TreeMap<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
|
||||
private Map<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
|
||||
return data.entrySet().stream()
|
||||
.filter(e -> plugin.getIdentifier(e.getKey()).isPresent())
|
||||
.map(entry -> Map.entry(plugin.getIdentifier(entry.getKey()).orElseThrow(), entry.getValue()))
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> plugin.deserializeData(entry.getKey(), entry.getValue()),
|
||||
(a, b) -> b, () -> Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR)
|
||||
entry -> plugin.deserializeData(entry.getKey(), entry.getValue(), getMinecraftVersion()),
|
||||
(a, b) -> a,
|
||||
HashMap::new
|
||||
));
|
||||
}
|
||||
|
||||
@@ -406,7 +407,9 @@ public class DataSnapshot {
|
||||
return deserialized.entrySet().stream()
|
||||
.collect(Collectors.toMap(
|
||||
entry -> entry.getKey().toString(),
|
||||
entry -> plugin.serializeData(entry.getKey(), entry.getValue())
|
||||
entry -> plugin.serializeData(entry.getKey(), entry.getValue()),
|
||||
(a, b) -> a,
|
||||
HashMap::new
|
||||
));
|
||||
}
|
||||
|
||||
@@ -421,6 +424,20 @@ public class DataSnapshot {
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a sorted iterable of the snapshots the snapshot is holding
|
||||
*
|
||||
* @return The data map
|
||||
* @since 3.8.2
|
||||
*/
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public Iterable<Map.Entry<Identifier, Data>> getSortedIterable() {
|
||||
final TreeMap<Identifier, Data> tree = Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR);
|
||||
tree.putAll(deserialized);
|
||||
return tree.entrySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack the {@link DataSnapshot} into a {@link DataSnapshot.Packed packed} snapshot
|
||||
*
|
||||
@@ -453,12 +470,12 @@ public class DataSnapshot {
|
||||
private String serverName;
|
||||
private boolean pinned;
|
||||
private OffsetDateTime timestamp;
|
||||
private final TreeMap<Identifier, Data> data;
|
||||
private final Map<Identifier, Data> data;
|
||||
|
||||
private Builder(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
this.pinned = false;
|
||||
this.data = Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR);
|
||||
this.data = Maps.newHashMap();
|
||||
this.timestamp = OffsetDateTime.now();
|
||||
this.id = UUID.randomUUID();
|
||||
this.serverName = plugin.getServerName();
|
||||
@@ -880,6 +897,20 @@ public class DataSnapshot {
|
||||
*/
|
||||
public static final SaveCause BACKUP_RESTORE = of("BACKUP_RESTORE");
|
||||
|
||||
/**
|
||||
* Indicates data was saved from executing the {@code /userdata save} command
|
||||
*
|
||||
* @since 3.8
|
||||
*/
|
||||
public static final SaveCause SAVE_COMMAND = of("SAVE_COMMAND", true);
|
||||
|
||||
/**
|
||||
* Indicates data was saved from executing the {@code /userdata dump} command
|
||||
*
|
||||
* @since 3.8
|
||||
*/
|
||||
public static final SaveCause DUMP_COMMAND = of("DUMP_COMMAND", true);
|
||||
|
||||
/**
|
||||
* Indicates data was saved by an API call
|
||||
*
|
||||
@@ -913,6 +944,8 @@ public class DataSnapshot {
|
||||
|
||||
private final boolean fireDataSaveEvent;
|
||||
|
||||
private static Map<String, SaveCause> registry;
|
||||
|
||||
/**
|
||||
* Get or create a {@link SaveCause} from a name
|
||||
*
|
||||
@@ -921,7 +954,7 @@ public class DataSnapshot {
|
||||
*/
|
||||
@NotNull
|
||||
public static SaveCause of(@NotNull String name) {
|
||||
return new SaveCause(name.length() > 32 ? name.substring(0, 31) : name, true);
|
||||
return of(name, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -933,7 +966,14 @@ public class DataSnapshot {
|
||||
*/
|
||||
@NotNull
|
||||
public static SaveCause of(@NotNull String name, boolean firesSaveEvent) {
|
||||
return new SaveCause(name.length() > 32 ? name.substring(0, 31) : name, firesSaveEvent);
|
||||
name = name.length() > 32 ? name.substring(0, 31) : name;
|
||||
|
||||
if (registry == null) registry = new HashMap<>();
|
||||
if (registry.containsKey(name)) return registry.get(name);
|
||||
|
||||
SaveCause cause = new SaveCause(name, firesSaveEvent);
|
||||
registry.put(cause.name(), cause);
|
||||
return cause;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -944,11 +984,10 @@ public class DataSnapshot {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Obsolete
|
||||
public static SaveCause[] values() {
|
||||
return new SaveCause[]{
|
||||
DISCONNECT, WORLD_SAVE, DEATH, SERVER_SHUTDOWN, INVENTORY_COMMAND, ENDERCHEST_COMMAND,
|
||||
BACKUP_RESTORE, API, MPDB_MIGRATION, LEGACY_MIGRATION, CONVERTED_FROM_V2
|
||||
};
|
||||
if (registry == null) registry = new HashMap<>();
|
||||
return registry.values().toArray(new SaveCause[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,11 @@ package net.william278.husksync.data;
|
||||
import lombok.*;
|
||||
import net.kyori.adventure.key.InvalidKeyException;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.kyori.adventure.key.KeyPattern;
|
||||
import org.intellij.lang.annotations.Subst;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
@@ -36,7 +38,10 @@ import java.util.stream.Stream;
|
||||
* Identifiers of different types of {@link Data}s
|
||||
*/
|
||||
@Getter
|
||||
public class Identifier {
|
||||
public class Identifier implements Comparable<Identifier> {
|
||||
|
||||
// Namespace for built-in identifiers
|
||||
private static final @KeyPattern String DEFAULT_NAMESPACE = "husksync";
|
||||
|
||||
// Built-in identifiers
|
||||
public static final Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
|
||||
@@ -50,7 +55,8 @@ public class Identifier {
|
||||
Dependency.optional("game_mode")
|
||||
);
|
||||
public static final Identifier ATTRIBUTES = huskSync("attributes", true,
|
||||
Dependency.required("potion_effects")
|
||||
Dependency.optional("inventory"),
|
||||
Dependency.optional("potion_effects")
|
||||
);
|
||||
public static final Identifier HEALTH = huskSync("health", true,
|
||||
Dependency.optional("attributes")
|
||||
@@ -91,8 +97,8 @@ public class Identifier {
|
||||
*/
|
||||
@NotNull
|
||||
public static Identifier from(@NotNull Key key, @NotNull Set<Dependency> dependencies) {
|
||||
if (key.namespace().equals("husksync")) {
|
||||
throw new IllegalArgumentException("You cannot register a key with \"husksync\" as the namespace!");
|
||||
if (key.namespace().equals(DEFAULT_NAMESPACE)) {
|
||||
throw new IllegalArgumentException("Cannot register with %s as key namespace!".formatted(key.namespace()));
|
||||
}
|
||||
return new Identifier(key, true, dependencies);
|
||||
}
|
||||
@@ -141,7 +147,7 @@ public class Identifier {
|
||||
@NotNull
|
||||
private static Identifier huskSync(@Subst("null") @NotNull String name,
|
||||
boolean configDefault) throws InvalidKeyException {
|
||||
return new Identifier(Key.key("husksync", name), configDefault, Collections.emptySet());
|
||||
return new Identifier(Key.key(DEFAULT_NAMESPACE, name), configDefault, Collections.emptySet());
|
||||
}
|
||||
|
||||
// Return an identifier with a HuskSync namespace
|
||||
@@ -149,7 +155,7 @@ public class Identifier {
|
||||
private static Identifier huskSync(@Subst("null") @NotNull String name,
|
||||
@SuppressWarnings("SameParameterValue") boolean configDefault,
|
||||
@NotNull Dependency... dependents) throws InvalidKeyException {
|
||||
return new Identifier(Key.key("husksync", name), configDefault, Set.of(dependents));
|
||||
return new Identifier(Key.key(DEFAULT_NAMESPACE, name), configDefault, Set.of(dependents));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,13 +213,30 @@ public class Identifier {
|
||||
* @return {@code false} if {@link #getKeyNamespace()} returns "husksync"; {@code true} otherwise
|
||||
*/
|
||||
public boolean isCustom() {
|
||||
return !getKeyNamespace().equals("husksync");
|
||||
return !getKeyNamespace().equals(DEFAULT_NAMESPACE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the minimal string representation of this key.
|
||||
* <p>
|
||||
* If the namespace of the key is {@link #DEFAULT_NAMESPACE}, only the key value will be returned.
|
||||
*
|
||||
* @return the minimal string key representation
|
||||
* @since 3.8
|
||||
*/
|
||||
@NotNull
|
||||
public String asMinimalString() {
|
||||
if (getKey().namespace().equals(DEFAULT_NAMESPACE)) {
|
||||
return getKey().value();
|
||||
}
|
||||
return getKey().asString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier as a string (the key)
|
||||
*
|
||||
* @return the identifier as a string
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
@Override
|
||||
@@ -222,25 +245,45 @@ public class Identifier {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the given object is an identifier with the same key as this identifier
|
||||
* Return whether this Identifier is equal to another Identifier
|
||||
*
|
||||
* @param obj the object to compare
|
||||
* @return {@code true} if the given object is an identifier with the same key as this identifier
|
||||
* @param obj another object
|
||||
* @return {@code true} if this identifier matches the identifier of {@code obj}
|
||||
* @since 3.8
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (obj instanceof Identifier other) {
|
||||
return key.equals(other.key);
|
||||
return asMinimalString().equals(other.asMinimalString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hash code of the Identifier (equivalent to {@link #asMinimalString()}->{@code #hashCode()}
|
||||
*
|
||||
* @return the hash code
|
||||
* @since 3.8
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return asMinimalString().hashCode();
|
||||
}
|
||||
|
||||
// Get the config entry for the identifier
|
||||
@NotNull
|
||||
private Map.Entry<String, Boolean> getConfigEntry() {
|
||||
return Map.entry(getKeyValue(), enabledByDefault);
|
||||
}
|
||||
|
||||
// Comparable; always sort this Identifier after any dependencies
|
||||
@Override
|
||||
public int compareTo(@NotNull Identifier o) {
|
||||
if (this.dependsOn(o)) return 1;
|
||||
if (o.dependsOn(this)) return -1;
|
||||
return this.key.compareTo(o.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two identifiers based on their dependencies.
|
||||
* <p>
|
||||
@@ -314,6 +357,11 @@ public class Identifier {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return key.toString().hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,12 +19,14 @@
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public interface SerializerRegistry {
|
||||
|
||||
@@ -39,7 +41,7 @@ public interface SerializerRegistry {
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
<T extends Data> TreeMap<Identifier, Serializer<T>> getSerializers();
|
||||
<T extends Data> Map<Identifier, Serializer<T>> getSerializers();
|
||||
|
||||
/**
|
||||
* Register a data serializer for the given {@link Identifier}
|
||||
@@ -86,8 +88,7 @@ public interface SerializerRegistry {
|
||||
* @since 3.0
|
||||
*/
|
||||
default Optional<Identifier> getIdentifier(@NotNull String key) {
|
||||
return getSerializers().keySet().stream()
|
||||
.filter(id -> id.getKey().asString().equals(key)).findFirst();
|
||||
return getSerializers().keySet().stream().filter(e -> e.toString().equals(key)).findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,9 +99,7 @@ public interface SerializerRegistry {
|
||||
* @since 3.5.4
|
||||
*/
|
||||
default Optional<Serializer<Data>> getSerializer(@NotNull Identifier identifier) {
|
||||
return getSerializers().entrySet().stream()
|
||||
.filter(entry -> entry.getKey().getKey().equals(identifier.getKey()))
|
||||
.map(Map.Entry::getValue).findFirst();
|
||||
return Optional.ofNullable(getSerializers().get(identifier));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,30 +118,47 @@ public interface SerializerRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize data for the given {@link Identifier}
|
||||
* Deserialize data of a given {@link Version Minecraft version} for the given {@link Identifier data identifier}
|
||||
*
|
||||
* @param identifier the {@link Identifier} to deserialize data for
|
||||
* @param data the data to deserialize
|
||||
* @param identifier the {@link Identifier} to deserialize data for
|
||||
* @param data the data to deserialize
|
||||
* @param dataMcVersion the Minecraft version of the data
|
||||
* @return the deserialized data
|
||||
* @throws IllegalStateException if no serializer is found for the given {@link Identifier}
|
||||
* @since 3.5.4
|
||||
* @since 3.6.4
|
||||
*/
|
||||
@NotNull
|
||||
default Data deserializeData(@NotNull Identifier identifier, @NotNull String data) throws IllegalStateException {
|
||||
return getSerializer(identifier).map(serializer -> serializer.deserialize(data)).orElseThrow(
|
||||
default Data deserializeData(@NotNull Identifier identifier, @NotNull String data,
|
||||
@NotNull Version dataMcVersion) throws IllegalStateException {
|
||||
return getSerializer(identifier).map(serializer -> serializer.deserialize(data, dataMcVersion)).orElseThrow(
|
||||
() -> new IllegalStateException("No serializer found for %s".formatted(identifier))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the set of registered data types
|
||||
* Deserialize data for the given {@link Identifier data identifier}
|
||||
*
|
||||
* @return the set of registered data types
|
||||
* @param identifier the {@link Identifier} to deserialize data for
|
||||
* @param data the data to deserialize
|
||||
* @return the deserialized data
|
||||
* @since 3.5.4
|
||||
* @deprecated Use {@link #deserializeData(Identifier, String, Version)} instead
|
||||
*/
|
||||
@NotNull
|
||||
@Deprecated(since = "3.6.5")
|
||||
default Data deserializeData(@NotNull Identifier identifier, @NotNull String data) {
|
||||
return deserializeData(identifier, data, getPlugin().getMinecraftVersion());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of registered data types, in dependency order
|
||||
*
|
||||
* @return the list of registered data types
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
default Set<Identifier> getRegisteredDataTypes() {
|
||||
return getSerializers().keySet();
|
||||
default List<Identifier> getRegisteredDataTypes() {
|
||||
return getSerializers().keySet().stream().sorted(DEPENDENCY_ORDER_COMPARATOR).toList();
|
||||
}
|
||||
|
||||
// Returns if a data type is available and enabled in the config
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A holder of data in the form of {@link Data}s, which can be synced
|
||||
@@ -46,7 +47,11 @@ public interface UserDataHolder extends DataHolder {
|
||||
.filter(Identifier::isEnabled)
|
||||
.map(id -> Map.entry(id, getData(id)))
|
||||
.filter(data -> data.getValue().isPresent())
|
||||
.collect(HashMap::new, (map, data) -> map.put(data.getKey(), data.getValue().get()), HashMap::putAll);
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> entry.getValue().get(),
|
||||
(a, b) -> a, HashMap::new
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,6 +80,15 @@ public interface UserDataHolder extends DataHolder {
|
||||
return DataSnapshot.builder(getPlugin()).data(this.getData()).saveCause(saveCause).buildAndPack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether data can be applied to the holder at this time
|
||||
*
|
||||
* @return {@code true} if data can be applied, otherwise false
|
||||
*/
|
||||
default boolean cannotApplySnapshot() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize and apply a data snapshot to this data owner
|
||||
* <p>
|
||||
@@ -90,9 +104,12 @@ public interface UserDataHolder extends DataHolder {
|
||||
* @since 3.0
|
||||
*/
|
||||
default void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull ThrowingConsumer<Boolean> runAfter) {
|
||||
final HuskSync plugin = getPlugin();
|
||||
if (cannotApplySnapshot()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unpack the snapshot
|
||||
final HuskSync plugin = getPlugin();
|
||||
final DataSnapshot.Unpacked unpacked;
|
||||
try {
|
||||
unpacked = snapshot.unpack(plugin);
|
||||
@@ -104,8 +121,12 @@ public interface UserDataHolder extends DataHolder {
|
||||
|
||||
// Synchronously attempt to apply the snapshot
|
||||
plugin.runSync(() -> {
|
||||
if (cannotApplySnapshot()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
for (Map.Entry<Identifier, Data> entry : unpacked.getData().entrySet()) {
|
||||
for (Map.Entry<Identifier, Data> entry : unpacked.getSortedIterable()) {
|
||||
final Identifier identifier = entry.getKey();
|
||||
if (!identifier.isEnabled()) {
|
||||
continue;
|
||||
|
||||
@@ -24,8 +24,10 @@ import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.intellij.lang.annotations.Language;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -56,8 +58,8 @@ public abstract class Database {
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
@NotNull
|
||||
protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException {
|
||||
return formatStatementTables(new String(Objects.requireNonNull(plugin.getResource(schemaFileName))
|
||||
.readAllBytes(), StandardCharsets.UTF_8)).split(";");
|
||||
return Arrays.stream(formatStatementTables(new String(Objects.requireNonNull(plugin.getResource(schemaFileName))
|
||||
.readAllBytes(), StandardCharsets.UTF_8)).split(";")).filter(s -> !s.isBlank()).toArray(String[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,10 +69,12 @@ public abstract class Database {
|
||||
* @return the formatted statement, with table placeholders replaced with the correct names
|
||||
*/
|
||||
@NotNull
|
||||
protected final String formatStatementTables(@NotNull String sql) {
|
||||
protected final String formatStatementTables(@NotNull @Language("SQL") String sql) {
|
||||
final Settings.DatabaseSettings settings = plugin.getSettings().getDatabase();
|
||||
return sql.replaceAll("%users_table%", settings.getTableName(TableName.USERS))
|
||||
.replaceAll("%user_data_table%", settings.getTableName(TableName.USER_DATA));
|
||||
.replaceAll("%user_data_table%", settings.getTableName(TableName.USER_DATA))
|
||||
.replaceAll("%map_data_table%", settings.getTableName(TableName.MAP_DATA))
|
||||
.replaceAll("%map_ids_table%", settings.getTableName(TableName.MAP_IDS));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,6 +111,14 @@ public abstract class Database {
|
||||
@Blocking
|
||||
public abstract Optional<User> getUserByName(@NotNull String username);
|
||||
|
||||
/**
|
||||
* Get all users
|
||||
*
|
||||
* @return A list of all users
|
||||
*/
|
||||
@NotNull
|
||||
@Blocking
|
||||
public abstract List<User> getAllUsers();
|
||||
|
||||
/**
|
||||
* Get the latest data snapshot for a user.
|
||||
@@ -127,6 +139,15 @@ public abstract class Database {
|
||||
@NotNull
|
||||
public abstract List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Get the number of unpinned {@link DataSnapshot}s a user has
|
||||
*
|
||||
* @param user the user to count snapshots for
|
||||
* @return the number of snapshots this user has saved
|
||||
*/
|
||||
@Blocking
|
||||
public abstract int getUnpinnedSnapshotCount(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Gets a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
|
||||
*
|
||||
@@ -238,6 +259,58 @@ public abstract class Database {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Write map data to a database
|
||||
*
|
||||
* @param serverName Name of the server the map originates from
|
||||
* @param mapId Original map ID
|
||||
* @param data Map data
|
||||
*/
|
||||
@Blocking
|
||||
public abstract void saveMapData(@NotNull String serverName, int mapId, byte @NotNull [] data);
|
||||
|
||||
/**
|
||||
* Read map data from a database
|
||||
*
|
||||
* @param serverName Name of the server the map originates from
|
||||
* @param mapId Original map ID
|
||||
* @return the map data bytes, or null if not found
|
||||
*/
|
||||
@Blocking
|
||||
public abstract byte @Nullable [] getMapData(@NotNull String serverName, int mapId);
|
||||
|
||||
/**
|
||||
* Reverse lookup: given a local map binding, find the origin server and map ID.
|
||||
*
|
||||
* @param serverName Name of the local server (to_server_name in the binding)
|
||||
* @param mapId Local map ID on this server (to_id in the binding)
|
||||
* @return Map.Entry with origin server name (key) and origin map ID (value), or null if not found
|
||||
*/
|
||||
@Blocking
|
||||
public abstract @Nullable Map.Entry<String, Integer> getMapBinding(@NotNull String serverName, int mapId);
|
||||
|
||||
/**
|
||||
* Bind map IDs across different servers
|
||||
*
|
||||
* @param fromServerName Name of the server the map originates from
|
||||
* @param fromMapId Original map ID
|
||||
* @param toServerName Name of the new server
|
||||
* @param toMapId New map ID
|
||||
*/
|
||||
@Blocking
|
||||
public abstract void setMapBinding(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName, int toMapId);
|
||||
|
||||
/**
|
||||
* Get map ID for the new server
|
||||
*
|
||||
* @param fromServerName Name of the server the map originates from
|
||||
* @param fromMapId Original map ID
|
||||
* @param toServerName Name of the new server
|
||||
* @return New map ID or -1 if not found
|
||||
*/
|
||||
@Blocking
|
||||
public abstract int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName);
|
||||
|
||||
/**
|
||||
* Wipes <b>all</b> {@link User} entries from the database.
|
||||
* <b>This should only be used when preparing tables for a data migration.</b>
|
||||
@@ -275,7 +348,9 @@ public abstract class Database {
|
||||
@Getter
|
||||
public enum TableName {
|
||||
USERS("husksync_users"),
|
||||
USER_DATA("husksync_user_data");
|
||||
USER_DATA("husksync_user_data"),
|
||||
MAP_DATA("husksync_map_data"),
|
||||
MAP_IDS("husksync_map_ids");
|
||||
|
||||
private final String defaultName;
|
||||
|
||||
|
||||
@@ -35,13 +35,11 @@ import org.bson.conversions.Bson;
|
||||
import org.bson.types.Binary;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.TimeZone;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class MongoDbDatabase extends Database {
|
||||
@@ -50,17 +48,17 @@ public class MongoDbDatabase extends Database {
|
||||
|
||||
private final String usersTable;
|
||||
private final String userDataTable;
|
||||
private final String mapDataTable;
|
||||
private final String mapIdsTable;
|
||||
|
||||
public MongoDbDatabase(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
this.usersTable = plugin.getSettings().getDatabase().getTableName(TableName.USERS);
|
||||
this.userDataTable = plugin.getSettings().getDatabase().getTableName(TableName.USER_DATA);
|
||||
this.mapDataTable = plugin.getSettings().getDatabase().getTableName(TableName.MAP_DATA);
|
||||
this.mapIdsTable = plugin.getSettings().getDatabase().getTableName(TableName.MAP_IDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the database and ensure tables are present; create tables if they do not exist.
|
||||
*
|
||||
* @throws IllegalStateException if the database could not be initialized
|
||||
*/
|
||||
@Override
|
||||
public void initialize() throws IllegalStateException {
|
||||
final Settings.DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
|
||||
@@ -68,12 +66,22 @@ public class MongoDbDatabase extends Database {
|
||||
ConnectionString URI = createConnectionURI(credentials);
|
||||
mongoConnectionHandler = new MongoConnectionHandler(URI, credentials.getDatabase());
|
||||
mongoCollectionHelper = new MongoCollectionHelper(mongoConnectionHandler);
|
||||
|
||||
// Check config for if tables should be created
|
||||
if (!plugin.getSettings().getDatabase().isCreateTables()) return;
|
||||
|
||||
if (mongoCollectionHelper.getCollection(usersTable) == null) {
|
||||
mongoCollectionHelper.createCollection(usersTable);
|
||||
}
|
||||
if (mongoCollectionHelper.getCollection(userDataTable) == null) {
|
||||
mongoCollectionHelper.createCollection(userDataTable);
|
||||
}
|
||||
if (mongoCollectionHelper.getCollection(mapDataTable) == null) {
|
||||
mongoCollectionHelper.createCollection(mapDataTable);
|
||||
}
|
||||
if (mongoCollectionHelper.getCollection(mapIdsTable) == null) {
|
||||
mongoCollectionHelper.createCollection(mapIdsTable);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " +
|
||||
"Please check the supplied database credentials in the config file", e);
|
||||
@@ -93,18 +101,13 @@ public class MongoDbDatabase extends Database {
|
||||
return new ConnectionString(baseURI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a {@link User} has an entry in the database and that their username is up-to-date
|
||||
*
|
||||
* @param user The {@link User} to ensure
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public void ensureUser(@NotNull User user) {
|
||||
try {
|
||||
getUser(user.getUuid()).ifPresentOrElse(
|
||||
existingUser -> {
|
||||
if (!existingUser.getUsername().equals(user.getUsername())) {
|
||||
if (!existingUser.getName().equals(user.getName())) {
|
||||
// Update a user's name if it has changed in the database
|
||||
try {
|
||||
Document filter = new Document("uuid", existingUser.getUuid());
|
||||
@@ -113,7 +116,7 @@ public class MongoDbDatabase extends Database {
|
||||
throw new MongoException("User document returned null!");
|
||||
}
|
||||
|
||||
Bson updates = Updates.set("username", user.getUsername());
|
||||
Bson updates = Updates.set("username", user.getName());
|
||||
mongoCollectionHelper.updateDocument(usersTable, doc, updates);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
@@ -123,7 +126,7 @@ public class MongoDbDatabase extends Database {
|
||||
() -> {
|
||||
// Insert new player data into the database
|
||||
try {
|
||||
Document doc = new Document("uuid", user.getUuid()).append("username", user.getUsername());
|
||||
Document doc = new Document("uuid", user.getUuid()).append("username", user.getName());
|
||||
mongoCollectionHelper.insertDocument(usersTable, doc);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
@@ -135,12 +138,6 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a player by their Minecraft account {@link UUID}
|
||||
*
|
||||
* @param uuid Minecraft account {@link UUID} of the {@link User} to get
|
||||
* @return An optional with the {@link User} present if they exist
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<User> getUser(@NotNull UUID uuid) {
|
||||
@@ -157,12 +154,6 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by their username (<i>case-insensitive</i>)
|
||||
*
|
||||
* @param username Username of the {@link User} to get (<i>case-insensitive</i>)
|
||||
* @return An optional with the {@link User} present if they exist
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<User> getUserByName(@NotNull String username) {
|
||||
@@ -180,12 +171,24 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest data snapshot for a user.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @return an optional containing the {@link DataSnapshot}, if it exists, or an empty optional if it does not
|
||||
*/
|
||||
@Override
|
||||
@NotNull
|
||||
public List<User> getAllUsers() {
|
||||
final List<User> users = Lists.newArrayList();
|
||||
try {
|
||||
final FindIterable<Document> doc = mongoCollectionHelper.getCollection(usersTable).find();
|
||||
for (Document document : doc) {
|
||||
users.add(new User(
|
||||
UUID.fromString(document.getString("uuid")),
|
||||
document.getString("username")
|
||||
));
|
||||
}
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get all users from the database", e);
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
||||
@@ -208,12 +211,6 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all {@link DataSnapshot} entries for a user from the database.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @return The list of a user's {@link DataSnapshot} entries
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
@NotNull
|
||||
@@ -237,13 +234,17 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @param versionUuid The UUID of the {@link DataSnapshot} entry to get
|
||||
* @return An optional containing the {@link DataSnapshot}, if it exists
|
||||
*/
|
||||
@Override
|
||||
public int getUnpinnedSnapshotCount(@NotNull User user) {
|
||||
try {
|
||||
Document filter = new Document("player_uuid", user.getUuid()).append("pinned", false);
|
||||
return (int) mongoCollectionHelper.getCollection(userDataTable).countDocuments(filter);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current snapshot count", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
@@ -265,27 +266,18 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal)</b> Prune user data for a given user to the maximum value as configured.
|
||||
*
|
||||
* @param user The user to prune data for
|
||||
* @implNote Data snapshots marked as {@code pinned} are exempt from rotation
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateSnapshots(@NotNull User user) {
|
||||
try {
|
||||
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
|
||||
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
|
||||
final int unpinnedSnapshots = getUnpinnedSnapshotCount(user);
|
||||
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
|
||||
if (unpinnedUserData.size() > maxSnapshots) {
|
||||
|
||||
if (unpinnedSnapshots > maxSnapshots) {
|
||||
Document filter = new Document("player_uuid", user.getUuid()).append("pinned", false);
|
||||
Document sort = new Document("timestamp", 1); // 1 = Ascending
|
||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable)
|
||||
.find(filter)
|
||||
.sort(sort)
|
||||
.limit(unpinnedUserData.size() - maxSnapshots);
|
||||
.find(filter).sort(sort)
|
||||
.limit(unpinnedSnapshots - maxSnapshots);
|
||||
|
||||
for (Document doc : iterable) {
|
||||
mongoCollectionHelper.deleteDocument(userDataTable, doc);
|
||||
@@ -296,12 +288,6 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @param versionUuid The UUID of the {@link DataSnapshot} entry to delete
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
@@ -319,14 +305,6 @@ public class MongoDbDatabase extends Database {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the most recent data snapshot by the given {@link User user}
|
||||
* The snapshot must have been created after {@link OffsetDateTime time} and NOT be pinned
|
||||
* Facilities the backup frequency feature, reducing redundant snapshots from being saved longer than needed
|
||||
*
|
||||
* @param user The user to delete a snapshot for
|
||||
* @param within The time to delete a snapshot after
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
|
||||
@@ -351,12 +329,6 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>Internal</b> - Create user data in the database
|
||||
*
|
||||
* @param user The user to add data for
|
||||
* @param data The {@link DataSnapshot} to set.
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
@@ -373,12 +345,6 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a saved {@link DataSnapshot} by given version UUID
|
||||
*
|
||||
* @param user The user whose data snapshot
|
||||
* @param data The {@link DataSnapshot} to update
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
@@ -395,10 +361,85 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipes <b>all</b> {@link User} entries from the database.
|
||||
* <b>This should only be used when preparing tables for a data migration.</b>
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public void saveMapData(@NotNull String serverName, int mapId, byte @NotNull [] data) {
|
||||
try {
|
||||
Document doc = new Document("server_name", serverName)
|
||||
.append("map_id", mapId)
|
||||
.append("data", new Binary(data));
|
||||
mongoCollectionHelper.insertDocument(mapDataTable, doc);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to write map data to the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public byte @Nullable [] getMapData(@NotNull String serverName, int mapId) {
|
||||
try {
|
||||
Document filter = new Document("server_name", serverName).append("map_id", mapId);
|
||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(mapDataTable).find(filter);
|
||||
Document doc = iterable.first();
|
||||
if (doc != null) {
|
||||
final Binary bin = doc.get("data", Binary.class);
|
||||
return bin.getData();
|
||||
}
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public @Nullable Map.Entry<String, Integer> getMapBinding(@NotNull String serverName, int mapId) {
|
||||
final Document filter = new Document("to_server_name", serverName).append("to_id", mapId);
|
||||
final FindIterable<Document> iterable = mongoCollectionHelper.getCollection(mapIdsTable).find(filter);
|
||||
final Document doc = iterable.first();
|
||||
if (doc != null) {
|
||||
return new AbstractMap.SimpleImmutableEntry<>(
|
||||
doc.getString("from_server_name"),
|
||||
doc.getInteger("from_id")
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public void setMapBinding(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName, int toMapId) {
|
||||
try {
|
||||
final Document doc = new Document("from_server_name", fromServerName)
|
||||
.append("from_id", fromMapId)
|
||||
.append("to_server_name", toServerName)
|
||||
.append("to_id", toMapId);
|
||||
mongoCollectionHelper.insertDocument(mapIdsTable, doc);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to connect map IDs in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName) {
|
||||
try {
|
||||
final Document filter = new Document("from_server_name", fromServerName)
|
||||
.append("from_id", fromMapId)
|
||||
.append("to_server_name", toServerName);
|
||||
final FindIterable<Document> iterable = mongoCollectionHelper.getCollection(mapIdsTable).find(filter);
|
||||
|
||||
final Document doc = iterable.first();
|
||||
if (doc != null) {
|
||||
return doc.getInteger("to_id");
|
||||
}
|
||||
return -1;
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get new map id from the database", e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public void wipeDatabase() {
|
||||
@@ -409,9 +450,6 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
@Override
|
||||
public void terminate() {
|
||||
if (mongoConnectionHandler != null) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
@@ -115,6 +116,9 @@ public class MySqlDatabase extends Database {
|
||||
);
|
||||
dataSource.setDataSourceProperties(properties);
|
||||
|
||||
// Check config for if tables should be created
|
||||
if (!plugin.getSettings().getDatabase().isCreateTables()) return;
|
||||
|
||||
// Prepare database schema; make tables if they don't exist
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
final String[] databaseSchema = getSchemaStatements(String.format("database/%s_schema.sql", flavor));
|
||||
@@ -137,7 +141,7 @@ public class MySqlDatabase extends Database {
|
||||
public void ensureUser(@NotNull User user) {
|
||||
getUser(user.getUuid()).ifPresentOrElse(
|
||||
existingUser -> {
|
||||
if (!existingUser.getUsername().equals(user.getUsername())) {
|
||||
if (!existingUser.getName().equals(user.getName())) {
|
||||
// Update a user's name if it has changed in the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
@@ -145,11 +149,12 @@ public class MySqlDatabase extends Database {
|
||||
SET `username`=?
|
||||
WHERE `uuid`=?"""))) {
|
||||
|
||||
statement.setString(1, user.getUsername());
|
||||
statement.setString(1, user.getName());
|
||||
statement.setString(2, existingUser.getUuid().toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
plugin.log(Level.INFO, "Updated " + user.getUsername() + "'s name in the database (" + existingUser.getUsername() + " -> " + user.getUsername() + ")");
|
||||
plugin.log(Level.INFO, "Updated " + user.getName() + "'s name in the database ("
|
||||
+ existingUser.getName() + " -> " + user.getName() + ")");
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e);
|
||||
}
|
||||
@@ -163,7 +168,7 @@ public class MySqlDatabase extends Database {
|
||||
VALUES (?,?);"""))) {
|
||||
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
statement.setString(2, user.getUsername());
|
||||
statement.setString(2, user.getName());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
@@ -218,6 +223,27 @@ public class MySqlDatabase extends Database {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<User> getAllUsers() {
|
||||
final List<User> users = Lists.newArrayList();
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `uuid`, `username`
|
||||
FROM `%users_table%`;
|
||||
"""))) {
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
users.add(new User(UUID.fromString(resultSet.getString("uuid")),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user by name from the database", e);
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
||||
@@ -273,11 +299,30 @@ public class MySqlDatabase extends Database {
|
||||
return retrievedData;
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's list of snapshots from the database", e);
|
||||
}
|
||||
return retrievedData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getUnpinnedSnapshotCount(@NotNull User user) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT COUNT(`version_uuid`)
|
||||
FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=? AND `pinned`=false;"""))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return resultSet.getInt(1);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current snapshot count", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
@@ -310,10 +355,9 @@ public class MySqlDatabase extends Database {
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateSnapshots(@NotNull User user) {
|
||||
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
|
||||
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
|
||||
final int unpinnedSnapshots = getUnpinnedSnapshotCount(user);
|
||||
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
|
||||
if (unpinnedUserData.size() > maxSnapshots) {
|
||||
if (unpinnedSnapshots > maxSnapshots) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM `%user_data_table%`
|
||||
@@ -321,7 +365,7 @@ public class MySqlDatabase extends Database {
|
||||
AND `pinned` IS FALSE
|
||||
ORDER BY `timestamp` ASC
|
||||
LIMIT %entry_count%;""".replace("%entry_count%",
|
||||
Integer.toString(unpinnedUserData.size() - maxSnapshots))))) {
|
||||
Integer.toString(unpinnedSnapshots - maxSnapshots))))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
@@ -409,6 +453,120 @@ public class MySqlDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public void saveMapData(@NotNull String serverName, int mapId, byte @NotNull [] data) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO `%map_data_table%`
|
||||
(`server_name`,`map_id`,`data`)
|
||||
VALUES (?,?,?);"""))) {
|
||||
statement.setString(1, serverName);
|
||||
statement.setInt(2, mapId);
|
||||
statement.setBlob(3, new ByteArrayInputStream(data));
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to write map data to the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public byte @Nullable [] getMapData(@NotNull String serverName, int mapId) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `data`
|
||||
FROM `%map_data_table%`
|
||||
WHERE `server_name`=? AND `map_id`=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, serverName);
|
||||
statement.setInt(2, mapId);
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final Blob blob = resultSet.getBlob("data");
|
||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
return dataByteArray;
|
||||
}
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public @Nullable Map.Entry<String, Integer> getMapBinding(@NotNull String serverName, int mapId) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `from_server_name`, `from_id`
|
||||
FROM `%map_ids_table%`
|
||||
WHERE `to_server_name`=? AND `to_id`=?
|
||||
LIMIT 1;
|
||||
"""))) {
|
||||
statement.setString(1, serverName);
|
||||
statement.setInt(2, mapId);
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return new AbstractMap.SimpleImmutableEntry<>(
|
||||
resultSet.getString("from_server_name"),
|
||||
resultSet.getInt("from_id")
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public void setMapBinding(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName, int toMapId) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO `%map_ids_table%`
|
||||
(`from_server_name`,`from_id`,`to_server_name`,`to_id`)
|
||||
VALUES (?,?,?,?);"""))) {
|
||||
statement.setString(1, fromServerName);
|
||||
statement.setInt(2, fromMapId);
|
||||
statement.setString(3, toServerName);
|
||||
statement.setInt(4, toMapId);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to connect map IDs in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `to_id`
|
||||
FROM `%map_ids_table%`
|
||||
WHERE `from_server_name`=? AND `from_id`=? AND `to_server_name`=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, fromServerName);
|
||||
statement.setInt(2, fromMapId);
|
||||
statement.setString(3, toServerName);
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return resultSet.getInt("to_id");
|
||||
}
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get new map id from the database", e);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void wipeDatabase() {
|
||||
try (Connection connection = getConnection()) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.*;
|
||||
@@ -51,12 +52,6 @@ public class PostgresDatabase extends Database {
|
||||
this.driverClass = "org.postgresql.Driver";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the auto-closeable connection from the hikariDataSource
|
||||
*
|
||||
* @return The {@link Connection} to the MySQL database
|
||||
* @throws SQLException if the connection fails for some reason
|
||||
*/
|
||||
@Blocking
|
||||
@NotNull
|
||||
private Connection getConnection() throws SQLException {
|
||||
@@ -114,6 +109,9 @@ public class PostgresDatabase extends Database {
|
||||
);
|
||||
dataSource.setDataSourceProperties(properties);
|
||||
|
||||
// Check config for if tables should be created
|
||||
if (!plugin.getSettings().getDatabase().isCreateTables()) return;
|
||||
|
||||
// Prepare database schema; make tables if they don't exist
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
final String[] databaseSchema = getSchemaStatements(String.format("database/%s_schema.sql", flavor));
|
||||
@@ -136,19 +134,19 @@ public class PostgresDatabase extends Database {
|
||||
public void ensureUser(@NotNull User user) {
|
||||
getUser(user.getUuid()).ifPresentOrElse(
|
||||
existingUser -> {
|
||||
if (!existingUser.getUsername().equals(user.getUsername())) {
|
||||
if (!existingUser.getName().equals(user.getName())) {
|
||||
// Update a user's name if it has changed in the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE "%users_table%"
|
||||
SET "username"=?
|
||||
WHERE "uuid"=?"""))) {
|
||||
UPDATE %users_table%
|
||||
SET username=?
|
||||
WHERE uuid=?;"""))) {
|
||||
|
||||
statement.setString(1, user.getUsername());
|
||||
statement.setString(1, user.getName());
|
||||
statement.setObject(2, existingUser.getUuid());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
plugin.log(Level.INFO, "Updated " + user.getUsername() + "'s name in the database (" + existingUser.getUsername() + " -> " + user.getUsername() + ")");
|
||||
plugin.log(Level.INFO, "Updated " + user.getName() + "'s name in the database (" + existingUser.getName() + " -> " + user.getName() + ")");
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e);
|
||||
}
|
||||
@@ -158,11 +156,11 @@ public class PostgresDatabase extends Database {
|
||||
// Insert new player data into the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO "%users_table%" ("uuid","username")
|
||||
INSERT INTO %users_table% (uuid,username)
|
||||
VALUES (?,?);"""))) {
|
||||
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.setString(2, user.getUsername());
|
||||
statement.setString(2, user.getName());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
@@ -177,9 +175,9 @@ public class PostgresDatabase extends Database {
|
||||
public Optional<User> getUser(@NotNull UUID uuid) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT "uuid", "username"
|
||||
FROM "%users_table%"
|
||||
WHERE "uuid"=?"""))) {
|
||||
SELECT uuid, username
|
||||
FROM %users_table%
|
||||
WHERE uuid=?;"""))) {
|
||||
|
||||
statement.setObject(1, uuid);
|
||||
|
||||
@@ -200,9 +198,9 @@ public class PostgresDatabase extends Database {
|
||||
public Optional<User> getUserByName(@NotNull String username) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT "uuid", "username"
|
||||
FROM "%users_table%"
|
||||
WHERE "username"=?"""))) {
|
||||
SELECT uuid, username
|
||||
FROM %users_table%
|
||||
WHERE username=?;"""))) {
|
||||
statement.setString(1, username);
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
@@ -217,15 +215,37 @@ public class PostgresDatabase extends Database {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<User> getAllUsers() {
|
||||
final List<User> users = Lists.newArrayList();
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT uuid, username
|
||||
FROM %users_table%;
|
||||
"""))) {
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
users.add(new User(UUID.fromString(resultSet.getString("uuid")),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user by name from the database", e);
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT "version_uuid", "timestamp", "data"
|
||||
FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=?
|
||||
ORDER BY "timestamp" DESC
|
||||
SELECT version_uuid, timestamp, data
|
||||
FROM %user_data_table%
|
||||
WHERE player_uuid=?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
@@ -251,10 +271,10 @@ public class PostgresDatabase extends Database {
|
||||
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT "version_uuid", "timestamp", "data"
|
||||
FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=?
|
||||
ORDER BY "timestamp" DESC;"""))) {
|
||||
SELECT version_uuid, timestamp, data
|
||||
FROM %user_data_table%
|
||||
WHERE player_uuid=?
|
||||
ORDER BY timestamp DESC;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
@@ -268,20 +288,39 @@ public class PostgresDatabase extends Database {
|
||||
return retrievedData;
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's list of snapshots from the database", e);
|
||||
}
|
||||
return retrievedData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getUnpinnedSnapshotCount(@NotNull User user) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT COUNT(version_uuid)
|
||||
FROM %user_data_table%
|
||||
WHERE player_uuid=? AND pinned=false;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return resultSet.getInt(1);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current snapshot count", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT "version_uuid", "timestamp", "data"
|
||||
FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=? AND "version_uuid"=?
|
||||
ORDER BY "timestamp" DESC
|
||||
SELECT version_uuid, timestamp, data
|
||||
FROM %user_data_table%
|
||||
WHERE player_uuid=? AND version_uuid=?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.setObject(2, versionUuid);
|
||||
@@ -303,18 +342,22 @@ public class PostgresDatabase extends Database {
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateSnapshots(@NotNull User user) {
|
||||
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
|
||||
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
|
||||
final int unpinnedSnapshots = getUnpinnedSnapshotCount(user);
|
||||
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
|
||||
if (unpinnedUserData.size() > maxSnapshots) {
|
||||
if (unpinnedSnapshots > maxSnapshots) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=?
|
||||
AND "pinned" = FALSE
|
||||
ORDER BY "timestamp" ASC
|
||||
LIMIT %entry_count%;""".replace("%entry_count%",
|
||||
Integer.toString(unpinnedUserData.size() - maxSnapshots))))) {
|
||||
WITH cte AS (
|
||||
SELECT version_uuid
|
||||
FROM %user_data_table%
|
||||
WHERE player_uuid=?
|
||||
AND pinned=FALSE
|
||||
ORDER BY timestamp ASC
|
||||
LIMIT %entry_count%
|
||||
)
|
||||
DELETE FROM %user_data_table%
|
||||
WHERE version_uuid IN (SELECT version_uuid FROM cte);""".replace("%entry_count%",
|
||||
Integer.toString(unpinnedSnapshots - maxSnapshots))))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
@@ -329,11 +372,10 @@ public class PostgresDatabase extends Database {
|
||||
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=? AND "version_uuid"=?
|
||||
LIMIT 1;"""))) {
|
||||
DELETE FROM %user_data_table%
|
||||
WHERE player_uuid=? AND version_uuid=?;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
statement.setObject(2, versionUuid);
|
||||
return statement.executeUpdate() > 0;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
@@ -347,12 +389,12 @@ public class PostgresDatabase extends Database {
|
||||
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=? AND "timestamp" = (
|
||||
SELECT "timestamp"
|
||||
FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=? AND "timestamp" > ? AND "pinned" = FALSE
|
||||
ORDER BY "timestamp" ASC
|
||||
DELETE FROM %user_data_table%
|
||||
WHERE player_uuid=? AND timestamp = (
|
||||
SELECT timestamp
|
||||
FROM %user_data_table%
|
||||
WHERE player_uuid=? AND timestamp > ? AND pinned=FALSE
|
||||
ORDER BY timestamp ASC
|
||||
LIMIT 1
|
||||
);"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
@@ -370,8 +412,8 @@ public class PostgresDatabase extends Database {
|
||||
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO "%user_data_table%"
|
||||
("player_uuid","version_uuid","timestamp","save_cause","pinned","data")
|
||||
INSERT INTO %user_data_table%
|
||||
(player_uuid,version_uuid,timestamp,save_cause,pinned,data)
|
||||
VALUES (?,?,?,?,?,?);"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.setObject(2, data.getId());
|
||||
@@ -391,10 +433,10 @@ public class PostgresDatabase extends Database {
|
||||
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE "%user_data_table%"
|
||||
SET "save_cause"=?,"pinned"=?,"data"=?
|
||||
WHERE "player_uuid"=? AND "version_uuid"=?
|
||||
LIMIT 1;"""))) {
|
||||
UPDATE %user_data_table%
|
||||
SET save_cause=?,pinned=?,data=?
|
||||
WHERE player_uuid=? AND version_uuid=?;
|
||||
"""))) {
|
||||
statement.setString(1, data.getSaveCause().name());
|
||||
statement.setBoolean(2, data.isPinned());
|
||||
statement.setBytes(3, data.asBytes(plugin));
|
||||
@@ -407,11 +449,122 @@ public class PostgresDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public void saveMapData(@NotNull String serverName, int mapId, byte @NotNull [] data) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO %map_data_table%
|
||||
(server_name,map_id,data)
|
||||
VALUES (?,?,?);"""))) {
|
||||
statement.setString(1, serverName);
|
||||
statement.setInt(2, mapId);
|
||||
statement.setBytes(3, data);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to write map data to the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public byte @Nullable [] getMapData(@NotNull String serverName, int mapId) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT data
|
||||
FROM %map_data_table%
|
||||
WHERE server_name=? AND map_id=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, serverName);
|
||||
statement.setInt(2, mapId);
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return resultSet.getBytes("data");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public @Nullable Map.Entry<String, Integer> getMapBinding(@NotNull String serverName, int mapId) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT from_server_name, from_id
|
||||
FROM %map_ids_table%
|
||||
WHERE to_server_name=? AND to_id=?
|
||||
LIMIT 1;
|
||||
"""))) {
|
||||
statement.setString(1, serverName);
|
||||
statement.setInt(2, mapId);
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return new AbstractMap.SimpleImmutableEntry<>(
|
||||
resultSet.getString("from_server_name"),
|
||||
resultSet.getInt("from_id")
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public void setMapBinding(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName, int toMapId) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO %map_ids_table%
|
||||
(from_server_name,from_id,to_server_name,to_id)
|
||||
VALUES (?,?,?,?);"""))) {
|
||||
statement.setString(1, fromServerName);
|
||||
statement.setInt(2, fromMapId);
|
||||
statement.setString(3, toServerName);
|
||||
statement.setInt(4, toMapId);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to connect map IDs in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT to_id
|
||||
FROM %map_ids_table%
|
||||
WHERE from_server_name=? AND from_id=? AND to_server_name=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, fromServerName);
|
||||
statement.setInt(2, fromMapId);
|
||||
statement.setString(3, toServerName);
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return resultSet.getInt("to_id");
|
||||
}
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get new map id from the database", e);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void wipeDatabase() {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
statement.executeUpdate(formatStatementTables("DELETE FROM \"%user_data_table%\";"));
|
||||
statement.executeUpdate(formatStatementTables("DELETE FROM %user_data_table%;"));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to wipe the database", e);
|
||||
|
||||
@@ -29,6 +29,7 @@ public class MongoCollectionHelper {
|
||||
|
||||
/**
|
||||
* Initialize the collection helper
|
||||
*
|
||||
* @param database Instance of {@link MongoConnectionHandler}
|
||||
*/
|
||||
public MongoCollectionHelper(@NotNull MongoConnectionHandler database) {
|
||||
@@ -37,6 +38,7 @@ public class MongoCollectionHelper {
|
||||
|
||||
/**
|
||||
* Create a collection
|
||||
*
|
||||
* @param collectionName the collection name
|
||||
*/
|
||||
public void createCollection(@NotNull String collectionName) {
|
||||
@@ -45,6 +47,7 @@ public class MongoCollectionHelper {
|
||||
|
||||
/**
|
||||
* Delete a collection
|
||||
*
|
||||
* @param collectionName the collection name
|
||||
*/
|
||||
public void deleteCollection(@NotNull String collectionName) {
|
||||
@@ -53,6 +56,7 @@ public class MongoCollectionHelper {
|
||||
|
||||
/**
|
||||
* Get a collection
|
||||
*
|
||||
* @param collectionName the collection name
|
||||
* @return MongoCollection<Document>
|
||||
*/
|
||||
@@ -62,8 +66,9 @@ public class MongoCollectionHelper {
|
||||
|
||||
/**
|
||||
* Add a document to a collection
|
||||
*
|
||||
* @param collectionName collection to add to
|
||||
* @param document Document to add
|
||||
* @param document Document to add
|
||||
*/
|
||||
public void insertDocument(@NotNull String collectionName, @NotNull Document document) {
|
||||
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
||||
@@ -72,9 +77,10 @@ public class MongoCollectionHelper {
|
||||
|
||||
/**
|
||||
* Update a document
|
||||
*
|
||||
* @param collectionName collection the document is in
|
||||
* @param document filter of document
|
||||
* @param updates Bson of updates
|
||||
* @param document filter of document
|
||||
* @param updates Bson of updates
|
||||
*/
|
||||
public void updateDocument(@NotNull String collectionName, @NotNull Document document, @NotNull Bson updates) {
|
||||
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
||||
@@ -83,8 +89,9 @@ public class MongoCollectionHelper {
|
||||
|
||||
/**
|
||||
* Delete a document
|
||||
*
|
||||
* @param collectionName collection the document is in
|
||||
* @param document filter to remove
|
||||
* @param document filter to remove
|
||||
*/
|
||||
public void deleteDocument(@NotNull String collectionName, @NotNull Document document) {
|
||||
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
||||
|
||||
@@ -35,9 +35,10 @@ public class MongoConnectionHandler {
|
||||
|
||||
/**
|
||||
* Initiate a connection to a Mongo Server
|
||||
*
|
||||
* @param uri The connection string
|
||||
*/
|
||||
public MongoConnectionHandler(@NotNull ConnectionString uri, @NotNull String databaseName) {
|
||||
public MongoConnectionHandler(@NotNull ConnectionString uri, @NotNull String databaseName) {
|
||||
try {
|
||||
final MongoClientSettings settings = MongoClientSettings.builder()
|
||||
.applyConnectionString(uri)
|
||||
@@ -48,7 +49,7 @@ public class MongoConnectionHandler {
|
||||
this.database = mongoClient.getDatabase(databaseName);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " +
|
||||
"Please check the supplied database credentials in the config file", e);
|
||||
"Please check the supplied database credentials in the config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,9 +25,7 @@ import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import static net.william278.husksync.config.Settings.SynchronizationSettings.SaveOnDeathSettings;
|
||||
|
||||
@@ -49,11 +47,12 @@ public abstract class EventListener {
|
||||
* @param user The {@link OnlineUser} to handle
|
||||
*/
|
||||
protected final void handlePlayerJoin(@NotNull OnlineUser user) {
|
||||
plugin.getDisconnectingPlayers().remove(user.getUuid());
|
||||
if (user.isNpc()) {
|
||||
return;
|
||||
}
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
plugin.getDataSyncer().setUserData(user);
|
||||
plugin.getDataSyncer().syncApplyUserData(user);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,11 +61,17 @@ public abstract class EventListener {
|
||||
* @param user The {@link OnlineUser} to handle
|
||||
*/
|
||||
protected final void handlePlayerQuit(@NotNull OnlineUser user) {
|
||||
if (user.isNpc() || plugin.isDisabling() || plugin.isLocked(user.getUuid())) {
|
||||
// Check the user is a user, the plugin isn't disabling, then mark as disconnecting
|
||||
if (user.isNpc() || plugin.isDisabling()) {
|
||||
return;
|
||||
}
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
plugin.getDataSyncer().saveUserData(user);
|
||||
plugin.getDisconnectingPlayers().add(user.getUuid());
|
||||
|
||||
// Lock, then save their data if the user is unlocked
|
||||
if (!plugin.isLocked(user.getUuid())) {
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
plugin.getDataSyncer().syncSaveUserData(user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,9 +84,9 @@ public abstract class EventListener {
|
||||
return;
|
||||
}
|
||||
usersInWorld.stream()
|
||||
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
|
||||
.forEach(user -> plugin.getDataSyncer().saveData(
|
||||
user, user.createSnapshot(DataSnapshot.SaveCause.WORLD_SAVE)
|
||||
.filter(user -> !user.isNpc() && !user.hasDisconnected() && !plugin.isLocked(user.getUuid()))
|
||||
.forEach(user -> plugin.getDataSyncer().saveCurrentUserData(
|
||||
user, DataSnapshot.SaveCause.WORLD_SAVE
|
||||
));
|
||||
}
|
||||
|
||||
@@ -98,8 +103,9 @@ public abstract class EventListener {
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't persist this to Redis for syncing, as this snapshot is from a state they won't be in post-respawn
|
||||
final DataSnapshot.Packed snapshot = user.createSnapshot(DataSnapshot.SaveCause.DEATH);
|
||||
snapshot.edit(plugin, (data -> data.getInventory().ifPresent(inventory -> inventory.setContents(items))));
|
||||
snapshot.edit(plugin, (data -> data.getInventory().ifPresent(inv -> inv.setContents(items))));
|
||||
plugin.getDataSyncer().saveData(user, snapshot);
|
||||
}
|
||||
|
||||
@@ -108,16 +114,12 @@ public abstract class EventListener {
|
||||
* Handle the plugin disabling
|
||||
*/
|
||||
public void handlePluginDisable() {
|
||||
// Save for all online players
|
||||
// Save for all online players.
|
||||
plugin.getOnlineUsers().stream()
|
||||
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
|
||||
.forEach(user -> {
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
plugin.getDataSyncer().saveData(
|
||||
user,
|
||||
user.createSnapshot(DataSnapshot.SaveCause.SERVER_SHUTDOWN),
|
||||
(saved, data) -> plugin.getRedisManager().clearUserData(saved)
|
||||
);
|
||||
plugin.getDataSyncer().saveCurrentUserData(user, DataSnapshot.SaveCause.SERVER_SHUTDOWN);
|
||||
});
|
||||
|
||||
// Close outstanding connections
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.gson.annotations.SerializedName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.mapdataapi.MapData;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class AdaptableMapData implements Adaptable {
|
||||
|
||||
@SerializedName("data")
|
||||
private final byte[] data;
|
||||
|
||||
public AdaptableMapData(@NotNull MapData data) {
|
||||
this(data.toBytes());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public MapData getData(int dataVersion) throws IOException {
|
||||
return MapData.fromByteArray(dataVersion, data);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,7 +27,10 @@ public enum RedisKeyType {
|
||||
|
||||
LATEST_SNAPSHOT,
|
||||
SERVER_SWITCH,
|
||||
DATA_CHECKOUT;
|
||||
DATA_CHECKOUT,
|
||||
MAP_ID,
|
||||
MAP_ID_REVERSED,
|
||||
MAP_DATA;
|
||||
|
||||
public static final int TTL_1_YEAR = 60 * 60 * 24 * 7 * 52; // 1 year
|
||||
public static final int TTL_10_SECONDS = 10; // 10 seconds
|
||||
|
||||
@@ -25,6 +25,7 @@ import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import redis.clients.jedis.*;
|
||||
import redis.clients.jedis.exceptions.JedisException;
|
||||
import redis.clients.jedis.util.Pool;
|
||||
@@ -61,41 +62,62 @@ public class RedisManager extends JedisPubSub {
|
||||
/**
|
||||
* Initialize Redis connection pool
|
||||
*/
|
||||
|
||||
@Blocking
|
||||
public void initialize() throws IllegalStateException {
|
||||
final Settings.RedisSettings.RedisCredentials credentials = plugin.getSettings().getRedis().getCredentials();
|
||||
|
||||
final String user = credentials.getUser();
|
||||
final String password = credentials.getPassword();
|
||||
final String host = credentials.getHost();
|
||||
final int port = credentials.getPort();
|
||||
final int database = credentials.getDatabase();
|
||||
final boolean useSSL = credentials.isUseSsl();
|
||||
|
||||
// Create the jedis pool
|
||||
// Configure JedisPoolConfig
|
||||
final JedisPoolConfig config = new JedisPoolConfig();
|
||||
config.setMaxIdle(0);
|
||||
config.setTestOnBorrow(true);
|
||||
config.setTestOnReturn(true);
|
||||
config.setMaxTotal(credentials.getMaxTotalConnections());
|
||||
config.setMaxIdle(credentials.getMaxIdleConnections());
|
||||
config.setMinIdle(credentials.getMinIdleConnections());
|
||||
config.setTestOnBorrow(credentials.isTestOnBorrow());
|
||||
config.setTestOnReturn(credentials.isTestOnReturn());
|
||||
config.setTestWhileIdle(credentials.isTestWhileIdle());
|
||||
config.setMinEvictableIdleTimeMillis(credentials.getMinEvictableIdleTimeMillis());
|
||||
config.setTimeBetweenEvictionRunsMillis(credentials.getTimeBetweenEvictionRunsMillis());
|
||||
|
||||
final Settings.RedisSettings.RedisSentinel sentinel = plugin.getSettings().getRedis().getSentinel();
|
||||
Set<String> redisSentinelNodes = new HashSet<>(sentinel.getNodes());
|
||||
|
||||
if (redisSentinelNodes.isEmpty()) {
|
||||
this.jedisPool = password.isEmpty()
|
||||
? new JedisPool(config, host, port, 0, useSSL)
|
||||
: new JedisPool(config, host, port, 0, password, useSSL);
|
||||
// Standalone Redis setup
|
||||
DefaultJedisClientConfig.Builder clientConfigBuilder = DefaultJedisClientConfig.builder()
|
||||
.ssl(useSSL)
|
||||
.database(database)
|
||||
.timeoutMillis(credentials.getConnectionTimeout()) // connection and socket timeout combined
|
||||
.user(user.isEmpty() ? null : user)
|
||||
.password(password.isEmpty() ? null : password);
|
||||
|
||||
this.jedisPool = new JedisPool(config, new HostAndPort(host, port), clientConfigBuilder.build());
|
||||
} else {
|
||||
final String sentinelPassword = sentinel.getPassword();
|
||||
this.jedisPool = new JedisSentinelPool(sentinel.getMaster(), redisSentinelNodes, password.isEmpty()
|
||||
? null : password, sentinelPassword.isEmpty() ? null : sentinelPassword);
|
||||
this.jedisPool = new JedisSentinelPool(
|
||||
sentinel.getMaster(),
|
||||
redisSentinelNodes,
|
||||
config,
|
||||
credentials.getConnectionTimeout(),
|
||||
credentials.getSocketTimeout(),
|
||||
password.isEmpty() ? null : password,
|
||||
sentinelPassword.isEmpty() ? null : sentinelPassword,
|
||||
database);
|
||||
}
|
||||
|
||||
// Ping the server to check the connection
|
||||
try {
|
||||
jedisPool.getResource().ping();
|
||||
try (var jedis = jedisPool.getResource()) {
|
||||
jedis.ping();
|
||||
} catch (JedisException e) {
|
||||
throw new IllegalStateException("Failed to establish connection with Redis. "
|
||||
+ "Please check the supplied credentials in the config file", e);
|
||||
throw new IllegalStateException("Failed to establish connection with Redis. " +
|
||||
"Please check the supplied credentials in the config file", e);
|
||||
}
|
||||
|
||||
// Subscribe using a thread (rather than a task)
|
||||
enabled = true;
|
||||
new Thread(this::subscribe, "husksync:redis_subscriber").start();
|
||||
}
|
||||
@@ -112,8 +134,7 @@ public class RedisManager extends JedisPubSub {
|
||||
this,
|
||||
Arrays.stream(RedisMessage.Type.values())
|
||||
.map(type -> type.getMessageChannel(clusterId))
|
||||
.toArray(String[]::new)
|
||||
);
|
||||
.toArray(String[]::new));
|
||||
} catch (Throwable t) {
|
||||
// Thread was unlocked due error
|
||||
onThreadUnlock(t);
|
||||
@@ -157,27 +178,47 @@ public class RedisManager extends JedisPubSub {
|
||||
|
||||
final RedisMessage redisMessage = RedisMessage.fromJson(plugin, message);
|
||||
switch (messageType) {
|
||||
case UPDATE_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
|
||||
case UPDATE_USER_DATA -> redisMessage.getTargetUser(plugin).ifPresent(
|
||||
user -> {
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
try {
|
||||
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload());
|
||||
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin,
|
||||
redisMessage.getPayload());
|
||||
user.applySnapshot(data, DataSnapshot.UpdateCause.UPDATED);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred updating user data from Redis", e);
|
||||
user.completeSync(false, DataSnapshot.UpdateCause.UPDATED, plugin);
|
||||
}
|
||||
}
|
||||
);
|
||||
case REQUEST_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
|
||||
});
|
||||
case REQUEST_USER_DATA -> redisMessage.getTargetUser(plugin).ifPresent(
|
||||
user -> RedisMessage.create(
|
||||
UUID.fromString(new String(redisMessage.getPayload(), StandardCharsets.UTF_8)),
|
||||
user.createSnapshot(DataSnapshot.SaveCause.INVENTORY_COMMAND).asBytes(plugin)
|
||||
).dispatch(plugin, RedisMessage.Type.RETURN_USER_DATA)
|
||||
);
|
||||
user.createSnapshot(DataSnapshot.SaveCause.INVENTORY_COMMAND).asBytes(plugin))
|
||||
.dispatch(plugin, RedisMessage.Type.RETURN_USER_DATA));
|
||||
case CHECK_IN_PETITION -> {
|
||||
if (!redisMessage.isTargetServer(plugin)
|
||||
|| !plugin.getSettings().getSynchronization().isCheckinPetitions()) {
|
||||
return;
|
||||
}
|
||||
final String payload = new String(redisMessage.getPayload(), StandardCharsets.UTF_8);
|
||||
final User user = new User(UUID.fromString(payload.split("/")[0]), payload.split("/")[1]);
|
||||
|
||||
// Only release checkout if user is truly offline AND not being processed
|
||||
final boolean isOnline = plugin.getOnlineUser(user.getUuid()).isPresent();
|
||||
final boolean isLocked = plugin.isLocked(user.getUuid());
|
||||
|
||||
if (isOnline || isLocked) {
|
||||
plugin.debug("[%s] Petition ignored - user still being processed (online=%s, locked=%s)"
|
||||
.formatted(user.getName(), isOnline, isLocked));
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getRedisManager().setUserCheckedOut(user, false);
|
||||
plugin.debug("[%s] Petition accepted - user checked in".formatted(user.getName()));
|
||||
}
|
||||
case RETURN_USER_DATA -> {
|
||||
final CompletableFuture<Optional<DataSnapshot.Packed>> future = pendingRequests.get(
|
||||
redisMessage.getTargetUuid()
|
||||
);
|
||||
final UUID target = redisMessage.getTargetUuid().orElse(null);
|
||||
final CompletableFuture<Optional<DataSnapshot.Packed>> future = pendingRequests.get(target);
|
||||
if (future != null) {
|
||||
try {
|
||||
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload());
|
||||
@@ -186,7 +227,7 @@ public class RedisManager extends JedisPubSub {
|
||||
plugin.log(Level.SEVERE, "An exception occurred returning user data from Redis", e);
|
||||
future.complete(Optional.empty());
|
||||
}
|
||||
pendingRequests.remove(redisMessage.getTargetUuid());
|
||||
pendingRequests.remove(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,58 +250,58 @@ public class RedisManager extends JedisPubSub {
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
public void sendUserDataUpdate(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
plugin.runAsync(() -> {
|
||||
final RedisMessage redisMessage = RedisMessage.create(user.getUuid(), data.asBytes(plugin));
|
||||
redisMessage.dispatch(plugin, RedisMessage.Type.UPDATE_USER_DATA);
|
||||
});
|
||||
final RedisMessage redisMessage = RedisMessage.create(user.getUuid(), data.asBytes(plugin));
|
||||
redisMessage.dispatch(plugin, RedisMessage.Type.UPDATE_USER_DATA);
|
||||
}
|
||||
|
||||
public CompletableFuture<Optional<DataSnapshot.Packed>> getUserData(@NotNull UUID requestId, @NotNull User user) {
|
||||
@Blocking
|
||||
public void petitionServerCheckin(@NotNull String server, @NotNull User user) {
|
||||
final RedisMessage redisMessage = RedisMessage.create(
|
||||
server, "%s/%s".formatted(user.getUuid(), user.getName()).getBytes(StandardCharsets.UTF_8));
|
||||
redisMessage.dispatch(plugin, RedisMessage.Type.CHECK_IN_PETITION);
|
||||
}
|
||||
|
||||
public CompletableFuture<Optional<DataSnapshot.Packed>> getOnlineUserData(@NotNull UUID requestId,
|
||||
@NotNull User user,
|
||||
@NotNull DataSnapshot.SaveCause saveCause) {
|
||||
return plugin.getOnlineUser(user.getUuid())
|
||||
.map(online -> CompletableFuture.completedFuture(
|
||||
Optional.of(online.createSnapshot(DataSnapshot.SaveCause.API)))
|
||||
)
|
||||
.orElse(this.requestData(requestId, user));
|
||||
Optional.of(online.createSnapshot(saveCause))))
|
||||
.orElse(this.getNetworkedUserData(requestId, user));
|
||||
}
|
||||
|
||||
private CompletableFuture<Optional<DataSnapshot.Packed>> requestData(@NotNull UUID requestId, @NotNull User user) {
|
||||
// Request a user's dat x-server
|
||||
private CompletableFuture<Optional<DataSnapshot.Packed>> getNetworkedUserData(@NotNull UUID requestId,
|
||||
@NotNull User user) {
|
||||
final CompletableFuture<Optional<DataSnapshot.Packed>> future = new CompletableFuture<>();
|
||||
pendingRequests.put(requestId, future);
|
||||
plugin.runAsync(() -> {
|
||||
final RedisMessage redisMessage = RedisMessage.create(
|
||||
user.getUuid(),
|
||||
requestId.toString().getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
requestId.toString().getBytes(StandardCharsets.UTF_8));
|
||||
redisMessage.dispatch(plugin, RedisMessage.Type.REQUEST_USER_DATA);
|
||||
});
|
||||
return future
|
||||
.orTimeout(
|
||||
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds(),
|
||||
TimeUnit.MILLISECONDS
|
||||
)
|
||||
TimeUnit.MILLISECONDS)
|
||||
.exceptionally(throwable -> {
|
||||
pendingRequests.remove(requestId);
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a user's data to Redis
|
||||
*
|
||||
* @param user the user to set data for
|
||||
* @param data the user's data to set
|
||||
* @param timeToLive The time to cache the data for
|
||||
*/
|
||||
// Set a user's data to Redis
|
||||
@Blocking
|
||||
public void setUserData(@NotNull User user, @NotNull DataSnapshot.Packed data, int timeToLive) {
|
||||
public void setUserData(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.setex(
|
||||
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId),
|
||||
timeToLive,
|
||||
data.asBytes(plugin)
|
||||
);
|
||||
plugin.debug(String.format("[%s] Set %s key on Redis", user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
|
||||
RedisKeyType.TTL_1_YEAR,
|
||||
data.asBytes(plugin));
|
||||
plugin.debug(String.format("[%s] Set %s key on Redis", user.getName(), RedisKeyType.LATEST_SNAPSHOT));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred setting user data on Redis", e);
|
||||
}
|
||||
@@ -270,9 +311,8 @@ public class RedisManager extends JedisPubSub {
|
||||
public void clearUserData(@NotNull User user) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.del(
|
||||
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId)
|
||||
);
|
||||
plugin.debug(String.format("[%s] Cleared %s on Redis", user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
|
||||
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId));
|
||||
plugin.debug(String.format("[%s] Cleared %s on Redis", user.getName(), RedisKeyType.LATEST_SNAPSHOT));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred clearing user data on Redis", e);
|
||||
}
|
||||
@@ -281,16 +321,20 @@ public class RedisManager extends JedisPubSub {
|
||||
@Blocking
|
||||
public void setUserCheckedOut(@NotNull User user, boolean checkedOut) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
final String key = getKeyString(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId);
|
||||
if (checkedOut) {
|
||||
jedis.set(
|
||||
getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId),
|
||||
plugin.getServerName().getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
key.getBytes(StandardCharsets.UTF_8),
|
||||
plugin.getServerName().getBytes(StandardCharsets.UTF_8));
|
||||
} else {
|
||||
jedis.del(getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId));
|
||||
if (jedis.del(key.getBytes(StandardCharsets.UTF_8)) == 0) {
|
||||
plugin.debug(String.format("[%s] %s key not set on Redis when attempting removal (%s)",
|
||||
user.getName(), RedisKeyType.DATA_CHECKOUT, key));
|
||||
return;
|
||||
}
|
||||
}
|
||||
plugin.debug(String.format("[%s] %s %s key to/from Redis", user.getUsername(),
|
||||
checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT));
|
||||
plugin.debug(String.format("[%s] %s %s key %s Redis (%s)", user.getName(),
|
||||
checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT, checkedOut ? "to" : "from", key));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred setting checkout to", e);
|
||||
}
|
||||
@@ -304,13 +348,13 @@ public class RedisManager extends JedisPubSub {
|
||||
if (readData != null) {
|
||||
final String checkoutServer = new String(readData, StandardCharsets.UTF_8);
|
||||
plugin.debug(String.format("[%s] Waiting for %s %s key to be unset on Redis",
|
||||
user.getUsername(), checkoutServer, RedisKeyType.DATA_CHECKOUT));
|
||||
user.getName(), checkoutServer, RedisKeyType.DATA_CHECKOUT));
|
||||
return Optional.of(checkoutServer);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred getting a user's checkout key from Redis", e);
|
||||
}
|
||||
plugin.debug(String.format("[%s] %s key not set on Redis", user.getUsername(),
|
||||
plugin.debug(String.format("[%s] %s key not set on Redis", user.getName(),
|
||||
RedisKeyType.DATA_CHECKOUT));
|
||||
return Optional.empty();
|
||||
}
|
||||
@@ -345,10 +389,9 @@ public class RedisManager extends JedisPubSub {
|
||||
jedis.setex(
|
||||
getKey(RedisKeyType.SERVER_SWITCH, user.getUuid(), clusterId),
|
||||
RedisKeyType.TTL_10_SECONDS,
|
||||
new byte[0]
|
||||
);
|
||||
new byte[0]);
|
||||
plugin.debug(String.format("[%s] Set %s key to Redis",
|
||||
user.getUsername(), RedisKeyType.SERVER_SWITCH));
|
||||
user.getName(), RedisKeyType.SERVER_SWITCH));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred setting a user's server switch key from Redis", e);
|
||||
}
|
||||
@@ -358,7 +401,8 @@ public class RedisManager extends JedisPubSub {
|
||||
* Fetch a user's data from Redis and consume the key if found
|
||||
*
|
||||
* @param user The user to fetch data for
|
||||
* @return The user's data, if it's present on the database. Otherwise, an empty optional.
|
||||
* @return The user's data, if it's present on the database. Otherwise, an empty
|
||||
* optional.
|
||||
*/
|
||||
@Blocking
|
||||
public Optional<DataSnapshot.Packed> getUserData(@NotNull User user) {
|
||||
@@ -367,11 +411,11 @@ public class RedisManager extends JedisPubSub {
|
||||
final byte[] dataByteArray = jedis.get(key);
|
||||
if (dataByteArray == null) {
|
||||
plugin.debug(String.format("[%s] Waiting for %s key from Redis",
|
||||
user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
|
||||
user.getName(), RedisKeyType.LATEST_SNAPSHOT));
|
||||
return Optional.empty();
|
||||
}
|
||||
plugin.debug(String.format("[%s] Read %s key from Redis",
|
||||
user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
|
||||
user.getName(), RedisKeyType.LATEST_SNAPSHOT));
|
||||
|
||||
// Consume the key (delete from redis)
|
||||
jedis.del(key);
|
||||
@@ -391,11 +435,11 @@ public class RedisManager extends JedisPubSub {
|
||||
final byte[] readData = jedis.get(key);
|
||||
if (readData == null) {
|
||||
plugin.debug(String.format("[%s] Waiting for %s key from Redis",
|
||||
user.getUsername(), RedisKeyType.SERVER_SWITCH));
|
||||
user.getName(), RedisKeyType.SERVER_SWITCH));
|
||||
return false;
|
||||
}
|
||||
plugin.debug(String.format("[%s] Read %s key from Redis",
|
||||
user.getUsername(), RedisKeyType.SERVER_SWITCH));
|
||||
user.getName(), RedisKeyType.SERVER_SWITCH));
|
||||
|
||||
// Consume the key (delete from redis)
|
||||
jedis.del(key);
|
||||
@@ -406,6 +450,121 @@ public class RedisManager extends JedisPubSub {
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
public String getStatusDump() {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
return jedis.info();
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
public long getLatency() {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.ping();
|
||||
return System.currentTimeMillis() - startTime;
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
public String getVersion() {
|
||||
final String info = getStatusDump();
|
||||
for (String line : info.split("\n")) {
|
||||
if (line.startsWith("redis_version:")) {
|
||||
return line.split(":")[1];
|
||||
}
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
@Blocking
|
||||
public void bindMapIds(@NotNull String fromServer, int fromId, @NotNull String toServer, int toId) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.setex(
|
||||
getMapIdKey(fromServer, fromId, toServer, clusterId),
|
||||
RedisKeyType.TTL_1_YEAR,
|
||||
String.valueOf(toId).getBytes(StandardCharsets.UTF_8));
|
||||
jedis.setex(
|
||||
getReversedMapIdKey(toServer, toId, clusterId),
|
||||
RedisKeyType.TTL_1_YEAR,
|
||||
String.format("%s:%s", fromServer, fromId).getBytes(StandardCharsets.UTF_8));
|
||||
plugin.debug(String.format("Bound map %s:%s -> %s:%s on Redis", fromServer, fromId, toServer, toId));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred binding map ids on Redis", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
public Optional<Integer> getBoundMapId(@NotNull String fromServer, int fromId, @NotNull String toServer) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
final byte[] readData = jedis.get(getMapIdKey(fromServer, fromId, toServer, clusterId));
|
||||
if (readData == null) {
|
||||
plugin.debug(String.format("[%s:%s] No bound map id for server %s Redis",
|
||||
fromServer, fromId, toServer));
|
||||
return Optional.empty();
|
||||
}
|
||||
plugin.debug(String.format("[%s:%s] Read bound map id for server %s from Redis",
|
||||
fromServer, fromId, toServer));
|
||||
|
||||
return Optional.of(Integer.parseInt(new String(readData, StandardCharsets.UTF_8)));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred getting bound map id from Redis", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
public @Nullable Map.Entry<String, Integer> getReversedMapBound(@NotNull String toServer, int toId) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
final byte[] readData = jedis.get(getReversedMapIdKey(toServer, toId, clusterId));
|
||||
if (readData == null) {
|
||||
plugin.debug(String.format("[%s:%s] No reversed map bound on Redis",
|
||||
toServer, toId));
|
||||
return null;
|
||||
}
|
||||
plugin.debug(String.format("[%s:%s] Read reversed map bound from Redis",
|
||||
toServer, toId));
|
||||
|
||||
String[] parts = new String(readData, StandardCharsets.UTF_8).split(":");
|
||||
return Map.entry(parts[0], Integer.parseInt(parts[1]));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred reading reversed map bound from Redis", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
public void setMapData(@NotNull String serverName, int mapId, byte[] data) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.setex(
|
||||
getMapDataKey(serverName, mapId, clusterId),
|
||||
RedisKeyType.TTL_1_YEAR,
|
||||
data);
|
||||
plugin.debug(String.format("Set map data %s:%s on Redis", serverName, mapId));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred setting map data on Redis", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
public byte @Nullable [] getMapData(@NotNull String serverName, int mapId) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
final byte[] readData = jedis.get(getMapDataKey(serverName, mapId, clusterId));
|
||||
if (readData == null) {
|
||||
plugin.debug(String.format("[%s:%s] No map data on Redis",
|
||||
serverName, mapId));
|
||||
return null;
|
||||
}
|
||||
plugin.debug(String.format("[%s:%s] Read map data from Redis",
|
||||
serverName, mapId));
|
||||
|
||||
return readData;
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred reading map data from Redis", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
public void terminate() {
|
||||
enabled = false;
|
||||
@@ -418,7 +577,28 @@ public class RedisManager extends JedisPubSub {
|
||||
}
|
||||
|
||||
private static byte[] getKey(@NotNull RedisKeyType keyType, @NotNull UUID uuid, @NotNull String clusterId) {
|
||||
return String.format("%s:%s", keyType.getKeyPrefix(clusterId), uuid).getBytes(StandardCharsets.UTF_8);
|
||||
return getKeyString(keyType, uuid, clusterId).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String getKeyString(@NotNull RedisKeyType keyType, @NotNull UUID uuid, @NotNull String clusterId) {
|
||||
return String.format("%s:%s", keyType.getKeyPrefix(clusterId), uuid);
|
||||
}
|
||||
|
||||
private static byte[] getMapIdKey(@NotNull String fromServer, int fromId, @NotNull String toServer,
|
||||
@NotNull String clusterId) {
|
||||
return String.format("%s:%s:%s:%s", RedisKeyType.MAP_ID.getKeyPrefix(clusterId), fromServer, fromId, toServer)
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static byte[] getReversedMapIdKey(@NotNull String toServer, int toId, @NotNull String clusterId) {
|
||||
return String.format("%s:%s:%s", RedisKeyType.MAP_ID_REVERSED.getKeyPrefix(clusterId), toServer, toId)
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static byte[] getMapDataKey(@NotNull String serverName, int mapId, @NotNull String clusterId) {
|
||||
return String.format("%s:%s:%s", RedisKeyType.MAP_DATA.getKeyPrefix(clusterId), serverName, mapId)
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,25 +21,42 @@ package net.william278.husksync.redis;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Setter
|
||||
public class RedisMessage implements Adaptable {
|
||||
|
||||
private @Nullable String targetServer;
|
||||
@SerializedName("target_uuid")
|
||||
private UUID targetUuid;
|
||||
private @Nullable UUID targetUuid;
|
||||
@Getter
|
||||
@Setter
|
||||
@SerializedName("payload")
|
||||
private byte[] payload;
|
||||
|
||||
private RedisMessage(byte[] payload) {
|
||||
setPayload(payload);
|
||||
}
|
||||
|
||||
private RedisMessage(@NotNull UUID targetUuid, byte[] message) {
|
||||
this(message);
|
||||
this.setTargetUuid(targetUuid);
|
||||
this.setPayload(message);
|
||||
}
|
||||
|
||||
private RedisMessage(@NotNull String targetServer, byte[] message) {
|
||||
this(message);
|
||||
this.setTargetServer(targetServer);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@@ -51,6 +68,11 @@ public class RedisMessage implements Adaptable {
|
||||
return new RedisMessage(targetUuid, message);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static RedisMessage create(@NotNull String targetServer, byte[] message) {
|
||||
return new RedisMessage(targetServer, message);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static RedisMessage fromJson(@NotNull HuskSync plugin, @NotNull String json) throws JsonSyntaxException {
|
||||
return plugin.getGson().fromJson(json, RedisMessage.class);
|
||||
@@ -63,28 +85,23 @@ public class RedisMessage implements Adaptable {
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public UUID getTargetUuid() {
|
||||
return targetUuid;
|
||||
public Optional<UUID> getTargetUuid() {
|
||||
return Optional.ofNullable(targetUuid);
|
||||
}
|
||||
|
||||
public void setTargetUuid(@NotNull UUID targetUuid) {
|
||||
this.targetUuid = targetUuid;
|
||||
public Optional<OnlineUser> getTargetUser(@NotNull HuskSync plugin) {
|
||||
return getTargetUuid().flatMap(plugin::getOnlineUser);
|
||||
}
|
||||
|
||||
public byte[] getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
public void setPayload(byte[] payload) {
|
||||
this.payload = payload;
|
||||
public boolean isTargetServer(@NotNull HuskSync plugin) {
|
||||
return targetServer != null && targetServer.equals(plugin.getServerName());
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
|
||||
UPDATE_USER_DATA,
|
||||
REQUEST_USER_DATA,
|
||||
RETURN_USER_DATA;
|
||||
RETURN_USER_DATA,
|
||||
CHECK_IN_PETITION;
|
||||
|
||||
@NotNull
|
||||
public String getMessageChannel(@NotNull String clusterId) {
|
||||
|
||||
@@ -81,18 +81,28 @@ public abstract class DataSyncer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a user's data should be fetched and applied to them
|
||||
* Called when a user's data should be fetched and applied to them as part of a synchronization process
|
||||
*
|
||||
* @param user the user to fetch data for
|
||||
*/
|
||||
public abstract void setUserData(@NotNull OnlineUser user);
|
||||
public abstract void syncApplyUserData(@NotNull OnlineUser user);
|
||||
|
||||
/**
|
||||
* Called when a user's data should be serialized and saved
|
||||
* Called when a user's data should be serialized and saved as part of a synchronization process
|
||||
*
|
||||
* @param user the user to save
|
||||
*/
|
||||
public abstract void saveUserData(@NotNull OnlineUser user);
|
||||
public abstract void syncSaveUserData(@NotNull OnlineUser user);
|
||||
|
||||
/**
|
||||
* Save a user's current data
|
||||
*
|
||||
* @param onlineUser the user to save data of
|
||||
* @param cause the save cause
|
||||
*/
|
||||
public void saveCurrentUserData(@NotNull OnlineUser onlineUser, @NotNull DataSnapshot.SaveCause cause) {
|
||||
this.saveData(onlineUser, onlineUser.createSnapshot(cause), getRedis()::setUserData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a {@link DataSnapshot.Packed user's data snapshot} to the database,
|
||||
@@ -163,7 +173,7 @@ public abstract class DataSyncer {
|
||||
() -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin)
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.WARNING, "Failed to set %s's data from the database".formatted(user.getUsername()), e);
|
||||
plugin.log(Level.WARNING, "Failed to set %s's data from the database".formatted(user.getName()), e);
|
||||
user.completeSync(false, DataSnapshot.UpdateCause.SYNCHRONIZED, plugin);
|
||||
}
|
||||
}
|
||||
@@ -175,7 +185,7 @@ public abstract class DataSyncer {
|
||||
final AtomicReference<Task.Repeating> task = new AtomicReference<>();
|
||||
final AtomicBoolean processing = new AtomicBoolean(false);
|
||||
final Runnable runnable = () -> {
|
||||
if (user.isOffline()) {
|
||||
if (user.cannotApplySnapshot()) {
|
||||
task.get().cancel();
|
||||
return;
|
||||
}
|
||||
@@ -188,7 +198,7 @@ public abstract class DataSyncer {
|
||||
if (plugin.isDisabling() || timesRun.getAndIncrement() > maxListenAttempts) {
|
||||
task.get().cancel();
|
||||
plugin.debug(String.format("[%s] Redis timed out after %s attempts; setting from database",
|
||||
user.getUsername(), timesRun.get()));
|
||||
user.getName(), timesRun.get()));
|
||||
setUserFromDatabase(user);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ package net.william278.husksync.sync;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.redis.RedisKeyType;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -35,7 +34,7 @@ public class DelayDataSyncer extends DataSyncer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserData(@NotNull OnlineUser user) {
|
||||
public void syncApplyUserData(@NotNull OnlineUser user) {
|
||||
plugin.runAsyncDelayed(
|
||||
() -> {
|
||||
// Fetch from the database if the user isn't changing servers
|
||||
@@ -58,12 +57,15 @@ public class DelayDataSyncer extends DataSyncer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveUserData(@NotNull OnlineUser onlineUser) {
|
||||
public void syncSaveUserData(@NotNull OnlineUser onlineUser) {
|
||||
plugin.runAsync(() -> {
|
||||
getRedis().setUserServerSwitch(onlineUser);
|
||||
saveData(
|
||||
onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
|
||||
(user, data) -> getRedis().setUserData(user, data, RedisKeyType.TTL_10_SECONDS)
|
||||
(user, data) -> {
|
||||
getRedis().setUserData(user, data);
|
||||
plugin.unlockPlayer(user.getUuid());
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,10 +21,11 @@ package net.william278.husksync.sync;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.redis.RedisKeyType;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class LockstepDataSyncer extends DataSyncer {
|
||||
|
||||
public LockstepDataSyncer(@NotNull HuskSync plugin) {
|
||||
@@ -43,11 +44,23 @@ public class LockstepDataSyncer extends DataSyncer {
|
||||
|
||||
// Consume their data when they are checked in
|
||||
@Override
|
||||
public void setUserData(@NotNull OnlineUser user) {
|
||||
public void syncApplyUserData(@NotNull OnlineUser user) {
|
||||
this.listenForRedisData(user, () -> {
|
||||
if (getRedis().getUserCheckedOut(user).isPresent()) {
|
||||
if (user.cannotApplySnapshot()) {
|
||||
plugin.debug("Not checking data state for user who has gone offline: %s".formatted(user.getName()));
|
||||
return false;
|
||||
}
|
||||
|
||||
// If they are checked out, ask the server to check them back in and return false
|
||||
final Optional<String> server = getRedis().getUserCheckedOut(user);
|
||||
if (server.isPresent() && !server.get().equals(plugin.getServerName())) {
|
||||
if (plugin.getSettings().getSynchronization().isCheckinPetitions()) {
|
||||
getRedis().petitionServerCheckin(server.get(), user);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// If they are checked in - or checked out on *this* server - we can apply their latest data
|
||||
getRedis().setUserCheckedOut(user, true);
|
||||
getRedis().getUserData(user).ifPresentOrElse(
|
||||
data -> user.applySnapshot(data, DataSnapshot.UpdateCause.SYNCHRONIZED),
|
||||
@@ -58,12 +71,13 @@ public class LockstepDataSyncer extends DataSyncer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveUserData(@NotNull OnlineUser onlineUser) {
|
||||
public void syncSaveUserData(@NotNull OnlineUser onlineUser) {
|
||||
plugin.runAsync(() -> saveData(
|
||||
onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
|
||||
(user, data) -> {
|
||||
getRedis().setUserData(user, data, RedisKeyType.TTL_1_YEAR);
|
||||
getRedis().setUserData(user, data);
|
||||
getRedis().setUserCheckedOut(user, false);
|
||||
plugin.unlockPlayer(user.getUuid());
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface CommandUser {
|
||||
public interface CommandUser {
|
||||
|
||||
@NotNull
|
||||
Audience getAudience();
|
||||
|
||||
@@ -43,11 +43,27 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the player has gone offline
|
||||
* Indicates if the player is offline
|
||||
*
|
||||
* @return {@code true} if the player has left the server; {@code false} otherwise
|
||||
* @deprecated use {@code hasDisconnected} instead
|
||||
*/
|
||||
public abstract boolean isOffline();
|
||||
@Deprecated(since = "3.8")
|
||||
public boolean isOffline() {
|
||||
return hasDisconnected();
|
||||
}
|
||||
|
||||
public abstract boolean hasDisconnected();
|
||||
|
||||
// Users cannot have snapshots applied if they have disconnected!
|
||||
@Override
|
||||
public boolean cannotApplySnapshot() {
|
||||
if (hasDisconnected()) {
|
||||
getPlugin().debug("[%s] Cannot apply snapshot as user is offline!".formatted(getName()));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
@@ -89,7 +105,9 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
||||
* @param description the description of the toast
|
||||
* @param iconMaterial the namespace-keyed material to use as an hasIcon of the toast
|
||||
* @param backgroundType the background ("ToastType") of the toast
|
||||
* @deprecated No longer supported
|
||||
*/
|
||||
@Deprecated(since = "3.6.7")
|
||||
public abstract void sendToast(@NotNull MineDown title, @NotNull MineDown description,
|
||||
@NotNull String iconMaterial, @NotNull String backgroundType);
|
||||
|
||||
@@ -115,7 +133,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
||||
|
||||
|
||||
/**
|
||||
* Set a player's status from a {@link DataSnapshot}
|
||||
* Apply a {@link DataSnapshot} to a player, updating their data
|
||||
*
|
||||
* @param snapshot The {@link DataSnapshot} to set the player's status from
|
||||
* @param cause The {@link DataSnapshot.UpdateCause} of the snapshot
|
||||
@@ -123,14 +141,12 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
||||
*/
|
||||
public void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull DataSnapshot.UpdateCause cause) {
|
||||
getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> {
|
||||
if (!isOffline()) {
|
||||
getPlugin().debug(String.format("Applying snapshot (%s) to %s (cause: %s)",
|
||||
snapshot.getShortId(), getUsername(), cause.getDisplayName()
|
||||
));
|
||||
UserDataHolder.super.applySnapshot(
|
||||
event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin())
|
||||
);
|
||||
}
|
||||
getPlugin().debug(String.format("Attempting to apply snapshot (%s) to %s (cause: %s)",
|
||||
snapshot.getShortId(), getName(), cause.getDisplayName()
|
||||
));
|
||||
UserDataHolder.super.applySnapshot(
|
||||
event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -145,12 +161,6 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
||||
switch (plugin.getSettings().getSynchronization().getNotificationDisplaySlot()) {
|
||||
case CHAT -> cause.getCompletedLocale(plugin).ifPresent(this::sendMessage);
|
||||
case ACTION_BAR -> cause.getCompletedLocale(plugin).ifPresent(this::sendActionBar);
|
||||
case TOAST -> cause.getCompletedLocale(plugin)
|
||||
.ifPresent(locale -> this.sendToast(
|
||||
locale, new MineDown(""),
|
||||
"minecraft:bell",
|
||||
"TASK"
|
||||
));
|
||||
}
|
||||
plugin.fireEvent(
|
||||
plugin.getSyncCompleteEvent(this),
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
package net.william278.husksync.user;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
@@ -26,39 +29,18 @@ import java.util.UUID;
|
||||
/**
|
||||
* Represents a user who has their data synchronized by HuskSync
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(exclude = {"name"})
|
||||
public class User {
|
||||
|
||||
private final UUID uuid;
|
||||
private final String name;
|
||||
|
||||
private final String username;
|
||||
|
||||
public User(@NotNull UUID uuid, @NotNull String username) {
|
||||
this.username = username;
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's unique account ID
|
||||
*/
|
||||
@NotNull
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's username
|
||||
*/
|
||||
@NotNull
|
||||
@Deprecated(since = "3.7.4")
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (object instanceof User other) {
|
||||
return this.getUuid().equals(other.getUuid());
|
||||
}
|
||||
return super.equals(object);
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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 de.exlll.configlib.Configuration;
|
||||
import de.exlll.configlib.YamlConfigurationProperties;
|
||||
import de.exlll.configlib.YamlConfigurationStore;
|
||||
import lombok.AllArgsConstructor;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static net.william278.husksync.config.ConfigProvider.YAML_CONFIGURATION_PROPERTIES;
|
||||
|
||||
public interface CompatibilityChecker {
|
||||
|
||||
String COMPATIBILITY_FILE = "compatibility.yml";
|
||||
|
||||
default void checkCompatibility() throws HuskSync.FailedToLoadException {
|
||||
final YamlConfigurationProperties p = YAML_CONFIGURATION_PROPERTIES.build();
|
||||
final CompatibilityConfig compat;
|
||||
|
||||
// Load compatibility file
|
||||
try (InputStream input = getResource(COMPATIBILITY_FILE)) {
|
||||
compat = new YamlConfigurationStore<>(CompatibilityConfig.class, p).read(input);
|
||||
} catch (Throwable e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to load compatibility config, skipping check.", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check compatibility
|
||||
if (!compat.isCompatibleWith(getPlugin().getMinecraftVersion())) {
|
||||
throw new HuskSync.FailedToLoadException("""
|
||||
Incompatible Minecraft version. This version of HuskSync is designed for Minecraft %s.
|
||||
Please download the correct version of HuskSync for your server's Minecraft version (%s)."""
|
||||
.formatted(compat.minecraftVersionRange(), getPlugin().getMinecraftVersion().toString()));
|
||||
}
|
||||
}
|
||||
|
||||
InputStream getResource(@NotNull String name);
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
@Configuration
|
||||
record CompatibilityConfig(@NotNull String minecraftVersionRange) {
|
||||
|
||||
@AllArgsConstructor
|
||||
enum ExpressionType {
|
||||
GTE(">=", (v, s) -> v.compareTo(Version.fromString(s.substring(2))) >= 0),
|
||||
LTE("<=", (v, s) -> v.compareTo(Version.fromString(s.substring(2))) <= 0),
|
||||
GT(">", (v, s) -> v.compareTo(Version.fromString(s.substring(1))) > 0),
|
||||
LT("<", (v, s) -> v.compareTo(Version.fromString(s.substring(1))) < 0),
|
||||
NOT("!", (v, s) -> v.compareTo(Version.fromString(s.substring(1))) != 0),
|
||||
E("=", (v, s) -> v.compareTo(Version.fromString(s.substring(1))) == 0);
|
||||
|
||||
private final String match;
|
||||
private final BiFunction<Version, String, Boolean> function;
|
||||
|
||||
private static boolean check(@NotNull String versionRange, @NotNull Version mcVer) {
|
||||
boolean passes = true;
|
||||
versions:
|
||||
for (String exp : versionRange.split(" ")) {
|
||||
for (ExpressionType type : values()) {
|
||||
if (exp.trim().startsWith(type.match)) {
|
||||
passes = passes && type.function.apply(mcVer, exp.trim());
|
||||
continue versions;
|
||||
}
|
||||
}
|
||||
passes = passes && mcVer.compareTo(Version.fromString(exp.trim())) == 0;
|
||||
}
|
||||
return passes;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCompatibleWith(@NotNull Version version) {
|
||||
return ExpressionType.check(minecraftVersionRange, version);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -49,7 +49,7 @@ public class DataSnapshotList {
|
||||
.map(snapshot -> plugin.getLocales()
|
||||
.getRawLocale(!snapshot.isInvalid() ? "data_list_item" : "data_list_item_invalid",
|
||||
getNumberIcon(snapshotNumber.getAndIncrement()),
|
||||
dataOwner.getUsername(),
|
||||
dataOwner.getName(),
|
||||
snapshot.getId().toString(),
|
||||
snapshot.getShortId(),
|
||||
snapshot.isPinned() ? "※" : " ",
|
||||
@@ -63,10 +63,10 @@ public class DataSnapshotList {
|
||||
.orElse("• " + snapshot.getId())).toList(),
|
||||
plugin.getLocales().getBaseChatList(6)
|
||||
.setHeaderFormat(plugin.getLocales()
|
||||
.getRawLocale("data_list_title", dataOwner.getUsername(),
|
||||
.getRawLocale("data_list_title", dataOwner.getName(),
|
||||
"%first_item_on_page_index%", "%last_item_on_page_index%", "%total_items%")
|
||||
.orElse(""))
|
||||
.setCommand("/husksync:userdata list " + dataOwner.getUsername())
|
||||
.setCommand("/husksync:userdata list " + dataOwner.getName())
|
||||
.build());
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ public class DataSnapshotOverview {
|
||||
// Title message, timestamp, owner and cause.
|
||||
final Locales locales = plugin.getLocales();
|
||||
locales.getLocale("data_manager_title", snapshot.getShortId(), snapshot.getId().toString(),
|
||||
dataOwner.getUsername(), dataOwner.getUuid().toString())
|
||||
dataOwner.getName(), dataOwner.getUuid().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
locales.getLocale("data_manager_timestamp",
|
||||
snapshot.getTimestamp().format(DateTimeFormatter
|
||||
@@ -106,14 +106,14 @@ public class DataSnapshotOverview {
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
if (user.hasPermission("husksync.command.inventory.edit")
|
||||
&& user.hasPermission("husksync.command.enderchest.edit")) {
|
||||
locales.getLocale("data_manager_item_buttons", dataOwner.getUsername(), snapshot.getId().toString())
|
||||
&& user.hasPermission("husksync.command.enderchest.edit")) {
|
||||
locales.getLocale("data_manager_item_buttons", dataOwner.getName(), snapshot.getId().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
locales.getLocale("data_manager_management_buttons", dataOwner.getUsername(), snapshot.getId().toString())
|
||||
locales.getLocale("data_manager_management_buttons", dataOwner.getName(), snapshot.getId().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
if (user.hasPermission("husksync.command.userdata.dump")) {
|
||||
locales.getLocale("data_manager_system_buttons", dataOwner.getUsername(), snapshot.getId().toString())
|
||||
locales.getLocale("data_manager_system_buttons", dataOwner.getName(), snapshot.getId().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.desertwell.util.Version;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface DataVersionSupplier {
|
||||
|
||||
int VERSION1_16_5 = 2586;
|
||||
int VERSION1_17_1 = 2730;
|
||||
int VERSION1_18_2 = 2975;
|
||||
int VERSION1_19_2 = 3120;
|
||||
int VERSION1_19_4 = 3337;
|
||||
int VERSION1_20_1 = 3465;
|
||||
int VERSION1_20_2 = 3578;
|
||||
int VERSION1_20_4 = 3700;
|
||||
int VERSION1_20_5 = 3837;
|
||||
int VERSION1_21_1 = 3955;
|
||||
int VERSION1_21_3 = 4082;
|
||||
int VERSION1_21_4 = 4189;
|
||||
int VERSION1_21_5 = 4323;
|
||||
int VERSION1_21_6 = 4435;
|
||||
int VERSION1_21_7 = 4438;
|
||||
int VERSION1_21_8 = 4438;
|
||||
int VERSION1_21_9 = 4554;
|
||||
int VERSION1_21_10 = 4556;
|
||||
int VERSION1_21_11 = 4671;
|
||||
|
||||
/**
|
||||
* Returns the data version for a Minecraft version
|
||||
*
|
||||
* @param mcVersion the Minecraft version
|
||||
* @return the data version int
|
||||
*/
|
||||
default int getDataVersion(@NotNull Version mcVersion) {
|
||||
return switch (mcVersion.toStringWithoutMetadata()) {
|
||||
case "1.16", "1.16.1", "1.16.2", "1.16.3", "1.16.4", "1.16.5" -> VERSION1_16_5;
|
||||
case "1.17", "1.17.1" -> VERSION1_17_1;
|
||||
case "1.18", "1.18.1", "1.18.2" -> VERSION1_18_2;
|
||||
case "1.19", "1.19.1", "1.19.2" -> VERSION1_19_2;
|
||||
case "1.19.4" -> VERSION1_19_4;
|
||||
case "1.20", "1.20.1" -> VERSION1_20_1;
|
||||
case "1.20.2" -> VERSION1_20_2;
|
||||
case "1.20.4" -> VERSION1_20_4;
|
||||
case "1.20.5", "1.20.6" -> VERSION1_20_5;
|
||||
case "1.21", "1.21.1" -> VERSION1_21_1;
|
||||
case "1.21.2", "1.21.3" -> VERSION1_21_3;
|
||||
case "1.21.4" -> VERSION1_21_4;
|
||||
case "1.21.5" -> VERSION1_21_5;
|
||||
case "1.21.6" -> VERSION1_21_6;
|
||||
case "1.21.7" -> VERSION1_21_7;
|
||||
case "1.21.8" -> VERSION1_21_8;
|
||||
case "1.21.9" -> VERSION1_21_9;
|
||||
case "1.21.10" -> VERSION1_21_10;
|
||||
case "1.21.11" -> VERSION1_21_11;
|
||||
default -> VERSION1_21_11; // Latest supported version
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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.HuskSync;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.toilet.DumpOptions;
|
||||
import net.william278.toilet.Toilet;
|
||||
import net.william278.toilet.dump.*;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import static net.william278.toilet.DumpOptions.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public interface DumpProvider {
|
||||
|
||||
@NotNull String BYTEBIN_URL = "https://bytebin.lucko.me";
|
||||
@NotNull String VIEWER_URL = "https://william278.net/dump";
|
||||
|
||||
@NotNull
|
||||
Toilet getToilet();
|
||||
|
||||
@NotNull
|
||||
@Blocking
|
||||
default String createDump(@NotNull CommandUser u) {
|
||||
return getToilet().dump(getPluginStatus(), u instanceof OnlineUser o
|
||||
? new DumpUser(o.getName(), o.getUuid()) : null,
|
||||
getRedisInfo()).toString();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default DumpOptions getDumpOptions() {
|
||||
return builder()
|
||||
.bytebinUrl(BYTEBIN_URL)
|
||||
.viewerUrl(VIEWER_URL)
|
||||
.projectMeta(ProjectMeta.builder()
|
||||
.id("husksync")
|
||||
.name("HuskSync")
|
||||
.version(getPlugin().getPluginVersion().toString())
|
||||
.md5("unknown")
|
||||
.author("William278")
|
||||
.sourceCode("https://github.com/WiIIiam278/HuskSync")
|
||||
.website("https://william278.net/project/husksync")
|
||||
.support("https://discord.gg/tVYhJfyDWG")
|
||||
.build())
|
||||
.fileInclusionRules(List.of(
|
||||
FileInclusionRule.configFile("config.yml", "Config File"),
|
||||
FileInclusionRule.configFile(getMessagesFile(), "Locales File")
|
||||
))
|
||||
.compatibilityRules(List.of(
|
||||
getCompatibilityWarning("CombatLogX", "Combat loggers require additional" +
|
||||
"configuration for use with HuskSync. Check https://william278.net/docs/husksync/event-priorities"),
|
||||
getIncompatibleNotice("UltimateAutoRestart", "Restart plugins are not" +
|
||||
"compatible with HuskSync as they affect the way the server shuts down, preventing data" +
|
||||
"from saving correctly during a restart. Check https://william278.net/docs/husksync/troubleshooting")
|
||||
))
|
||||
.build();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Blocking
|
||||
private PluginStatus getPluginStatus() {
|
||||
return PluginStatus.builder()
|
||||
.blocks(List.of(getSystemStatus(), getRegisteredDataTypes()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Blocking
|
||||
private PluginStatus.MapStatusBlock getSystemStatus() {
|
||||
return new PluginStatus.MapStatusBlock(
|
||||
Map.ofEntries(
|
||||
Map.entry("Language", StatusLine.LANGUAGE.getValue(getPlugin())),
|
||||
Map.entry("Database Type", StatusLine.DATABASE_TYPE.getValue(getPlugin())),
|
||||
Map.entry("Database Local", StatusLine.IS_DATABASE_LOCAL.getValue(getPlugin())),
|
||||
Map.entry("Locked User Handler", StatusLine.LOCKED_USER_HANDLER.getValue(getPlugin())),
|
||||
Map.entry("Server Name", StatusLine.SERVER_NAME.getValue(getPlugin())),
|
||||
Map.entry("Redis Version", StatusLine.REDIS_VERSION.getValue(getPlugin())),
|
||||
Map.entry("Redis Latency", StatusLine.REDIS_LATENCY.getValue(getPlugin())),
|
||||
Map.entry("Redis Sentinel", StatusLine.USING_REDIS_SENTINEL.getValue(getPlugin())),
|
||||
Map.entry("Redis Database", StatusLine.REDIS_DATABASE.getValue(getPlugin())),
|
||||
Map.entry("Redis User", StatusLine.USING_REDIS_USER.getValue(getPlugin())),
|
||||
Map.entry("Redis Password", StatusLine.USING_REDIS_PASSWORD.getValue(getPlugin())),
|
||||
Map.entry("Redis SSL", StatusLine.REDIS_USING_SSL.getValue(getPlugin())),
|
||||
Map.entry("Redis Local", StatusLine.IS_REDIS_LOCAL.getValue(getPlugin()))
|
||||
),
|
||||
"Plugin Status", "fa6-solid:wrench"
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Blocking
|
||||
private PluginStatus.MapStatusBlock getRegisteredDataTypes() {
|
||||
return new PluginStatus.MapStatusBlock(
|
||||
getPlugin().getRegisteredDataTypes().stream().collect(Collectors.toMap(
|
||||
i -> i.getKey().asMinimalString(),
|
||||
i -> i.isEnabled() ? "✅ Enabled" : "❌ Disabled",
|
||||
(a, b) -> a)
|
||||
),
|
||||
"Registered Data Types", "carbon:data-blob"
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Blocking
|
||||
private ExtraFile getRedisInfo() {
|
||||
return new ExtraFile(
|
||||
"redis-status", "Redis Status", "devicon-plain:redis",
|
||||
getPlugin().getRedisManager().getStatusDump(),
|
||||
"markdown"
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private CompatibilityRule getCompatibilityWarning(@NotNull String plugin, @NotNull String description) {
|
||||
return CompatibilityRule.builder()
|
||||
.labelToApply(new PluginInfo.Label("Warning", "#fcba03", description))
|
||||
.resourceName(plugin).build();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private CompatibilityRule getIncompatibleNotice(@NotNull String plugin, @NotNull String description) {
|
||||
return CompatibilityRule.builder()
|
||||
.labelToApply(new PluginInfo.Label("Incompatible", "#ff3300", description))
|
||||
.resourceName(plugin).build();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String getMessagesFile() {
|
||||
return "messages-%s.yml".formatted(getPlugin().getSettings().getLanguage());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
@@ -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.util;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.JoinConfiguration;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.database.Database;
|
||||
import org.apache.commons.text.WordUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public enum StatusLine {
|
||||
PLUGIN_VERSION(plugin -> Component.text("v" + plugin.getPluginVersion().toStringWithoutMetadata())
|
||||
.appendSpace().append(plugin.getPluginVersion().getMetadata().isBlank() ? Component.empty()
|
||||
: Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))),
|
||||
SERVER_VERSION(plugin -> Component.text(plugin.getServerVersion())),
|
||||
LANGUAGE(plugin -> Component.text(plugin.getSettings().getLanguage())),
|
||||
MINECRAFT_VERSION(plugin -> Component.text(plugin.getMinecraftVersion().toString())),
|
||||
JAVA_VERSION(plugin -> Component.text(System.getProperty("java.version"))),
|
||||
JAVA_VENDOR(plugin -> Component.text(System.getProperty("java.vendor"))),
|
||||
SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
|
||||
CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())),
|
||||
SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully(
|
||||
plugin.getSettings().getSynchronization().getMode().toString()
|
||||
))),
|
||||
DELAY_LATENCY(plugin -> Component.text(
|
||||
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms"
|
||||
)),
|
||||
DATABASE_TYPE(plugin ->
|
||||
Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() +
|
||||
(plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ?
|
||||
(plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : ""))
|
||||
),
|
||||
IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())),
|
||||
REDIS_VERSION(plugin -> Component.text(plugin.getRedisManager().getVersion())),
|
||||
USING_REDIS_SENTINEL(plugin -> getBoolean(
|
||||
!plugin.getSettings().getRedis().getSentinel().getMaster().isBlank()
|
||||
)),
|
||||
REDIS_DATABASE(plugin -> Component.text(plugin.getSettings().getRedis().getCredentials().getDatabase())),
|
||||
USING_REDIS_USER(plugin -> getBoolean(
|
||||
!plugin.getSettings().getRedis().getCredentials().getUser().isBlank()
|
||||
)),
|
||||
USING_REDIS_PASSWORD(plugin -> getBoolean(
|
||||
!plugin.getSettings().getRedis().getCredentials().getPassword().isBlank()
|
||||
)),
|
||||
REDIS_USING_SSL(plugin -> getBoolean(
|
||||
plugin.getSettings().getRedis().getCredentials().isUseSsl()
|
||||
)),
|
||||
REDIS_LATENCY(plugin -> Component.text("%sms".formatted(plugin.getRedisManager().getLatency()))),
|
||||
IS_REDIS_LOCAL(plugin -> getLocalhostBoolean(
|
||||
plugin.getSettings().getRedis().getCredentials().getHost()
|
||||
)),
|
||||
LOCKED_USER_HANDLER(plugin -> Component.text(plugin.getLockedHandler().getClass().getSimpleName())),
|
||||
DATA_TYPES(plugin -> Component.join(
|
||||
JoinConfiguration.commas(true),
|
||||
plugin.getRegisteredDataTypes().stream().map(i -> Component.textOfChildren(Component.text(i.toString())
|
||||
.appendSpace().append(Component.text(i.isEnabled() ? '✔' : '❌')))
|
||||
.color(i.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED)
|
||||
.hoverEvent(HoverEvent.showText(
|
||||
Component.text(i.isEnabled() ? "Enabled" : "Disabled")
|
||||
.append(Component.newline())
|
||||
.append(Component.text("Dependencies: %s".formatted(i.getDependencies()
|
||||
.isEmpty() ? "(None)" : i.getDependencies().stream()
|
||||
.map(d -> "%s (%s)".formatted(
|
||||
d.getKey().value(), d.isRequired() ? "Required" : "Optional"
|
||||
)).collect(Collectors.joining(", ")))
|
||||
).color(NamedTextColor.GRAY))
|
||||
))).toList()
|
||||
));
|
||||
|
||||
private final Function<HuskSync, Component> supplier;
|
||||
|
||||
StatusLine(@NotNull Function<HuskSync, Component> supplier) {
|
||||
this.supplier = supplier;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Component get(@NotNull HuskSync plugin) {
|
||||
return Component
|
||||
.text("•").appendSpace()
|
||||
.append(Component.text(
|
||||
WordUtils.capitalizeFully(name().replaceAll("_", " ")),
|
||||
TextColor.color(0x848484)
|
||||
))
|
||||
.append(Component.text(':')).append(Component.space().color(NamedTextColor.WHITE))
|
||||
.append(supplier.apply(plugin));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getValue(@NotNull HuskSync plugin) {
|
||||
return PlainTextComponentSerializer.plainText().serialize(supplier.apply(plugin));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Component getBoolean(boolean value) {
|
||||
return Component.text(value ? "Yes" : "No", value ? NamedTextColor.GREEN : NamedTextColor.RED);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Component getLocalhostBoolean(@NotNull String value) {
|
||||
return getBoolean(value.equals("127.0.0.1") || value.equals("0.0.0.0")
|
||||
|| value.equals("localhost") || value.equals("::1"));
|
||||
}
|
||||
}
|
||||
@@ -19,17 +19,15 @@
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import net.william278.toilet.web.Flusher;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -38,94 +36,42 @@ import java.util.Locale;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static net.william278.husksync.util.DumpProvider.BYTEBIN_URL;
|
||||
|
||||
/**
|
||||
* Utility class for dumping {@link DataSnapshot}s to a file or as a paste on the web
|
||||
*/
|
||||
public class DataDumper {
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class UserDataDumper implements Flusher {
|
||||
|
||||
private static final String LOGS_SITE_ENDPOINT = "https://api.mclo.gs/1/log";
|
||||
private static final String PASTE_VIEWER_URL = "https://pastes.dev";
|
||||
|
||||
private final HuskSync plugin;
|
||||
private final DataSnapshot.Packed snapshot;
|
||||
private final User user;
|
||||
|
||||
private DataDumper(@NotNull DataSnapshot.Packed snapshot, @NotNull User user, @NotNull HuskSync implementor) {
|
||||
this.snapshot = snapshot;
|
||||
this.user = user;
|
||||
this.plugin = implementor;
|
||||
}
|
||||
private final HuskSync plugin;
|
||||
|
||||
/**
|
||||
* Create a {@link DataDumper} of the given {@link DataSnapshot}
|
||||
* Create a {@link UserDataDumper} of the given {@link DataSnapshot}
|
||||
*
|
||||
* @param dataSnapshot The {@link DataSnapshot} to dump
|
||||
* @param user The {@link User} whose data is being dumped
|
||||
* @param plugin The implementing {@link HuskSync} plugin
|
||||
* @return A {@link DataDumper} for the given {@link DataSnapshot}
|
||||
* @param snapshot The {@link DataSnapshot} to dump
|
||||
* @param user The {@link User} whose data is being dumped
|
||||
* @param plugin The implementing {@link HuskSync} plugin
|
||||
* @return A {@link UserDataDumper} for the given {@link DataSnapshot}
|
||||
*/
|
||||
public static DataDumper create(@NotNull DataSnapshot.Packed dataSnapshot,
|
||||
@NotNull User user, @NotNull HuskSync plugin) {
|
||||
return new DataDumper(dataSnapshot, user, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps the data snapshot to a string
|
||||
*
|
||||
* @return the data snapshot as a string
|
||||
*/
|
||||
@Override
|
||||
@NotNull
|
||||
public String toString() {
|
||||
return snapshot.asJson(plugin);
|
||||
public static UserDataDumper create(@NotNull DataSnapshot.Packed snapshot, @NotNull User user,
|
||||
@NotNull HuskSync plugin) {
|
||||
return new UserDataDumper(snapshot, user, plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String toWeb() {
|
||||
try {
|
||||
final URL url = new URL(LOGS_SITE_ENDPOINT);
|
||||
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setDoOutput(true);
|
||||
|
||||
// Dispatch the request
|
||||
final byte[] messageBody = getWebContentField().getBytes(StandardCharsets.UTF_8);
|
||||
final int messageLength = messageBody.length;
|
||||
connection.setFixedLengthStreamingMode(messageLength);
|
||||
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
|
||||
connection.connect();
|
||||
try (OutputStream messageOutputStream = connection.getOutputStream()) {
|
||||
messageOutputStream.write(messageBody);
|
||||
}
|
||||
|
||||
// Get the response
|
||||
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
|
||||
// Get the body as a json
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
|
||||
final StringBuilder response = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
response.append(line);
|
||||
}
|
||||
|
||||
// Parse the response as json
|
||||
final JsonObject responseJson = JsonParser.parseString(response.toString()).getAsJsonObject();
|
||||
if (responseJson.has("url")) {
|
||||
return responseJson.get("url").getAsString();
|
||||
}
|
||||
return "(Failed to get URL from response)";
|
||||
}
|
||||
} else {
|
||||
return "(Failed to upload to logs site, got: " + connection.getResponseCode() + ")";
|
||||
}
|
||||
return "%s/%s".formatted(PASTE_VIEWER_URL, uploadDump(toString(), BYTEBIN_URL, "husksync"));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "Failed to upload data to logs site", e);
|
||||
plugin.log(Level.SEVERE, "Failed to upload data.", e);
|
||||
}
|
||||
return "(Failed to upload to logs site)";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String getWebContentField() {
|
||||
return "content=" + URLEncoder.encode(toString(), StandardCharsets.UTF_8);
|
||||
return "(Failed to upload. Try dumping to a file instead.)";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,7 +86,7 @@ public class DataDumper {
|
||||
writer.write(toString()); // Write the data from #getString to the file using a writer
|
||||
return filePath.toString();
|
||||
} catch (IOException e) {
|
||||
throw new IOException("Failed to write data to file", e);
|
||||
throw new IOException("Failed to write dump to file", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,11 +124,22 @@ public class DataDumper {
|
||||
@NotNull
|
||||
private String getFileName() {
|
||||
return new StringJoiner("_")
|
||||
.add(user.getUsername())
|
||||
.add(user.getName())
|
||||
.add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")))
|
||||
.add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH))
|
||||
.add(snapshot.getShortId())
|
||||
+ ".json";
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps the data snapshot to a string
|
||||
*
|
||||
* @return the data snapshot as a string
|
||||
*/
|
||||
@Override
|
||||
@NotNull
|
||||
public String toString() {
|
||||
return snapshot.asJson(plugin);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,4 +29,28 @@ CREATE TABLE IF NOT EXISTS `%user_data_table%`
|
||||
FOREIGN KEY (`player_uuid`) REFERENCES `%users_table%` (`uuid`) ON DELETE CASCADE
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci;
|
||||
COLLATE = utf8mb4_unicode_ci;
|
||||
|
||||
-- Create the map data table if it does not exist
|
||||
CREATE TABLE IF NOT EXISTS `%map_data_table%`
|
||||
(
|
||||
`server_name` varchar(32) NOT NULL,
|
||||
`map_id` int NOT NULL,
|
||||
`data` longblob NOT NULL,
|
||||
PRIMARY KEY (`server_name`, `map_id`)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci;
|
||||
|
||||
-- Create the map ids table if it does not exist
|
||||
CREATE TABLE IF NOT EXISTS `%map_ids_table%`
|
||||
(
|
||||
`from_server_name` varchar(32) NOT NULL,
|
||||
`from_id` int NOT NULL,
|
||||
`to_server_name` varchar(32) NOT NULL,
|
||||
`to_id` int NOT NULL,
|
||||
PRIMARY KEY (`from_server_name`, `from_id`, `to_server_name`),
|
||||
FOREIGN KEY (`from_server_name`, `from_id`) REFERENCES `%map_data_table%` (`server_name`, `map_id`) ON DELETE CASCADE
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci;
|
||||
|
||||
@@ -26,4 +26,26 @@ CREATE TABLE IF NOT EXISTS `%user_data_table%`
|
||||
PRIMARY KEY (`version_uuid`, `player_uuid`),
|
||||
FOREIGN KEY (`player_uuid`) REFERENCES `%users_table%` (`uuid`) ON DELETE CASCADE
|
||||
) CHARACTER SET utf8
|
||||
COLLATE utf8_unicode_ci;
|
||||
COLLATE utf8_unicode_ci;
|
||||
|
||||
# Create the map data table if it does not exist
|
||||
CREATE TABLE IF NOT EXISTS `%map_data_table%`
|
||||
(
|
||||
`server_name` varchar(32) NOT NULL,
|
||||
`map_id` int NOT NULL,
|
||||
`data` longblob NOT NULL,
|
||||
PRIMARY KEY (`server_name`, `map_id`)
|
||||
) CHARACTER SET utf8
|
||||
COLLATE utf8_unicode_ci;
|
||||
|
||||
# Create the map ids table if it does not exist
|
||||
CREATE TABLE IF NOT EXISTS `%map_ids_table%`
|
||||
(
|
||||
`from_server_name` varchar(32) NOT NULL,
|
||||
`from_id` int NOT NULL,
|
||||
`to_server_name` varchar(32) NOT NULL,
|
||||
`to_id` int NOT NULL,
|
||||
PRIMARY KEY (`from_server_name`, `from_id`, `to_server_name`),
|
||||
FOREIGN KEY (`from_server_name`, `from_id`) REFERENCES `%map_data_table%` (`server_name`, `map_id`) ON DELETE CASCADE
|
||||
) CHARACTER SET utf8
|
||||
COLLATE utf8_unicode_ci;
|
||||
|
||||
@@ -19,4 +19,24 @@ CREATE TABLE IF NOT EXISTS "%user_data_table%"
|
||||
|
||||
PRIMARY KEY (version_uuid, player_uuid),
|
||||
FOREIGN KEY (player_uuid) REFERENCES "%users_table%" (uuid) ON DELETE CASCADE
|
||||
);
|
||||
);
|
||||
|
||||
-- Create the map data table if it does not exist
|
||||
CREATE TABLE IF NOT EXISTS "%map_data_table%"
|
||||
(
|
||||
server_name varchar(32) NOT NULL,
|
||||
map_id int NOT NULL,
|
||||
data bytea NOT NULL,
|
||||
PRIMARY KEY (server_name, map_id)
|
||||
);
|
||||
|
||||
-- Create the map ids table if it does not exist
|
||||
CREATE TABLE IF NOT EXISTS "%map_ids_table%"
|
||||
(
|
||||
from_server_name varchar(32) NOT NULL,
|
||||
from_id int NOT NULL,
|
||||
to_server_name varchar(32) NOT NULL,
|
||||
to_id int NOT NULL,
|
||||
PRIMARY KEY (from_server_name, from_id, to_server_name),
|
||||
FOREIGN KEY (from_server_name, from_id) REFERENCES "%map_data_table%" (server_name, map_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
@@ -23,11 +23,12 @@ locales:
|
||||
data_list_title: '[Лист от](#00fb9a) [снапшоти на данните на потребителя](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||
data_deleted: '[❌ Успешно изтрихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
||||
data_restored: '[⏪ Успешно възстановихме](#00fb9a) [текущите потребителски данни за](#00fb9a) [%1%](#00fb9a show_text=&7UUID на Играча:\n&8%2%) [от снапшот](#00fb9a) [%3%.](#00fb9a show_text=&7Версия на UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Успешно закачихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
||||
data_unpinned: '[※ Успешно откачихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
data_dumped: '[☂ Successfully dumped user data snapshot %1% for %2%. Click to view:](#00fb9a)'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
@@ -40,6 +41,8 @@ locales:
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_save_command: 'save command'
|
||||
save_cause_dump_command: 'dump command'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
@@ -51,6 +54,9 @@ locales:
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Презаредихме конфигурацията и файловете със съобщения.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||
error_invalid_syntax: '[Грешка:](#ff3300) [Неправилен синтаксис. Използвайте:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Грешка:](#ff3300) [Не можахме да открием играч с това име.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
|
||||
@@ -23,11 +23,12 @@ locales:
|
||||
data_list_title: '[Nutzerdaten-Schnappschüsse von %1%:](#00fb9a) [(%2%-%3% von](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Nutzerdaten-Schnappschuss für %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Angeheftet:\n&8Angeheftete Schnappschüsse werden nicht automatisch rotiert. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versions-Zeitstempel:&7\n&8Zeitpunkt der Speicherung der Daten\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Speicherungsgrund:\n&8Grund für das Speichern der Daten run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Schnappschuss-Größe:&7\n&8Geschätzte Dateigröße des Schnappschusses (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||
data_deleted: '[❌ Nutzerdaten-Schnappschuss erfolgreich gelöscht](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Erfgreich wiederhergestellt](#00fb9a) [Aktuelle Nutzerdaten des Schnappschusses von %1%](#00fb9a show_text=&7Spieler-UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versions-UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Nutzerdaten-Schnappschuss erfolgreich angepinnt](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ Nutzerdaten-Schnappschuss erfolgreich losgelöst](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Nutzerdaten-Schnappschuss %1% für %2% erfolgreich gedumpt nach:](#00fb9a) &7%3%'
|
||||
data_dumped: '[☂ Nutzerdaten-Schnappschuss %1% für %2% erfolgreich gedumpt nach:](#00fb9a)'
|
||||
list_footer: '\n%1%[Seite](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7Siehe vorherige Seite run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7Siehe nächste Seite run_command=%2% %1%)'
|
||||
@@ -39,18 +40,23 @@ locales:
|
||||
save_cause_disconnect: 'Server verlassen'
|
||||
save_cause_world_save: 'Welt gespeichert'
|
||||
save_cause_death: 'Tod'
|
||||
save_cause_server_shutdown: 'Server gestoppt'
|
||||
save_cause_inventory_command: 'Inventar Befehl'
|
||||
save_cause_enderchest_command: 'Enderchest Befehl'
|
||||
save_cause_backup_restore: 'Backup wiederhergestellt'
|
||||
save_cause_server_shutdown: 'server gestoppt'
|
||||
save_cause_save_command: 'save command'
|
||||
save_cause_dump_command: 'dump command'
|
||||
save_cause_inventory_command: 'inventar Befehl'
|
||||
save_cause_enderchest_command: 'enderchest Befehl'
|
||||
save_cause_backup_restore: 'backup wiederhergestellt'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB Migration'
|
||||
save_cause_legacy_migration: 'Legacy Migration'
|
||||
save_cause_legacy_migration: 'legacy Migration'
|
||||
save_cause_converted_from_v2: 'Import von v2'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Die Konfigurations- und Sprachdateien wurden neu geladen.](#00fb9a)\n[⚠ Stelle sicher, dass die Konfigurationsdateien auf allen Servern aktuell sind!](#00fb9a)\n[Ein Neustart wird benötigt, damit Konfigurations-Änderungen wirkbar werden.](#00fb9a italic)'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| Du verwendest die neuste Version von HuskSync (v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| Eine neue Version von HuskSync ist verfügbar: v%1% (Aktuelle Version: v%2%).](#ff7e5e)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||
error_invalid_syntax: '[Fehler:](#ff3300) [Falsche Syntax. Nutze:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Fehler:](#ff3300) [Es konnte kein Spieler mit diesem Namen gefunden werden.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
|
||||
@@ -23,11 +23,12 @@ locales:
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ Successfully unpinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
data_dumped: '[☂ Successfully dumped user data snapshot %1% for %2%. Click to view:](#00fb9a)'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
@@ -40,6 +41,8 @@ locales:
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_save_command: 'save command'
|
||||
save_cause_dump_command: 'dump command'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
@@ -51,6 +54,9 @@ locales:
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Reloaded config and message files.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||
error_invalid_syntax: '[Error:](#ff3300) [Incorrect syntax. Usage:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Error:](#ff3300) [Could not find a player by that name.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
|
||||
@@ -23,11 +23,12 @@ locales:
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||
data_deleted: '[❌ Se ha eliminado correctamente la snapshot del usuario](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Restaurado correctamente](#00fb9a) [%1%](#00fb9a show_text=&7UUID del jugador:\n&8%2%)[Informacion actual de la snapshot del jugador](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Se ha anclado perfectamente la snapshot del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7UUID del usuario:\n&8%4%)'
|
||||
data_unpinned: '[※ Se ha desanclado perfectamente la snapshot del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7UUID del usuario:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a)'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
@@ -40,6 +41,8 @@ locales:
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_save_command: 'save command'
|
||||
save_cause_dump_command: 'dump command'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
@@ -51,6 +54,9 @@ locales:
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Recargada la configuración y los archivos de lenguaje.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||
error_invalid_syntax: '[Error:](#ff3300) [Sintanxis incorrecta. Usa:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Error:](#ff3300) [No se ha podido encontrar un jugador con ese nombre.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
|
||||
@@ -23,11 +23,12 @@ locales:
|
||||
data_list_title: '[Les instantanés des données utilisateur de %1%:](#00fb9a) [(%2%-%3% sur](#00fb9a)[%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Instantané des données utilisateur pour %2%\n&8⚡ %4% run_command=/userdataview %2% %3%) [%5%](#d8ff2b show_text=&7Épinglé:\n&8Les instantanés épinglés ne serontpas automatiquement supprimés. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962show_text=&7Horodatage de la version:&7\n&8Quand les données ont été enregistrées\n&8%7% run_command=/userdataview %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Cause de la sauvegarde:\n&8Ce qui a causél''enregistrement des données run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fashow_text=&7Taille de l''instantané:&7\n&8Taille du fichier estimée de l''instantané (en KiB) run_command=/userdataview %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7Instantané des données utilisateur pour %2%\n&8⚡%4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Épinglé:\n&8Lesinstantanés épinglés ne seront pas automatiquement supprimés. suggest_command=/userdata delete %2%%3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Instantané des donnéesinvalide\n&#ff7e5e&Cliquez pour supprimer\n\n&7⚠ %10% suggest_command=/userdata delete%2% %3%)'
|
||||
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||
data_deleted: '[❌ Instantané des données utilisateur supprimé avec succès](#00fb9a) [%1%](#00fb9ashow_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%.](#00fb9a show_text=&7UUID du joueur:\n&8%4%)'
|
||||
data_restored: '[⏪ Données utilisateur actuelles de %1% restaurées avec succès à partir de l''instantané](#00fb9a) [%3%.](#00fb9a show_text=&7UUID de la version:\n&8%4%)'
|
||||
data_pinned: '[※ Instantané des données utilisateur épinglé avec succès](#00fb9a) [%1%](#00fb9ashow_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%.](#00fb9a show_text=&7UUID du joueur:\n&8%4%)'
|
||||
data_unpinned: '[※ Instantané des données utilisateur détaché avec succès](#00fb9a) [%1%](#00fb9ashow_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%.](#00fb9a show_text=&7UUID du joueur:\n&8%4%)'
|
||||
data_dumped: '[☂ Dump de l''instantané des données utilisateur %1% pour %2% à:](#00fb9a)&7%3%'
|
||||
data_dumped: '[☂ Dump de l''instantané des données utilisateur %1% pour %2% à:](#00fb9a)'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7Voir la page précédente run_command=%2%%1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7Voir la page suivante run_command=%2% %1%)'
|
||||
@@ -40,6 +41,8 @@ locales:
|
||||
save_cause_world_save: 'sauvegarde du monde'
|
||||
save_cause_death: 'mort'
|
||||
save_cause_server_shutdown: 'arrêt du serveur'
|
||||
save_cause_save_command: 'save command'
|
||||
save_cause_dump_command: 'dump command'
|
||||
save_cause_inventory_command: 'commande d''inventaire'
|
||||
save_cause_enderchest_command: 'commande du coffre de l''Ender'
|
||||
save_cause_backup_restore: 'restauration de sauvegarde'
|
||||
@@ -51,6 +54,9 @@ locales:
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| Une nouvelle version de HuskSync est disponible:v%1% (version actuelle: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Config et messages rechargés.](#00fb9a)\n[⚠Assurez-vous que les fichiers de configuration sont à jour sur tous les serveurs!](#00fb9a)\n[Un redémarrage est nécessairepour que les modifications de configuration prennent effet.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| Rapport d''état du système:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||
error_invalid_syntax: '[Erreur:](#ff3300) [Syntaxe incorrecte. Utilisation:](#ff7e5e) [%1%](#ff7e5eitalic show_text=&#ff7e5e&Cliquez pour suggérer suggest_command=%1%)'
|
||||
error_invalid_player: '[Erreur:](#ff3300) [Impossible de trouver un joueur avec ce nom.](#ff7e5e)'
|
||||
error_invalid_data: '[Erreur:](#ff3300) [Impossible de déballer les données de l''instantané car elles sont invalides ou corrompues.](#ff7e5e) [(Détails…)](gray show_text=&7⚠ %1%)'
|
||||
|
||||
@@ -23,11 +23,12 @@ locales:
|
||||
data_list_title: '[Cuplikan data %1%:](#00fb9a) [(%2%-%3% dari](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Cuplikan data pengguna untuk %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Disematkan:\n&8Cuplikan yang disematkan tidak akan dirotasi otomatis. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versi stampel waktu:&7\n&8Saat data disimpan\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Disimpan karena:\n&8Apa yang menyebabkan data disimpan run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Ukuran cuplikan:&7\n&8Perkiraan ukuran file cuplikan (dalam KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||
data_deleted: '[❌ Berhasil menghapus cuplikan data pengguna](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
|
||||
data_restored: '[⏪ Berhasil dipulihkan](#00fb9a) [%1%](#00fb9a show_text=&7UUID Pemain:\n&8%2%)[data pengguna saat ini dari cuplikan](#00fb9a) [%3%.](#00fb9a show_text=&7Versi UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Berhasil menyematkan cuplikan data pengguna](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
|
||||
data_unpinned: '[※ Berhasil melepaskan cuplikan data pengguna yang disematkan](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
|
||||
data_dumped: '[☂ Berhasil membuang cuplikan data pengguna %1% untuk %2% ke:](#00fb9a) &7%3%'
|
||||
data_dumped: '[☂ Berhasil membuang cuplikan data pengguna %1% untuk %2% ke:](#00fb9a)'
|
||||
list_footer: '\n%1%[Halaman](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7Lihat halaman sebelumnya run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7Lihat halaman selanjutnya run_command=%2% %1%)'
|
||||
@@ -40,6 +41,8 @@ locales:
|
||||
save_cause_world_save: 'penyimpanan dunia'
|
||||
save_cause_death: 'kematian'
|
||||
save_cause_server_shutdown: 'pematian server'
|
||||
save_cause_save_command: 'save command'
|
||||
save_cause_dump_command: 'dump command'
|
||||
save_cause_inventory_command: 'perintah inventaris'
|
||||
save_cause_enderchest_command: 'perintah enderchest'
|
||||
save_cause_backup_restore: 'pemulihan cadangan'
|
||||
@@ -51,6 +54,9 @@ locales:
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| Versi baru HuskSync tersedia: v%1% (menjalankan: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Memuat ulang file konfigurasi dan pesan.](#00fb9a)\n[⚠ Pastikan file konfigurasi sudah diperbarui di semua server!](#00fb9a)\n[Diperlukan pengaktifan ulang agar perubahan konfigurasi dapat diterapkan.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| Laporan status sistem:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||
error_invalid_syntax: '[Kesalahan:](#ff3300) [Sintaks salah. Penggunaan:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Klik untuk menyarankan suggest_command=%1%)'
|
||||
error_invalid_player: '[Kesalahan:](#ff3300) [Tidak dapat menemukan pemain dengan nama tersebut.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
|
||||
@@ -23,11 +23,12 @@ locales:
|
||||
data_list_title: '[Lista delle istantanee di %1%:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Instantanea di %2%&8⚡ id: %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Fissata:\n&8Se fissata, l''istantanea non viene mai modificata. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Data di salvataggio:&7\n&8Momento preciso in cui è stato salvato il dato\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Causa di salvataggio:\n&8Che cosa ha causato il salvataggio run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Peso dell''istantanea:&7\n&8Peso stimato del file (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||
data_deleted: '[❌ Istantanea eliminata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Ripristato con successo](#00fb9a) [Dati dall''istantanea di](#00fb9a)[%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versione di UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Instantanea fissata](#00fb9a) [%1%](#00fb9a show_text=&7UUID della versione:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ L''istantanea dei dati utente è stata sbloccata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Hai ottenuto il dump dell''istantanea %1% di %2% nel formato:](#00fb9a) &7%3%'
|
||||
data_dumped: '[☂ Hai ottenuto il dump dell''istantanea %1% di %2% nel formato:](#00fb9a)'
|
||||
list_footer: '\n%1%[Pagina](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7Visualizza pagina precedente run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7Visualizza pagina successiva run_command=%2% %1%)'
|
||||
@@ -40,6 +41,8 @@ locales:
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_save_command: 'save command'
|
||||
save_cause_dump_command: 'dump command'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
@@ -51,6 +54,9 @@ locales:
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| Disponibile una nuova versione: v%1% (running: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Configurazione e messaggi ricaricati.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||
error_invalid_syntax: '[Errore:](#ff3300) [Sintassi errata. Usa:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Errore:](#ff3300) [Impossibile trovare un giocatore con questo nome.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
|
||||
@@ -12,7 +12,7 @@ locales:
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7バージョンタイムスタンプ:\n&8データの保存時期)'
|
||||
data_manager_pinned: '[※ ピン留めされたスナップショット](#d8ff2b show_text=&7ピン留め:\n&8このユーザーデータのスナップショットは自動的にローテーションされません。)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存理由:\n&8データが保存された理由)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7サーバー:\n&8データが保存されたサーバー名)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7スナップショットサイズ:\n&8スナップショットの推定ファイルサイズ(単位:KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7体力) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7空腹度) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7経験値レベル) [🏹 %5%](dark_aqua show_text=&7ゲームモード)'
|
||||
data_manager_advancements_statistics: '[⭐ 進捗: %1%](color=#ffc43b-#f5c962 show_text=&7達成した進捗:\n&8%2%) [⌛ プレイ時間: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7ゲーム内のプレイ時間\n&8⚠ ゲーム内の統計に基づく)\n'
|
||||
@@ -22,12 +22,13 @@ locales:
|
||||
data_manager_advancements_preview_remaining: 'さらに %1% 件…'
|
||||
data_list_title: '[%1% のユーザーデータスナップショット:](#00fb9a) [(%4%件中](#00fb9a bold) [%2%-%3%件](#00fb9a)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7%2% のユーザーデータスナップショット&8⚡ %4% run_command=/husksync:userdata view %2% %3%) [%5%](#d8ff2b show_text=&7ピン留め:\n&8ピン留めされたスナップショットは自動的にローテーションしません。 run_command=/husksync:userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7バージョンタイムスタンプ:&7\n&8データの保存時期\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7保存理由:\n&8データが保存された理由 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7スナップショットサイズ:&7\n&8スナップショットの推定ファイルサイズ (単位:KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7%2% のユーザーデータスナップショット\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7ピン留め:\n&8ピン留めされたスナップショットは自動的にローテーションしません。 suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&無効なデータのスナップショット\n&#ff7e5e&クリックで消去\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_saved: '[%1% の現在のユーザーデータのスナップショットを正常に保存しました。](#00fb9a)'
|
||||
data_deleted: '[❌](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [の消去に成功しました。](#00fb9a)'
|
||||
data_restored: '[⏪](#00fb9a) [スナップショット](#00fb9a) [%3%](#00fb9a show_text=&7Version UUID:\n&8%4%) [から](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [の現在のユーザーデータの復元に成功しました。](#00fb9a)'
|
||||
data_pinned: '[※](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [のピン留めに成功しました。](#00fb9a)'
|
||||
data_unpinned: '[※](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [のピン外しに成功しました。](#00fb9a)'
|
||||
data_dumped: '[☂ %2% のユーザーデータスナップショット %1% のダンプに成功:](#00fb9a) &7%3%'
|
||||
data_dumped: '[☂ %2% のユーザーデータスナップショット %1% のダンプに成功:](#00fb9a)'
|
||||
list_footer: '\n%1%[ページ](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7前のページへ run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7次のページへ run_command=%2% %1%)'
|
||||
@@ -36,30 +37,35 @@ locales:
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: 'disconnect'
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
save_cause_disconnect: '切断'
|
||||
save_cause_world_save: 'ワールド保存'
|
||||
save_cause_death: '死亡'
|
||||
save_cause_server_shutdown: 'サーバーシャットダウン'
|
||||
save_cause_save_command: 'セーブコマンド'
|
||||
save_cause_dump_command: 'ダンプコマンド'
|
||||
save_cause_inventory_command: 'インベントリコマンド'
|
||||
save_cause_enderchest_command: 'エンダーチェストコマンド'
|
||||
save_cause_backup_restore: 'バックアップ復元'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB migration'
|
||||
save_cause_legacy_migration: 'legacy migration'
|
||||
save_cause_converted_from_v2: 'converted from v2'
|
||||
save_cause_mpdb_migration: 'MPDB移行'
|
||||
save_cause_legacy_migration: 'レガシー移行'
|
||||
save_cause_converted_from_v2: 'v2からの変換'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| HuskSyncの最新バージョンを実行しています(v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| HuskSyncの最新バージョンが更新されています: v%1% (実行中: v%2%).](#ff7e5e)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| HuskSyncの新バージョンが利用可能になりました: v%1% (実行中: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 設定ファイルとメッセージファイルを再読み込みしました。](#00fb9a)\n[⚠ すべてのサーバーで設定ファイルが最新であることを確認してください!](#00fb9a)\n[設定の変更を有効にするには再起動が必要です。](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
error_invalid_syntax: '[Error:](#ff3300) [構文が正しくありません。使用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&クリックでサジェスト suggest_command=%1%)'
|
||||
error_invalid_player: '[Error:](#ff3300) [そのプレイヤーは見つかりませんでした](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[Error:](#ff3300) [このコマンドを実行する権限がありません](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [そのコマンドは%1%コンソールからのみ実行できます](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: そのコマンドはゲーム内でしか使えません。'
|
||||
error_no_data_to_display: '[Error:](#ff3300) [表示するユーザーデータが見つかりませんでした。](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Error:](#ff3300) [そのバージョンUUIDのユーザーデータが見つかりませんでした。](#ff7e5e)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| システムステータスレポート:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| システムダンプを作成しますか? 含まれる内容:](#00fb9a)\n[• 最新のサーバーログと HuskSync の設定ファイル](gray)\n[• 現在のプラグインシステムの状態](gray)\n[• Java と Minecraft サーバー環境の情報](gray)\n[• 現在インストールされている他のプラグイン一覧](gray)\n[確認するには、次を実行してください:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7クリックでダンプを準備 run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| システムステータスダンプを準備中です。お待ちください…](#00fb9a)'
|
||||
system_dump_ready: '[HuskSync](#00fb9a bold) [| システムステータスダンプが準備できました!クリックで表示:](#00fb9a)'
|
||||
error_invalid_syntax: '[エラー:](#ff3300) [構文が正しくありません。使用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&クリックでサジェスト suggest_command=%1%)'
|
||||
error_invalid_player: '[エラー:](#ff3300) [そのプレイヤーは見つかりませんでした](#ff7e5e)'
|
||||
error_invalid_data: '[エラー:](#ff3300) [スナップショットが無効または破損しているため、ユーザーデータを展開できません。](#ff7e5e) [(詳細…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[エラー:](#ff3300) [このコマンドを実行する権限がありません](#ff7e5e)'
|
||||
error_console_command_only: '[エラー:](#ff3300) [そのコマンドは%1%コンソールからのみ実行できます](#ff7e5e)'
|
||||
error_in_game_command_only: 'エラー: そのコマンドはゲーム内でしか使えません。'
|
||||
error_no_data_to_display: '[エラー:](#ff3300) [表示するユーザーデータが見つかりませんでした。](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[エラー:](#ff3300) [そのバージョンUUIDのユーザーデータが見つかりませんでした。](#ff7e5e)'
|
||||
husksync_command_description: 'HuskSyncプラグインを管理する'
|
||||
userdata_command_description: 'プレーヤーのユーザーデータを表示・管理・復元する'
|
||||
userdata_command_description: 'プレイヤーのユーザーデータを表示・管理・復元する'
|
||||
inventory_command_description: 'プレイヤーのインベントリを閲覧・編集する'
|
||||
enderchest_command_description: 'プレイヤーのエンダーチェストを閲覧・編集する'
|
||||
|
||||
@@ -23,11 +23,12 @@ locales:
|
||||
data_list_title: '[%1%님의 유저 데이터 스냅샷 목록:](#00fb9a) [(%2%-%3% 중](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7%2%&7님의 유저 데이터 스냅샷&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7고정됨:\n&8고정된 스냅샷은 자동적으로 갱신되지 않습니다. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7저장 시각:&7\n&8데이터가 저장된 시각입니다.\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7저장 사유:\n&8데이터가 저장된 사유입니다. run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7스냅샷 크기:&7\n&8스냅샷 파일의 대략적인 크기입니다. (단위 KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||
data_deleted: '[❌ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%) [님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 삭제하였습니다.](#00fb9a)'
|
||||
data_restored: '[⏪ 성공적으로 복구되었습니다.](#00fb9a) [%1%](#00fb9a show_text=&7플레이어 UUID:\n&8%2%)[님의 현재 유저 데이터 스냅샷이](#00fb9a) [%3%](#00fb9a show_text=&7버전 UUID:\n&8%4%)[으로 변경되었습니다.](#00fb9a)'
|
||||
data_pinned: '[※ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%)[님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 고정하였습니다.](#00fb9a)'
|
||||
data_unpinned: '[※ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%) [님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 고정 해제하였습니다.](#00fb9a)'
|
||||
data_dumped: '[☂ 성공적으로 %2%님의 유저 데이터 스냅샷 %1%를 다음으로 덤프하였습니다:](#00fb9a) &7%3%'
|
||||
data_dumped: '[☂ 성공적으로 %2%님의 유저 데이터 스냅샷 %1%를 다음으로 덤프하였습니다:](#00fb9a)'
|
||||
list_footer: '\n%1%[페이지](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7이전 페이지 보기 run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7다음 페이지 보기 run_command=%2% %1%)'
|
||||
@@ -40,6 +41,8 @@ locales:
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_save_command: 'save command'
|
||||
save_cause_dump_command: 'dump command'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
@@ -51,6 +54,9 @@ locales:
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| 새로운 버전의 HuskSync가 존재합니다: v%1% (현재 버전: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 콘피그와 메시지 파일을 다시 불러왔습니다.](#00fb9a)\n[⚠ 모든 서버의 컨피그 파일을 변경하였는지 확인하세요!](#00fb9a)\n[몇몇 설정은 재시작 후에 적용됩니다.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||
error_invalid_syntax: '[오류:](#ff3300) [잘못된 사용법. 사용법:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&클릭하여 입력할 수 있습니다. suggest_command=%1%)'
|
||||
error_invalid_player: '[오류:](#ff3300) [해당 이름의 사용자를 찾을 수 없습니다.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
|
||||
@@ -23,11 +23,12 @@ locales:
|
||||
data_list_title: '[%1%''s momentopnamen van gebruikersgegevens:](#00fb9a) [(%2%-%3% van](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Gebruikersgegevens momentopname voor %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Vastgezet:\n&8Vastgezette momentopnamen worden niet automatisch gerouleerd. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versie tijdmarkering:&7\n&8Wanneer de data was opgeslagen\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Reden opslaan:\n&8Waarom de data is opgeslagen run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Grootte van momentopname:&7\n&8Geschatte bestandsgrootte van de momentopname (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||
data_deleted: '[❌ Momentopname van gebruikersgegevens is verwijderd](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Succesvol hersteld](#00fb9a) [%1%](#00fb9a show_text=&7Speler UUID:\n&8%2%)[''s huidige gebruikersgegevens uit momentopname](#00fb9a) [%3%.](#00fb9a show_text=&7Versie UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Momentopname van gebruikersgegevens is vastgezet](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ Momentopname van gebruikersgegevens is losgemaakt](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ De momentopname van gebruikersgegevens %1% voor %2% is met succes gedumpt naar:](#00fb9a) &7%3%'
|
||||
data_dumped: '[☂ De momentopname van gebruikersgegevens %1% voor %2% is met succes gedumpt naar:](#00fb9a)'
|
||||
list_footer: '\n%1%[Pagina](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7Bekijk vorige pagina run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7Bekijk volgende pagina run_command=%2% %1%)'
|
||||
@@ -40,6 +41,8 @@ locales:
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_save_command: 'save command'
|
||||
save_cause_dump_command: 'dump command'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
@@ -51,6 +54,9 @@ locales:
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| Er is een nieuwe versie van HuskSync beschikbaar: v%1% (huidige versie: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Configuratie- en berichtbestanden opnieuw geladen.](#00fb9a)\n[⚠ Controleer of de configuratiebestanden up-to-date zijn op alle servers!](#00fb9a)\n[Een herstart is nodig voor de configuratiewijzigingen van kracht te laten worden.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||
error_invalid_syntax: '[Error:](#ff3300) [Onjuiste syntaxis. Gebruik:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Error:](#ff3300) [Kan geen speler met die naam vinden.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
|
||||
@@ -23,11 +23,12 @@ locales:
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||
data_deleted: '[❌ Snapshot de dados do usuário deletada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Restaurada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Snapshot de dados do usuário marcada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ Snapshot de dados do usuário desmarcada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
data_dumped: '[☂ Successfully dumped user data snapshot %1% for %2%. Click to view:](#00fb9a)'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
@@ -40,6 +41,8 @@ locales:
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_save_command: 'save command'
|
||||
save_cause_dump_command: 'dump command'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
@@ -51,6 +54,9 @@ locales:
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Arquivos de configuração e mensagens recarregados.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||
error_invalid_syntax: '[Error:](#ff3300) [Sintaxe incorreta. Utilize:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Error:](#ff3300) [Não foi possível encontrar um jogador com esse nome.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
|
||||
@@ -23,11 +23,12 @@ locales:
|
||||
data_list_title: '[Снимки данных %1%:](#00fb9a) [(%2%-%3% из](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Снимок данных %4% пользователя %2%&8⚡ run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Закреплен:\n&8Закрепленные снимки данных не удаляются автоматически run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Время:&7\n&8Когда данные были сохранены\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Причина сохранения:\n&8Что привело к сохранению данных run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Размер:&7\n&8Предполагаемый размер снимка (в килобайтах) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||
data_deleted: '[❌ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID игрока:\n&8%4%) [удален.](#00fb9a)'
|
||||
data_restored: '[⏪ Данные пользователя](#00fb9a) [%1%](#00fb9a show_text=&7UUID игрока:\n&8%2%) [из снимка](#00fb9a) [%3%](#00fb9a show_text=&7UUID снимка:\n&8%4%) [успешно восстановлены.](#00fb9a)'
|
||||
data_pinned: '[※ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID игрока:\n&8%4%) [успешно закреплен.](#00fb9a)'
|
||||
data_unpinned: '[※ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID пользователя:\n&8%4%) [успешно откреплен.](#00fb9a)'
|
||||
data_dumped: '[☂ Дамп снимка данных %1% пользователя %2% успешно выгружен:](#00fb9a) &7%3%'
|
||||
data_dumped: '[☂ Дамп снимка данных %1% пользователя %2% успешно выгружен:](#00fb9a)'
|
||||
list_footer: '\n%1%[Страница](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%\n'
|
||||
list_previous_page_button: '[◀](white show_text=&7Просмотр предыдущей страницы run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7Просмотр следующей страницы run_command=%2% %1%)'
|
||||
@@ -40,6 +41,8 @@ locales:
|
||||
save_cause_world_save: 'сохранение мира'
|
||||
save_cause_death: 'смерть'
|
||||
save_cause_server_shutdown: 'отключение сервера'
|
||||
save_cause_save_command: 'save command'
|
||||
save_cause_dump_command: 'dump command'
|
||||
save_cause_inventory_command: 'команда inventory'
|
||||
save_cause_enderchest_command: 'команда enderchest'
|
||||
save_cause_backup_restore: 'восстановление из снимка'
|
||||
@@ -51,6 +54,9 @@ locales:
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| Доступна новая версия HuskSync: v%1% (текущая: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Конфигурация и файлы локализации перезагружены.](#00fb9a)\n[⚠ Убедитесь, что файлы конфигурации обновлены на всех серверах!](#00fb9a)\n[Необходима перезагрузка для вступления изменений конфигурации в силу.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||
error_invalid_syntax: '[Ошибка:](#ff3300) [Неправильный синтаксис. Используйте:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Ошибка:](#ff3300) [Не удалось найти игрока с данным именем.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
|
||||
@@ -23,11 +23,12 @@ locales:
|
||||
data_list_title: '[%1%''ın kullanıcı veri anlıkları:](#00fb9a) [(%2%-%3% /](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Oyuncu Veri Anlığı %2% için %3%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Sabitlendi:\n&8Sabitlenmiş anlıklar otomatik olarak döndürülmez. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versiyon zaman damgası:&7\n&8Verinin ne zaman kaydedildiği\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Kaydetme sebebi:\n&8Verinin kaydedilme nedeni run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Anlık boyutu:&7\n&8Anlının tahmini dosya boyutu (KiB cinsinden) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||
data_deleted: '[❌ Kullanıcı veri anlığı başarıyla silindi](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Başarıyla geri yüklendi](#00fb9a) [%1%](#00fb9a show_text=&7Oyuncu UUID:\n&8%2%)[''ın mevcut kullanıcı verisi anlığından](#00fb9a) [%3%.](#00fb9a show_text=&7Versiyon UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Kullanıcı veri anlığı başarıyla sabitlendi](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ Kullanıcı veri anlığı başarıyla çözüldü](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Kullanıcı veri anlığı başarıyla döküldü %1% için %2%:](#00fb9a) &7%3%'
|
||||
data_dumped: '[☂ Kullanıcı veri anlığı başarıyla döküldü %1% için %2%:](#00fb9a)'
|
||||
list_footer: '\n%1%[Sayfa](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7Önceki sayfayı görüntüle run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7Sonraki sayfayı görüntüle run_command=%2% %1%)'
|
||||
@@ -40,6 +41,8 @@ locales:
|
||||
save_cause_world_save: 'dünya kaydı'
|
||||
save_cause_death: 'ölüm'
|
||||
save_cause_server_shutdown: 'sunucu kapatma'
|
||||
save_cause_save_command: 'save command'
|
||||
save_cause_dump_command: 'dump command'
|
||||
save_cause_inventory_command: 'envanter komutu'
|
||||
save_cause_enderchest_command: 'ender sandığı komutu'
|
||||
save_cause_backup_restore: 'yedek geri yükleme'
|
||||
@@ -51,6 +54,9 @@ locales:
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| HuskSync\''in yeni bir sürümü mevcut: v%1% (kullanılan sürüm: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Yapılandırma ve mesaj dosyaları yeniden yüklendi.](#00fb9a)\n[⚠ Lütfen yapılandırma dosyalarının tüm sunucularda güncel olduğundan emin olun!](#00fb9a)\n[Yapılandırma değişikliklerinin etkili olabilmesi için bir yeniden başlatma gereklidir.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| Sistem durumu raporu:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||
error_invalid_syntax: '[Hata:](#ff3300) [Yanlış sözdizimi. Kullanım:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Öneri için tıklayın Suggest_command=%1%)'
|
||||
error_invalid_player: '[Hata:](#ff3300) [Bu isimde bir oyuncu bulunamadı.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
|
||||
@@ -23,11 +23,12 @@ locales:
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ Successfully unpinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
data_dumped: '[☂ Successfully dumped user data snapshot %1% for %2%. Click to view:](#00fb9a)'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
@@ -40,6 +41,8 @@ locales:
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_save_command: 'save command'
|
||||
save_cause_dump_command: 'dump command'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
@@ -51,6 +54,9 @@ locales:
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Перезавантажено конфіґ та файли повідомлень.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||
error_invalid_syntax: '[Помилка:](#ff3300) [Неправильний синтакс. Використання:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Помилка:](#ff3300) [Гравця не знайдено](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user