mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-23 16:49:19 +00:00
Compare commits
246 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
099a258cf8 | ||
|
|
480f59a166 | ||
|
|
45c2f5350f | ||
|
|
ed88d77852 | ||
|
|
e7fc9f015e | ||
|
|
cabde9e8d8 | ||
|
|
4df7d2def4 | ||
|
|
59ed77c169 | ||
|
|
53da3bd40c | ||
|
|
abdf8223fc | ||
|
|
a5efeecad3 | ||
|
|
4d26b24d13 | ||
|
|
29b3a60c64 | ||
|
|
da894f57c4 | ||
|
|
1bd703641b | ||
|
|
1b1d4c8e8d | ||
|
|
842ec0e28d | ||
|
|
2d5648408e | ||
|
|
41b3240741 | ||
|
|
bc03e8f3e3 | ||
|
|
86799f4c08 | ||
|
|
a3e004cf71 | ||
|
|
a7aeb1de21 | ||
|
|
1a703102c3 | ||
|
|
368c68f42b | ||
|
|
e191713bdc | ||
|
|
1604338498 | ||
|
|
c223797bf4 | ||
|
|
9b10adc8e4 | ||
|
|
5935f1ab5f | ||
|
|
3455b10a20 | ||
|
|
34e08b712d | ||
|
|
605d314a58 | ||
|
|
daaf5147a7 | ||
|
|
50eb9a7543 | ||
|
|
7d8ef7b6b3 | ||
|
|
347d2d0a8f | ||
|
|
bd560fcc99 | ||
|
|
b68aedc99a | ||
|
|
47373d8974 | ||
|
|
a57b8df994 | ||
|
|
17235637a5 | ||
|
|
cd5abd5a65 | ||
|
|
5c6631cdcf | ||
|
|
621afcd5c6 | ||
|
|
112a974a6c | ||
|
|
f9d46b4aff | ||
|
|
dfd828bca1 | ||
|
|
2df9fd897a | ||
|
|
ff2531539e | ||
|
|
52ec138273 | ||
|
|
0f7a866652 | ||
|
|
eeb52ac41e | ||
|
|
4c7ec9ec21 | ||
|
|
2f9064c4c6 | ||
|
|
5c234cdb1d | ||
|
|
7d8a74381b | ||
|
|
04a7793585 | ||
|
|
ea068529f6 | ||
|
|
fead3df0d8 | ||
|
|
0c5a42a344 | ||
|
|
75a2378ea8 | ||
|
|
662fc96ad5 | ||
|
|
f456443da0 | ||
|
|
07da1c04ce | ||
|
|
845abf370a | ||
|
|
83b5209a75 | ||
|
|
8e9850dd19 | ||
|
|
1d24209b68 | ||
|
|
da70a54d78 | ||
|
|
fc7330213a | ||
|
|
d8272ba52d | ||
|
|
315f0eeb2f | ||
|
|
8e83617ac4 | ||
|
|
212bb0beb8 | ||
|
|
c16231b12b | ||
|
|
93f7294859 | ||
|
|
32ac57e2a4 | ||
|
|
c949c976d6 | ||
|
|
ab736829f2 | ||
|
|
4433926ce7 | ||
|
|
f819fd4d5e | ||
|
|
e7659255fe | ||
|
|
0dee2e8319 | ||
|
|
7b35c47315 | ||
|
|
5056a794d8 | ||
|
|
5e6068431a | ||
|
|
8d69508689 | ||
|
|
efb6d8a7de | ||
|
|
79d9778378 | ||
|
|
6a6695e447 | ||
|
|
8862e6cd70 | ||
|
|
0b29de9efc | ||
|
|
962cdfce0b | ||
|
|
0c527202e5 | ||
|
|
d4e33aa9d2 | ||
|
|
2fcd58fc18 | ||
|
|
3d10b2324f | ||
|
|
31419f3b97 | ||
|
|
8105ac27fc | ||
|
|
44f251a948 | ||
|
|
463e707d27 | ||
|
|
2d85910744 | ||
|
|
268b279fdf | ||
|
|
a8ca3314d8 | ||
|
|
2bdd3dae37 | ||
|
|
e29564c4ad | ||
|
|
6b8bb23af9 | ||
|
|
91bbe05851 | ||
|
|
8ed6869aad | ||
|
|
7efdf0d329 | ||
|
|
49c32e3f98 | ||
|
|
f0574527b9 | ||
|
|
ad510a8fca | ||
|
|
303b287705 | ||
|
|
549508b9c1 | ||
|
|
6c8a577701 | ||
|
|
862177bec7 | ||
|
|
dbed4d83a2 | ||
|
|
aa2090d97a | ||
|
|
b168ede7c5 | ||
|
|
0e706d36c4 | ||
|
|
69d68de5c0 | ||
|
|
3d5395e5ae | ||
|
|
332c71f041 | ||
|
|
b9fbcd72dd | ||
|
|
68897e6265 | ||
|
|
04606a7c9a | ||
|
|
6286bbe2ad | ||
|
|
24ba209f8f | ||
|
|
05d588f681 | ||
|
|
9aa3606f54 | ||
|
|
fc05e4b17a | ||
|
|
7b2b47de83 | ||
|
|
be0b4e3397 | ||
|
|
dd1ba594de | ||
|
|
89368778f3 | ||
|
|
e3fb1762a1 | ||
|
|
516c243df8 | ||
|
|
b7aa75fcd5 | ||
|
|
549f013e0f | ||
|
|
14c56af465 | ||
|
|
bee01dd15a | ||
|
|
e97551e67e | ||
|
|
97023e8425 | ||
|
|
4fa7106a46 | ||
|
|
e0b81e4c76 | ||
|
|
c4adec3082 | ||
|
|
107238360c | ||
|
|
6141adbdb9 | ||
|
|
eaa2ed74a6 | ||
|
|
44c652c452 | ||
|
|
78cf6bff63 | ||
|
|
8ad4158ec0 | ||
|
|
405e6d7162 | ||
|
|
cff1c8f982 | ||
|
|
f43ca2f043 | ||
|
|
3114ab1a62 | ||
|
|
2da9749b0c | ||
|
|
d4d510e100 | ||
|
|
550ea26097 | ||
|
|
2b1e72a42e | ||
|
|
75f8bee706 | ||
|
|
a3b50a0bf5 | ||
|
|
e9ab0909ce | ||
|
|
1e91b4b4ce | ||
|
|
043b51d812 | ||
|
|
fa5cea2aa3 | ||
|
|
e35dcf3aad | ||
|
|
68ec79add6 | ||
|
|
70235963ba | ||
|
|
245fbec80c | ||
|
|
4d1a465c03 | ||
|
|
07dc0b8c12 | ||
|
|
525f15e65b | ||
|
|
017d26673a | ||
|
|
087c787ec2 | ||
|
|
7218390f65 | ||
|
|
bd312c48ea | ||
|
|
e4cc792f54 | ||
|
|
7941745ed0 | ||
|
|
21f125c48a | ||
|
|
18b8b958fe | ||
|
|
35c23c7970 | ||
|
|
4bb38f67d3 | ||
|
|
98cf42065b | ||
|
|
328d4476aa | ||
|
|
8293d767da | ||
|
|
7b8fb92737 | ||
|
|
0f1cc2d24f | ||
|
|
676ba7a10a | ||
|
|
82dc765f66 | ||
|
|
16cfbc9410 | ||
|
|
2b4c7e6c3d | ||
|
|
a03d540938 | ||
|
|
6bcb3e7908 | ||
|
|
facbda65a8 | ||
|
|
2f5ddf6164 | ||
|
|
4dfbc0e32b | ||
|
|
07d0376dd6 | ||
|
|
d23ea087c1 | ||
|
|
ea77f2d782 | ||
|
|
ef3dc7e602 | ||
|
|
3fe6245ddf | ||
|
|
a35e83a424 | ||
|
|
be5d1128de | ||
|
|
8463e1bb7a | ||
|
|
5456b232f0 | ||
|
|
b0e585841c | ||
|
|
cd298af5ae | ||
|
|
e19477aada | ||
|
|
7f75b9a917 | ||
|
|
819421492b | ||
|
|
8f13a3955c | ||
|
|
73de0ff392 | ||
|
|
93edb0de4c | ||
|
|
bb5ae0b741 | ||
|
|
ccd7601a0e | ||
|
|
50d15e9580 | ||
|
|
aa1e8b8e95 | ||
|
|
3ff01f7bb3 | ||
|
|
93ab25bf44 | ||
|
|
4c0addfd67 | ||
|
|
b77cf2524d | ||
|
|
501ea3f609 | ||
|
|
a93af95fd2 | ||
|
|
39767c5cd0 | ||
|
|
48f7037898 | ||
|
|
67dddf0cfa | ||
|
|
eeb5e57c1e | ||
|
|
5a6ea2cffe | ||
|
|
07ddd34f8e | ||
|
|
a0b86c298f | ||
|
|
6fbef032bc | ||
|
|
318aacd432 | ||
|
|
ba1b2ff62e | ||
|
|
67ef4888da | ||
|
|
a5d3015c6e | ||
|
|
131a364f53 | ||
|
|
19636d9447 | ||
|
|
f803a0b57b | ||
|
|
28afffe95e | ||
|
|
c7e100a78a | ||
|
|
12e223618d | ||
|
|
f6773f4e68 | ||
|
|
b9434a56e8 |
44
.github/workflows/ci.yml
vendored
44
.github/workflows/ci.yml
vendored
@@ -1,44 +0,0 @@
|
||||
name: CI Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'master' ]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'workflows/**'
|
||||
- 'README.md'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Set up JDK 17 📦'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: 'Build with Gradle 🏗️'
|
||||
uses: gradle/gradle-build-action@v3
|
||||
with:
|
||||
arguments: build test publish
|
||||
env:
|
||||
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
- name: 'Fetch Version Name 📝'
|
||||
run: |
|
||||
echo "::set-output name=VERSION_NAME::$(${{github.workspace}}/gradlew properties --no-daemon --console=plain -q | grep "^version:" | awk '{printf $2}')"
|
||||
id: fetch-version
|
||||
- name: Get Version
|
||||
run: |
|
||||
echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV
|
||||
70
.github/workflows/ci_1.20.1.yml
vendored
Normal file
70
.github/workflows/ci_1.20.1.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
name: CI Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'minecraft/1.20.1' ]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'workflows/**'
|
||||
- 'README.md'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: 'Build - 1.20.1'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Setup JDK 21 📦'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
- name: 'Setup Gradle 8.8 🏗️'
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
gradle-version: '8.8'
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: 'minecraft/1.20.1'
|
||||
env:
|
||||
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: '[Current - 1.20.1] Build 🛎️'
|
||||
run: |
|
||||
./gradlew clean build publish
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
- name: 'Fetch Version String 📝'
|
||||
run: |
|
||||
echo "::set-output name=VERSION_NAME::$(./gradlew properties --no-daemon --console=plain -q | grep "^version:" | awk '{printf $2}')"
|
||||
id: fetch-version
|
||||
- name: 'Set Version Variable 📝'
|
||||
run: |
|
||||
echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV
|
||||
- name: 'Publish to William278.net 🚀'
|
||||
uses: WiIIiam278/bones-publish-action@v1
|
||||
with:
|
||||
api-key: ${{ secrets.BONES_API_KEY }}
|
||||
project: 'husksync'
|
||||
channel: 'alpha'
|
||||
version: ${{ env.version_name }}
|
||||
changelog: ${{ github.event.head_commit.message }}
|
||||
distro-names: |
|
||||
paper-1.20.1
|
||||
fabric-1.20.1
|
||||
distro-groups: |
|
||||
paper
|
||||
fabric
|
||||
distro-descriptions: |
|
||||
Paper 1.20.1
|
||||
Fabric 1.20.1
|
||||
files: |
|
||||
target/HuskSync-Paper-${{ env.version_name }}+mc.1.20.1.jar
|
||||
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.20.1.jar
|
||||
68
.github/workflows/ci_1.21.1.yml
vendored
Normal file
68
.github/workflows/ci_1.21.1.yml
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
name: CI Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'master' ]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'workflows/**'
|
||||
- 'README.md'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: 'Build - 1.21.1'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Setup JDK 21 📦'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
- name: 'Setup Gradle 8.8 🏗️'
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
gradle-version: '8.8'
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
env:
|
||||
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: '[Current - 1.21.1] Build 🛎️'
|
||||
run: |
|
||||
./gradlew clean build publish
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
- name: 'Fetch Version String 📝'
|
||||
run: |
|
||||
echo "::set-output name=VERSION_NAME::$(./gradlew properties --no-daemon --console=plain -q | grep "^version:" | awk '{printf $2}')"
|
||||
id: fetch-version
|
||||
- name: 'Set Version Variable 📝'
|
||||
run: |
|
||||
echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV
|
||||
- name: 'Publish to William278.net 🚀'
|
||||
uses: WiIIiam278/bones-publish-action@v1
|
||||
with:
|
||||
api-key: ${{ secrets.BONES_API_KEY }}
|
||||
project: 'husksync'
|
||||
channel: 'alpha'
|
||||
version: ${{ env.version_name }}
|
||||
changelog: ${{ github.event.head_commit.message }}
|
||||
distro-names: |
|
||||
paper-1.21.1
|
||||
fabric-1.21.1
|
||||
distro-groups: |
|
||||
paper
|
||||
fabric
|
||||
distro-descriptions: |
|
||||
Paper 1.21.1
|
||||
Fabric 1.21.1
|
||||
files: |
|
||||
target/HuskSync-Paper-${{ env.version_name }}+mc.1.21.1.jar
|
||||
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.1.jar
|
||||
4
.github/workflows/pr_tests.yml
vendored
4
.github/workflows/pr_tests.yml
vendored
@@ -14,10 +14,10 @@ jobs:
|
||||
steps:
|
||||
- name: 'Checkout for CI 🛎'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Set up JDK 17 📦'
|
||||
- name: 'Set up JDK 21 📦'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
- name: 'Build with Gradle 🏗️'
|
||||
uses: gradle/gradle-build-action@v3
|
||||
|
||||
64
.github/workflows/release.yml
vendored
64
.github/workflows/release.yml
vendored
@@ -8,26 +8,76 @@ permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: 'Publish Release'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Set up JDK 17 📦'
|
||||
- name: 'Setup JDK 21 📦'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
- name: 'Build with Gradle 🏗️'
|
||||
uses: gradle/gradle-build-action@v3
|
||||
- name: 'Setup Gradle 8.8 🏗️'
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
arguments: build test publish
|
||||
gradle-version: '8.8'
|
||||
- name: '[Current - 1.21.1] Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: '1_21_1'
|
||||
- name: '[LTS - 1.20.1] Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: 'minecraft/1.20.1'
|
||||
path: '1_20_1'
|
||||
env:
|
||||
RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
RELEASES_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: '[Current - 1.21.1] Build 🛎️'
|
||||
run: |
|
||||
mkdir target
|
||||
cd 1_21_1
|
||||
./gradlew clean build publish -Dforce-hide-version-meta=1
|
||||
cp -rf target/* ../target/
|
||||
cd ..
|
||||
- name: '[LTS - 1.20.1] Build 🛎️'
|
||||
run: |
|
||||
cd 1_20_1
|
||||
./gradlew clean build publish -Dforce-hide-version-meta=1
|
||||
cp -rf target/* ../target/
|
||||
cd ..
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
- name: 'Publish to William278.net 🚀'
|
||||
uses: WiIIiam278/bones-publish-action@v1
|
||||
with:
|
||||
api-key: ${{ secrets.BONES_API_KEY }}
|
||||
project: 'husksync'
|
||||
channel: 'release'
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
changelog: ${{ github.event.release.body }}
|
||||
distro-names: |
|
||||
paper-1.21.1
|
||||
fabric-1.21.1
|
||||
paper-1.20.1
|
||||
fabric-1.20.1
|
||||
distro-groups: |
|
||||
paper
|
||||
fabric
|
||||
paper
|
||||
fabric
|
||||
distro-descriptions: |
|
||||
Paper 1.21.1
|
||||
Fabric 1.21.1
|
||||
Paper 1.20.1
|
||||
Fabric 1.20.1
|
||||
files: |
|
||||
target/HuskSync-Paper-${{ github.event.release.tag_name }}+mc.1.21.1.jar
|
||||
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.1.jar
|
||||
target/HuskSync-Paper-${{ github.event.release.tag_name }}+mc.1.20.1.jar
|
||||
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.20.1.jar
|
||||
3
.github/workflows/update_docs.yml
vendored
3
.github/workflows/update_docs.yml
vendored
@@ -13,7 +13,8 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deploy-wiki:
|
||||
update-docs:
|
||||
name: 'Update Docs'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
|
||||
39
README.md
39
README.md
@@ -1,11 +1,11 @@
|
||||
<!--suppress ALL -->
|
||||
<p align="center">
|
||||
<img src="images/banner.png" alt="HuskSync" />
|
||||
<a href="https://github.com/WiIIiam278/HuskSync/actions/workflows/ci.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/ci.yml?branch=master&logo=github"/>
|
||||
<a href="https://github.com/WiIIiam278/HuskSync/actions/workflows/ci_1.21.1.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/ci_1.21.1.yml?branch=master&logo=github"/>
|
||||
</a>
|
||||
<a href="https://repo.william278.net/#/releases/net/william278/husksync/">
|
||||
<img src="https://repo.william278.net/api/badge/latest/releases/net/william278/husksync?color=00fb9a&name=Maven&prefix=v" />
|
||||
<img src="https://repo.william278.net/api/badge/latest/releases/net/william278/husksync/husksync-common?color=00fb9a&name=Maven&prefix=v" />
|
||||
</a>
|
||||
<a href="https://discord.gg/tVYhJfyDWG">
|
||||
<img src="https://img.shields.io/discord/818135932103557162.svg?label=&logo=discord&logoColor=fff&color=7389D8&labelColor=6A7EC2" />
|
||||
@@ -26,7 +26,7 @@
|
||||
</p>
|
||||
<br/>
|
||||
|
||||
**HuskSync** is a modern, cross-server player data synchronization system that enables the comprehensive synchronization of your user's data across multiple proxied servers. It does this by making use of Redis and MySQL to optimally cache data while players change servers.
|
||||
**HuskSync** is a modern, cross-server player data synchronization system that enables the comprehensive synchronization of your user's data across multiple proxied servers. It does this by making use of Redis and a MySQL/Mongo/PostgreSQL to optimally cache data while players change servers.
|
||||
|
||||
## Features
|
||||
**⭐ Seamless synchronization** — Utilises optimised Redis caching when players change server to sync player data super quickly for a seamless experience.
|
||||
@@ -43,16 +43,35 @@
|
||||
|
||||
**Ready?** [It's syncing time!](https://william278.net/docs/husksync/setup)
|
||||
|
||||
## Setup
|
||||
Requires a MySQL (v8.0+) database, a Redis (v5.0+) server and any number of Spigot-based 1.17.1+ Minecraft servers, running Java 17+.
|
||||
## 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:
|
||||
|
||||
1. Place the plugin jar file in the /plugins/ directory of each Spigot server. You do not need to install HuskSync as a proxy plugin.
|
||||
| Minecraft | Latest HuskSync | Java Version | Platforms | Support Ends |
|
||||
|:---------------:|:---------------:|:------------:|:--------------|:--------------------------|
|
||||
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **Active Release** |
|
||||
| 1.20.6 | 3.6.8 | 17 | Paper | ❌ _October 2024_ |
|
||||
| 1.20.4 | 3.6.8 | 17 | Paper | ❌ _July 2024_ |
|
||||
| 1.20.1 | _latest_ | 17 | Paper, Fabric | ✅ **November 2025** (LTS) |
|
||||
| 1.17.1 - 1.19.4 | 3.6.8 | 17 | Paper | ❌ _Support ended_ |
|
||||
| 1.16.5 | 3.2.1 | 16 | Paper | ❌ _Support ended_ |
|
||||
|
||||
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 or Fabric Minecraft servers (see [Compatibility](#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.
|
||||
3. Navigate to the HuskSync config file on each server (~/plugins/HuskSync/config.yml) and fill in both the MySQL and Redis database credentials.
|
||||
3. Navigate to the HuskSync config file on each server and fill in both your database and Redis server credentials.
|
||||
4. Start every server again and synchronization will begin.
|
||||
|
||||
## Development
|
||||
To build HuskSync, simply run the following in the root of the repository:
|
||||
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
|
||||
@@ -66,7 +85,7 @@ HuskSync is licensed under the Apache 2.0 license.
|
||||
Contributions to the project are welcome—feel free to open a pull request with new features, improvements and/or fixes!
|
||||
|
||||
### Support
|
||||
Due to its complexity, official binaries and customer support for HuskSync is provided through a paid model. This means that support is only available to users who have purchased a license to the plugin from Spigot, Polymart, Craftaro, or BuiltByBit and have provided proof of purchase. Please join our Discord server if you have done so and need help!
|
||||
Due to its complexity, official binaries and customer support for HuskSync is provided through a paid model. This means that support is only available to users who have purchased a license to the plugin from Spigot, Polymart, or BuiltByBit and have provided proof of purchase. Please join our Discord server if you have done so and need help!
|
||||
|
||||
### Translations
|
||||
Translations of the plugin locales are welcome to help make the plugin more accessible. Please submit a pull request with your translations as a `.yml` file.
|
||||
|
||||
79
build.gradle
79
build.gradle
@@ -1,9 +1,10 @@
|
||||
import org.apache.tools.ant.filters.ReplaceTokens
|
||||
|
||||
plugins {
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||
id 'com.gradleup.shadow' version '8.3.2'
|
||||
id 'org.cadixdev.licenser' version '0.6.1' apply false
|
||||
id 'org.ajoberstar.grgit' version '5.2.1'
|
||||
id 'fabric-loom' version '1.7-SNAPSHOT' apply false
|
||||
id 'org.ajoberstar.grgit' version '5.2.2'
|
||||
id 'maven-publish'
|
||||
id 'java'
|
||||
}
|
||||
@@ -17,9 +18,12 @@ ext {
|
||||
set 'version', version.toString()
|
||||
set 'description', description.toString()
|
||||
|
||||
set 'minecraft_version', minecraft_version.toString()
|
||||
set 'jedis_version', jedis_version.toString()
|
||||
set 'mysql_driver_version', mysql_driver_version.toString()
|
||||
set 'mariadb_driver_version', mariadb_driver_version.toString()
|
||||
set 'postgres_driver_version', postgres_driver_version.toString()
|
||||
set 'mongodb_driver_version', mongodb_driver_version.toString()
|
||||
set 'snappy_version', snappy_version.toString()
|
||||
}
|
||||
|
||||
@@ -55,32 +59,35 @@ publishing {
|
||||
}
|
||||
|
||||
allprojects {
|
||||
apply plugin: 'com.github.johnrengelman.shadow'
|
||||
apply plugin: 'com.gradleup.shadow'
|
||||
apply plugin: 'org.cadixdev.licenser'
|
||||
apply plugin: 'java'
|
||||
|
||||
compileJava.options.encoding = 'UTF-8'
|
||||
compileJava.options.release.set 17
|
||||
compileJava.options.release.set Integer.parseInt(rootProject.ext.javaVersion)
|
||||
javadoc.options.encoding = 'UTF-8'
|
||||
javadoc.options.addStringOption('Xdoclint:none', '-quiet')
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven { url 'https://repo.william278.net/releases/' }
|
||||
maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
|
||||
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
|
||||
maven { url 'https://repo.papermc.io/repository/maven-public/' }
|
||||
maven { url "https://repo.dmulloy2.net/repository/public/" }
|
||||
maven { url 'https://repo.codemc.io/repository/maven-public/' }
|
||||
maven { url 'https://repo.minebench.de/' }
|
||||
maven { url 'https://repo.alessiodp.com/releases/' }
|
||||
maven { url 'https://jitpack.io' }
|
||||
maven { url 'https://mvn-repo.arim.space/lesser-gpl3/' }
|
||||
maven { url 'https://libraries.minecraft.net/' }
|
||||
maven { url 'https://repo.william278.net/releases/' }
|
||||
}
|
||||
|
||||
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'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.1'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.1'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.1'
|
||||
}
|
||||
|
||||
test {
|
||||
@@ -94,14 +101,20 @@ allprojects {
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
version rootProject.version
|
||||
archivesBaseName = "${rootProject.name}-${project.name.capitalize()}"
|
||||
|
||||
@@ -114,8 +127,13 @@ subprojects {
|
||||
archiveClassifier.set('')
|
||||
}
|
||||
|
||||
// Append the compatible Minecraft version to the version
|
||||
if (['bukkit', 'paper', 'fabric'].contains(project.name)) {
|
||||
version += "+mc.${minecraft_version}"
|
||||
}
|
||||
|
||||
// API publishing
|
||||
if (['common', 'bukkit'].contains(project.name)) {
|
||||
if (['common', 'bukkit', 'fabric'].contains(project.name)) {
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
@@ -147,13 +165,26 @@ subprojects {
|
||||
mavenJavaBukkit(MavenPublication) {
|
||||
groupId = 'net.william278.husksync'
|
||||
artifactId = 'husksync-bukkit'
|
||||
version = "$rootProject.version"
|
||||
version = "$rootProject.version+${minecraft_version}"
|
||||
artifact shadowJar
|
||||
artifact sourcesJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (['fabric'].contains(project.name)) {
|
||||
publications {
|
||||
mavenJavaFabric(MavenPublication) {
|
||||
groupId = 'net.william278.husksync'
|
||||
artifactId = 'husksync-fabric'
|
||||
version = "$rootProject.version+${minecraft_version}"
|
||||
artifact remapJar
|
||||
artifact sourcesJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,19 +192,29 @@ subprojects {
|
||||
clean.delete "$rootDir/target"
|
||||
}
|
||||
|
||||
logger.lifecycle("Building HuskSync ${version} by William278")
|
||||
logger.lifecycle("Building HuskSync ${version} by William278 for Minecraft ${minecraft_version}")
|
||||
|
||||
@SuppressWarnings('GrMethodMayBeStatic')
|
||||
def versionMetadata() {
|
||||
// Get if there is a tag for this commit
|
||||
// If the force-hide-version-meta environment variable is set, return ''
|
||||
if (System.getProperty('force-hide-version-meta') != null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// Require grgit
|
||||
if (grgit == null) {
|
||||
return '-unknown'
|
||||
}
|
||||
|
||||
// If unclean, return the last commit hash with -indev
|
||||
if (!grgit.status().clean) {
|
||||
return '-' + grgit.head().abbreviatedId + '-indev'
|
||||
}
|
||||
|
||||
// Otherwise if this matches a tag, return nothing
|
||||
def tag = grgit.tag.list().find { it.commit.id == grgit.head().id }
|
||||
if (tag != null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// Otherwise, get the last commit hash and if it's a clean head
|
||||
if (grgit == null) {
|
||||
return '-' + System.getenv("GITHUB_RUN_NUMBER") ? 'build.' + System.getenv("GITHUB_RUN_NUMBER") : 'unknown'
|
||||
}
|
||||
return '-' + grgit.head().abbreviatedId + (grgit.status().clean ? '' : '-indev')
|
||||
return '-' + grgit.head().abbreviatedId
|
||||
}
|
||||
@@ -1,29 +1,30 @@
|
||||
dependencies {
|
||||
implementation project(path: ':common')
|
||||
|
||||
implementation 'org.bstats:bstats-bukkit:3.0.2'
|
||||
implementation 'net.william278.uniform:uniform-bukkit:1.2.1'
|
||||
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 'me.lucko:commodore:2.2'
|
||||
implementation 'net.kyori:adventure-platform-bukkit:4.3.2'
|
||||
implementation 'dev.triumphteam:triumph-gui:3.1.7'
|
||||
implementation 'space.arim.morepaperlib:morepaperlib:0.4.3'
|
||||
implementation 'de.tr7zw:item-nbt-api:2.12.2'
|
||||
implementation 'org.bstats:bstats-bukkit:3.1.0'
|
||||
implementation 'net.kyori:adventure-platform-bukkit:4.3.4'
|
||||
implementation 'dev.triumphteam:triumph-gui:3.1.10'
|
||||
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
|
||||
implementation 'de.tr7zw:item-nbt-api:2.13.2'
|
||||
|
||||
compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.30'
|
||||
compileOnly 'commons-io:commons-io:2.15.1'
|
||||
compileOnly 'org.json:json:20231013'
|
||||
compileOnly 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
|
||||
compileOnly 'com.github.Exlll.ConfigLib:configlib-yaml:v4.4.0'
|
||||
compileOnly 'com.zaxxer:HikariCP:5.1.0'
|
||||
compileOnly "org.spigotmc:spigot-api:${bukkit_spigot_api}"
|
||||
compileOnly 'com.github.retrooper.packetevents:spigot:2.3.0'
|
||||
compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.34'
|
||||
compileOnly 'commons-io:commons-io:2.17.0'
|
||||
compileOnly 'org.json:json:20240303'
|
||||
compileOnly 'net.william278:minedown:1.8.2'
|
||||
compileOnly 'de.exlll:configlib-yaml:4.5.0'
|
||||
compileOnly 'com.zaxxer:HikariCP:6.0.0'
|
||||
compileOnly 'net.william278:DesertWell:2.0.4'
|
||||
compileOnly 'net.william278:AdvancementAPI:97a9583413'
|
||||
compileOnly "redis.clients:jedis:$jedis_version"
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.30'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.34'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
@@ -39,17 +40,16 @@ shadowJar {
|
||||
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'de.exlll', 'net.william278.huskclaims.libraries'
|
||||
relocate 'de.exlll', 'net.william278.husksync.libraries'
|
||||
relocate 'net.william278.uniform', 'net.william278.husksync.libraries.uniform'
|
||||
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'
|
||||
relocate 'net.querz', 'net.william278.husksync.libraries.nbtparser'
|
||||
relocate 'net.roxeez', 'net.william278.husksync.libraries'
|
||||
relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
|
||||
relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib'
|
||||
|
||||
@@ -34,20 +34,18 @@ import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.adapter.GsonAdapter;
|
||||
import net.william278.husksync.adapter.SnappyGsonAdapter;
|
||||
import net.william278.husksync.api.BukkitHuskSyncAPI;
|
||||
import net.william278.husksync.command.BukkitCommand;
|
||||
import net.william278.husksync.command.PluginCommand;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.config.Server;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.BukkitSerializer;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.Serializer;
|
||||
import net.william278.husksync.data.*;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.database.MongoDbDatabase;
|
||||
import net.william278.husksync.database.MySqlDatabase;
|
||||
import net.william278.husksync.database.PostgresDatabase;
|
||||
import net.william278.husksync.event.BukkitEventDispatcher;
|
||||
import net.william278.husksync.hook.PlanHook;
|
||||
import net.william278.husksync.listener.BukkitEventListener;
|
||||
import net.william278.husksync.listener.EventListener;
|
||||
import net.william278.husksync.migrator.LegacyMigrator;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.migrator.MpdbMigrator;
|
||||
@@ -59,15 +57,17 @@ 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.uniform.Uniform;
|
||||
import net.william278.uniform.bukkit.BukkitUniform;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.map.MapView;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import space.arim.morepaperlib.MorePaperLib;
|
||||
import space.arim.morepaperlib.commands.CommandRegistration;
|
||||
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
|
||||
import space.arim.morepaperlib.scheduling.AttachedScheduler;
|
||||
import space.arim.morepaperlib.scheduling.GracefulScheduling;
|
||||
import space.arim.morepaperlib.scheduling.RegionalScheduler;
|
||||
|
||||
@@ -87,7 +87,9 @@ 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 Map<Identifier, Serializer<? extends Data>> serializers = Maps.newLinkedHashMap();
|
||||
private final TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap(
|
||||
SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR
|
||||
);
|
||||
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();
|
||||
@@ -99,7 +101,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
private MorePaperLib paperLib;
|
||||
private Database database;
|
||||
private RedisManager redisManager;
|
||||
private EventListener eventListener;
|
||||
private BukkitEventListener eventListener;
|
||||
private DataAdapter dataAdapter;
|
||||
private DataSyncer dataSyncer;
|
||||
private LegacyConverter legacyConverter;
|
||||
@@ -114,11 +116,10 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
private Server serverName;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
public void onLoad() {
|
||||
// Initial plugin setup
|
||||
this.disabling = false;
|
||||
this.gson = createGson();
|
||||
this.audiences = BukkitAudiences.create(this);
|
||||
this.paperLib = new MorePaperLib(this);
|
||||
|
||||
// Load settings and locales
|
||||
@@ -128,6 +129,20 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
loadServer();
|
||||
});
|
||||
|
||||
this.eventListener = createEventListener();
|
||||
eventListener.onLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
this.audiences = BukkitAudiences.create(this);
|
||||
|
||||
// Check compatibility
|
||||
checkCompatibility();
|
||||
|
||||
// Register commands
|
||||
initialize("commands", (plugin) -> getUniform().register(PluginCommand.Type.create(this)));
|
||||
|
||||
// Prepare data adapter
|
||||
initialize("data adapter", (plugin) -> {
|
||||
if (settings.getSynchronization().isCompressData()) {
|
||||
@@ -139,17 +154,20 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
|
||||
// Prepare serializers
|
||||
initialize("data serializers", (plugin) -> {
|
||||
registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
|
||||
registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this));
|
||||
registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this));
|
||||
registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(this));
|
||||
registerSerializer(Identifier.LOCATION, new BukkitSerializer.Location(this));
|
||||
registerSerializer(Identifier.HEALTH, new BukkitSerializer.Health(this));
|
||||
registerSerializer(Identifier.HUNGER, new BukkitSerializer.Hunger(this));
|
||||
registerSerializer(Identifier.GAME_MODE, new BukkitSerializer.GameMode(this));
|
||||
registerSerializer(Identifier.STATISTICS, new Serializer.Json<>(this, BukkitData.Statistics.class));
|
||||
registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this));
|
||||
registerSerializer(Identifier.STATISTICS, new BukkitSerializer.Statistics(this));
|
||||
registerSerializer(Identifier.EXPERIENCE, new BukkitSerializer.Experience(this));
|
||||
registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
|
||||
registerSerializer(Identifier.GAME_MODE, new Serializer.Json<>(this, BukkitData.GameMode.class));
|
||||
registerSerializer(Identifier.FLIGHT_STATUS, new Serializer.Json<>(this, BukkitData.FlightStatus.class));
|
||||
registerSerializer(Identifier.ATTRIBUTES, new Serializer.Json<>(this, BukkitData.Attributes.class));
|
||||
registerSerializer(Identifier.HEALTH, new Serializer.Json<>(this, BukkitData.Health.class));
|
||||
registerSerializer(Identifier.HUNGER, new Serializer.Json<>(this, BukkitData.Hunger.class));
|
||||
registerSerializer(Identifier.EXPERIENCE, new Serializer.Json<>(this, BukkitData.Experience.class));
|
||||
registerSerializer(Identifier.LOCATION, new Serializer.Json<>(this, BukkitData.Location.class));
|
||||
validateDependencies();
|
||||
});
|
||||
|
||||
// Setup available migrators
|
||||
@@ -163,7 +181,11 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
|
||||
// Initialize the database
|
||||
initialize(getSettings().getDatabase().getType().getDisplayName() + " database connection", (plugin) -> {
|
||||
this.database = new MySqlDatabase(this);
|
||||
this.database = switch (settings.getDatabase().getType()) {
|
||||
case MYSQL, MARIADB -> new MySqlDatabase(this);
|
||||
case POSTGRES -> new PostgresDatabase(this);
|
||||
case MONGO -> new MongoDbDatabase(this);
|
||||
};
|
||||
this.database.initialize();
|
||||
});
|
||||
|
||||
@@ -180,10 +202,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
});
|
||||
|
||||
// Register events
|
||||
initialize("events", (plugin) -> this.eventListener = createEventListener());
|
||||
|
||||
// Register commands
|
||||
initialize("commands", (plugin) -> BukkitCommand.Type.registerCommands(this));
|
||||
initialize("events", (plugin) -> eventListener.onEnable());
|
||||
|
||||
// Register plugin hooks
|
||||
initialize("hooks", (plugin) -> {
|
||||
@@ -229,7 +248,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
@Override
|
||||
@NotNull
|
||||
public Set<OnlineUser> getOnlineUsers() {
|
||||
return Bukkit.getOnlinePlayers().stream()
|
||||
return getServer().getOnlinePlayers().stream()
|
||||
.map(player -> BukkitUser.adapt(player, this))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
@@ -237,7 +256,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
@Override
|
||||
@NotNull
|
||||
public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
|
||||
final Player player = Bukkit.getPlayer(uuid);
|
||||
final Player player = getServer().getPlayer(uuid);
|
||||
if (player == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
@@ -250,15 +269,19 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
this.dataSyncer = dataSyncer;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Uniform getUniform() {
|
||||
return BukkitUniform.getInstance(this);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
|
||||
if (playerCustomDataStore.containsKey(user.getUuid())) {
|
||||
return playerCustomDataStore.get(user.getUuid());
|
||||
}
|
||||
final Map<Identifier, Data> data = Maps.newHashMap();
|
||||
playerCustomDataStore.put(user.getUuid(), data);
|
||||
return data;
|
||||
return playerCustomDataStore.compute(
|
||||
user.getUuid(),
|
||||
(uuid, data) -> data == null ? Maps.newHashMap() : data
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -269,7 +292,8 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
|
||||
@Override
|
||||
public boolean isDependencyLoaded(@NotNull String name) {
|
||||
return Bukkit.getPluginManager().getPlugin(name) != null;
|
||||
final Plugin plugin = getServer().getPluginManager().getPlugin(name);
|
||||
return plugin != null;
|
||||
}
|
||||
|
||||
// Register bStats metrics
|
||||
@@ -281,7 +305,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
try {
|
||||
new Metrics(this, metricsId);
|
||||
} catch (Throwable e) {
|
||||
log(Level.WARNING, "Failed to register bStats metrics (" + e.getMessage() + ")");
|
||||
log(Level.WARNING, "Failed to register bStats metrics (%s)".formatted(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +327,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
@NotNull
|
||||
@Override
|
||||
public Version getMinecraftVersion() {
|
||||
return Version.fromString(Bukkit.getBukkitVersion());
|
||||
return Version.fromString(getServer().getBukkitVersion());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -312,6 +336,12 @@ 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);
|
||||
@@ -329,14 +359,14 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public RegionalScheduler getRegionalScheduler() {
|
||||
public RegionalScheduler getSyncScheduler() {
|
||||
return regionalScheduler == null
|
||||
? regionalScheduler = getScheduler().globalRegionalScheduler() : regionalScheduler;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public CommandRegistration getCommandRegistrar() {
|
||||
return paperLib.commandRegistration();
|
||||
public AttachedScheduler getUserSyncScheduler(@NotNull UserDataHolder user) {
|
||||
return getScheduler().entitySpecificScheduler(((BukkitUser) user).getPlayer());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -347,7 +377,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public HuskSync getPlugin() {
|
||||
public BukkitHuskSync getPlugin() {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -59,6 +60,10 @@ public class BukkitHuskSyncAPI extends HuskSyncAPI {
|
||||
*/
|
||||
@NotNull
|
||||
public static BukkitHuskSyncAPI getInstance() {
|
||||
if (!JavaPlugin.getProvidingPlugin(BukkitHuskSyncAPI.class).getName().equals("HuskSync")) {
|
||||
throw new NotRegisteredException("This is likely because you have shaded HuskSync into your plugin JAR " +
|
||||
"and need to fix your maven/gradle/build script so that it *compiles against* HuskSync instead.");
|
||||
}
|
||||
if (instance == null) {
|
||||
throw new NotRegisteredException();
|
||||
}
|
||||
|
||||
@@ -1,60 +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.command;
|
||||
|
||||
import me.lucko.commodore.CommodoreProvider;
|
||||
import me.lucko.commodore.file.CommodoreFileReader;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BrigadierUtil {
|
||||
|
||||
/**
|
||||
* Uses commodore to register command completions.
|
||||
*
|
||||
* @param plugin instance of the registering Bukkit plugin
|
||||
* @param bukkitCommand the Bukkit PluginCommand to register completions for
|
||||
* @param command the {@link Command} to register completions for
|
||||
*/
|
||||
protected static void registerCommodore(@NotNull BukkitHuskSync plugin,
|
||||
@NotNull org.bukkit.command.Command bukkitCommand,
|
||||
@NotNull Command command) {
|
||||
final InputStream commodoreFile = plugin.getResource(
|
||||
"commodore/" + bukkitCommand.getName() + ".commodore"
|
||||
);
|
||||
if (commodoreFile == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
CommodoreProvider.getCommodore(plugin).register(bukkitCommand,
|
||||
CommodoreFileReader.INSTANCE.parse(commodoreFile),
|
||||
player -> player.hasPermission(command.getPermission()));
|
||||
} catch (IOException e) {
|
||||
plugin.log(Level.SEVERE, String.format(
|
||||
"Failed to read command commodore completions for %s", bukkitCommand.getName()), e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,164 +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.command;
|
||||
|
||||
|
||||
import me.lucko.commodore.CommodoreProvider;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.permissions.Permission;
|
||||
import org.bukkit.permissions.PermissionDefault;
|
||||
import org.bukkit.plugin.PluginManager;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class BukkitCommand extends org.bukkit.command.Command {
|
||||
|
||||
private final BukkitHuskSync plugin;
|
||||
private final Command command;
|
||||
|
||||
public BukkitCommand(@NotNull Command command, @NotNull BukkitHuskSync plugin) {
|
||||
super(command.getName(), command.getDescription(), command.getUsage(), command.getAliases());
|
||||
this.command = command;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) {
|
||||
this.command.onExecuted(sender instanceof Player p ? BukkitUser.adapt(p, plugin) : plugin.getConsole(), args);
|
||||
return true;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias,
|
||||
@NotNull String[] args) throws IllegalArgumentException {
|
||||
if (!(this.command instanceof TabProvider provider)) {
|
||||
return List.of();
|
||||
}
|
||||
final CommandUser user = sender instanceof Player p ? BukkitUser.adapt(p, plugin) : plugin.getConsole();
|
||||
if (getPermission() == null || user.hasPermission(getPermission())) {
|
||||
return provider.getSuggestions(user, args);
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
public void register() {
|
||||
// Register with bukkit
|
||||
plugin.getCommandRegistrar().getServerCommandMap().register("husksync", this);
|
||||
|
||||
// Register permissions
|
||||
BukkitCommand.addPermission(
|
||||
plugin,
|
||||
command.getPermission(),
|
||||
command.getUsage(),
|
||||
BukkitCommand.getPermissionDefault(command.isOperatorCommand())
|
||||
);
|
||||
final List<Permission> childNodes = command.getAdditionalPermissions()
|
||||
.entrySet().stream()
|
||||
.map((entry) -> BukkitCommand.addPermission(
|
||||
plugin,
|
||||
entry.getKey(),
|
||||
"",
|
||||
BukkitCommand.getPermissionDefault(entry.getValue()))
|
||||
)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
if (!childNodes.isEmpty()) {
|
||||
BukkitCommand.addPermission(
|
||||
plugin,
|
||||
command.getPermission("*"),
|
||||
command.getUsage(),
|
||||
PermissionDefault.FALSE,
|
||||
childNodes.toArray(new Permission[0])
|
||||
);
|
||||
}
|
||||
|
||||
// Register commodore TAB completion
|
||||
if (CommodoreProvider.isSupported() && plugin.getSettings().isBrigadierTabCompletion()) {
|
||||
BrigadierUtil.registerCommodore(plugin, this, command);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected static Permission addPermission(@NotNull BukkitHuskSync plugin, @NotNull String node,
|
||||
@NotNull String description, @NotNull PermissionDefault permissionDefault,
|
||||
@NotNull Permission... children) {
|
||||
final Map<String, Boolean> childNodes = Arrays.stream(children)
|
||||
.map(Permission::getName)
|
||||
.collect(HashMap::new, (map, child) -> map.put(child, true), HashMap::putAll);
|
||||
|
||||
final PluginManager manager = plugin.getServer().getPluginManager();
|
||||
if (manager.getPermission(node) != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Permission permission;
|
||||
if (description.isEmpty()) {
|
||||
permission = new Permission(node, permissionDefault, childNodes);
|
||||
} else {
|
||||
permission = new Permission(node, description, permissionDefault, childNodes);
|
||||
}
|
||||
manager.addPermission(permission);
|
||||
|
||||
return permission;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static PermissionDefault getPermissionDefault(boolean isOperatorCommand) {
|
||||
return isOperatorCommand ? PermissionDefault.OP : PermissionDefault.TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Commands available on the Bukkit HuskSync implementation
|
||||
*/
|
||||
public enum Type {
|
||||
|
||||
HUSKSYNC_COMMAND(HuskSyncCommand::new),
|
||||
USERDATA_COMMAND(UserDataCommand::new),
|
||||
INVENTORY_COMMAND(InventoryCommand::new),
|
||||
ENDER_CHEST_COMMAND(EnderChestCommand::new);
|
||||
|
||||
public final Function<BukkitHuskSync, Command> commandSupplier;
|
||||
|
||||
Type(@NotNull Function<BukkitHuskSync, Command> supplier) {
|
||||
this.commandSupplier = supplier;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Command createCommand(@NotNull BukkitHuskSync plugin) {
|
||||
return commandSupplier.apply(plugin);
|
||||
}
|
||||
|
||||
public static void registerCommands(@NotNull BukkitHuskSync plugin) {
|
||||
Arrays.stream(values())
|
||||
.map((type) -> type.createCommand(plugin))
|
||||
.forEach((command) -> new BukkitCommand(command, plugin).register());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,27 +21,35 @@ package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import de.tr7zw.changeme.nbtapi.NBT;
|
||||
import de.tr7zw.changeme.nbtapi.NBTCompound;
|
||||
import de.tr7zw.changeme.nbtapi.NBTContainer;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBTCompoundList;
|
||||
import de.tr7zw.changeme.nbtapi.utils.DataFixerUtil;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.api.HuskSyncAPI;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static net.william278.husksync.data.BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
|
||||
import static net.william278.husksync.data.Data.Items.Inventory.HELD_ITEM_SLOT_TAG;
|
||||
import static net.william278.husksync.data.Data.Items.Inventory.ITEMS_TAG;
|
||||
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class BukkitSerializer {
|
||||
|
||||
protected final HuskSync plugin;
|
||||
|
||||
private BukkitSerializer(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public BukkitSerializer(@NotNull HuskSyncAPI api) {
|
||||
this.plugin = api.getPlugin();
|
||||
@@ -53,25 +61,29 @@ public class BukkitSerializer {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
public static class Inventory extends BukkitSerializer implements Serializer<BukkitData.Items.Inventory> {
|
||||
private static final String ITEMS_TAG = "items";
|
||||
private static final String HELD_ITEM_SLOT_TAG = "held_item_slot";
|
||||
public static class Inventory extends BukkitSerializer implements Serializer<BukkitData.Items.Inventory>,
|
||||
ItemDeserializer {
|
||||
|
||||
public Inventory(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.Items.Inventory deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
public BukkitData.Items.Inventory deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
|
||||
throws DeserializationException {
|
||||
final ReadWriteNBT root = NBT.parseNBT(serialized);
|
||||
final ItemStack[] items = root.getItemStackArray(ITEMS_TAG);
|
||||
final int heldItemSlot = root.getInteger(HELD_ITEM_SLOT_TAG);
|
||||
final ReadWriteNBT items = root.hasTag(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null;
|
||||
return BukkitData.Items.Inventory.from(
|
||||
items == null ? new ItemStack[INVENTORY_SLOT_COUNT] : items,
|
||||
heldItemSlot
|
||||
items != null ? getItems(items, dataMcVersion) : new ItemStack[INVENTORY_SLOT_COUNT],
|
||||
root.hasTag(HELD_ITEM_SLOT_TAG) ? root.getInteger(HELD_ITEM_SLOT_TAG) : 0
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.Items.Inventory deserialize(@NotNull String serialized) {
|
||||
return deserialize(serialized, plugin.getMinecraftVersion());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.Items.Inventory data) throws SerializationException {
|
||||
@@ -83,18 +95,25 @@ public class BukkitSerializer {
|
||||
|
||||
}
|
||||
|
||||
public static class EnderChest extends BukkitSerializer implements Serializer<BukkitData.Items.EnderChest> {
|
||||
public static class EnderChest extends BukkitSerializer implements Serializer<BukkitData.Items.EnderChest>,
|
||||
ItemDeserializer {
|
||||
|
||||
public EnderChest(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.Items.EnderChest deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
final ItemStack[] items = NBT.itemStackArrayFromNBT(NBT.parseNBT(serialized));
|
||||
public BukkitData.Items.EnderChest deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
|
||||
throws DeserializationException {
|
||||
final ItemStack[] items = getItems(NBT.parseNBT(serialized), dataMcVersion);
|
||||
return items == null ? BukkitData.Items.EnderChest.empty() : BukkitData.Items.EnderChest.adapt(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.Items.EnderChest deserialize(@NotNull String serialized) {
|
||||
return deserialize(serialized, plugin.getMinecraftVersion());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.Items.EnderChest data) throws SerializationException {
|
||||
@@ -102,6 +121,59 @@ public class BukkitSerializer {
|
||||
}
|
||||
}
|
||||
|
||||
// Utility interface for deserializing and upgrading item stacks from legacy versions
|
||||
private interface ItemDeserializer {
|
||||
|
||||
@Nullable
|
||||
default ItemStack[] getItems(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion) {
|
||||
if (mcVersion.compareTo(getPlugin().getMinecraftVersion()) < 0) {
|
||||
return upgradeItemStacks((NBTCompound) tag, mcVersion);
|
||||
}
|
||||
return NBT.itemStackArrayFromNBT(tag);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ItemStack @NotNull [] upgradeItemStacks(@NotNull NBTCompound itemsNbt, @NotNull Version mcVersion) {
|
||||
final ReadWriteNBTCompoundList items = itemsNbt.getCompoundList("items");
|
||||
final ItemStack[] itemStacks = new ItemStack[itemsNbt.getInteger("size")];
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
if (items.get(i) == null) {
|
||||
itemStacks[i] = new ItemStack(Material.AIR);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
itemStacks[i] = NBT.itemStackFromNBT(upgradeItemData(items.get(i), mcVersion));
|
||||
} catch (Throwable e) {
|
||||
itemStacks[i] = new ItemStack(Material.AIR);
|
||||
}
|
||||
}
|
||||
return itemStacks;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ReadWriteNBT upgradeItemData(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion)
|
||||
throws NoSuchFieldException, IllegalAccessException {
|
||||
return DataFixerUtil.fixUpItemData(tag, 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;
|
||||
case "1.21" -> DataFixerUtil.VERSION1_21;
|
||||
default -> DataFixerUtil.getCurrentVersion();
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
}
|
||||
|
||||
public static class PotionEffects extends BukkitSerializer implements Serializer<BukkitData.PotionEffects> {
|
||||
|
||||
private static final TypeToken<List<Data.PotionEffects.Effect>> TYPE = new TypeToken<>() {
|
||||
@@ -149,46 +221,6 @@ public class BukkitSerializer {
|
||||
}
|
||||
}
|
||||
|
||||
public static class Location extends BukkitSerializer implements Serializer<BukkitData.Location> {
|
||||
|
||||
public Location(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.Location deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
return plugin.getDataAdapter().fromJson(serialized, BukkitData.Location.class);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.Location element) throws SerializationException {
|
||||
return plugin.getDataAdapter().toJson(element);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Statistics extends BukkitSerializer implements Serializer<BukkitData.Statistics> {
|
||||
|
||||
public Statistics(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.Statistics deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
return BukkitData.Statistics.from(plugin.getGson().fromJson(
|
||||
serialized,
|
||||
BukkitData.Statistics.StatisticsMap.class
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.Statistics element) throws SerializationException {
|
||||
return plugin.getGson().toJson(element.getStatisticsSet());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class PersistentData extends BukkitSerializer implements Serializer<BukkitData.PersistentData> {
|
||||
|
||||
public PersistentData(@NotNull HuskSync plugin) {
|
||||
@@ -208,56 +240,19 @@ public class BukkitSerializer {
|
||||
|
||||
}
|
||||
|
||||
public static class Health extends Json<BukkitData.Health> implements Serializer<BukkitData.Health> {
|
||||
/**
|
||||
* @deprecated Use {@link Serializer.Json} in the common module instead
|
||||
*/
|
||||
@Deprecated(since = "2.6")
|
||||
public class Json<T extends Data & Adaptable> extends Serializer.Json<T> {
|
||||
|
||||
public Health(@NotNull HuskSync plugin) {
|
||||
super(plugin, BukkitData.Health.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Hunger extends Json<BukkitData.Hunger> implements Serializer<BukkitData.Hunger> {
|
||||
|
||||
public Hunger(@NotNull HuskSync plugin) {
|
||||
super(plugin, BukkitData.Hunger.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Experience extends Json<BukkitData.Experience> implements Serializer<BukkitData.Experience> {
|
||||
|
||||
public Experience(@NotNull HuskSync plugin) {
|
||||
super(plugin, BukkitData.Experience.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class GameMode extends Json<BukkitData.GameMode> implements Serializer<BukkitData.GameMode> {
|
||||
|
||||
public GameMode(@NotNull HuskSync plugin) {
|
||||
super(plugin, BukkitData.GameMode.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static abstract class Json<T extends Data & Adaptable> extends BukkitSerializer implements Serializer<T> {
|
||||
|
||||
private final Class<T> type;
|
||||
|
||||
protected Json(@NotNull HuskSync plugin, Class<T> type) {
|
||||
super(plugin);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
return plugin.getDataAdapter().fromJson(serialized, type);
|
||||
public Json(@NotNull HuskSync plugin, @NotNull Class<T> type) {
|
||||
super(plugin, type);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull T element) throws SerializationException {
|
||||
return plugin.getDataAdapter().toJson(element);
|
||||
public BukkitHuskSync getPlugin() {
|
||||
return (BukkitHuskSync) plugin;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
@@ -42,8 +41,10 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
case "statistics" -> getStatistics();
|
||||
case "health" -> getHealth();
|
||||
case "hunger" -> getHunger();
|
||||
case "attributes" -> getAttributes();
|
||||
case "experience" -> getExperience();
|
||||
case "game_mode" -> getGameMode();
|
||||
case "flight_status" -> getFlightStatus();
|
||||
case "persistent_data" -> getPersistentData();
|
||||
default -> throw new IllegalStateException(String.format("Unexpected data type: %s", id));
|
||||
};
|
||||
@@ -66,9 +67,9 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
.isSyncDeadPlayersChangingServer())) {
|
||||
return Optional.of(BukkitData.Items.Inventory.empty());
|
||||
}
|
||||
final PlayerInventory inventory = getBukkitPlayer().getInventory();
|
||||
final PlayerInventory inventory = getPlayer().getInventory();
|
||||
return Optional.of(BukkitData.Items.Inventory.from(
|
||||
getMapPersister().persistLockedMaps(inventory.getContents(), getBukkitPlayer()),
|
||||
getMapPersister().persistLockedMaps(inventory.getContents(), getPlayer()),
|
||||
inventory.getHeldItemSlot()
|
||||
));
|
||||
}
|
||||
@@ -77,71 +78,89 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
@Override
|
||||
default Optional<Data.Items.EnderChest> getEnderChest() {
|
||||
return Optional.of(BukkitData.Items.EnderChest.adapt(
|
||||
getMapPersister().persistLockedMaps(getBukkitPlayer().getEnderChest().getContents(), getBukkitPlayer())
|
||||
getMapPersister().persistLockedMaps(getPlayer().getEnderChest().getContents(), getPlayer())
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.PotionEffects> getPotionEffects() {
|
||||
return Optional.of(BukkitData.PotionEffects.from(getBukkitPlayer().getActivePotionEffects()));
|
||||
return Optional.of(BukkitData.PotionEffects.from(getPlayer().getActivePotionEffects()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Advancements> getAdvancements() {
|
||||
return Optional.of(BukkitData.Advancements.adapt(getBukkitPlayer()));
|
||||
return Optional.of(BukkitData.Advancements.adapt(getPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Location> getLocation() {
|
||||
return Optional.of(BukkitData.Location.adapt(getBukkitPlayer().getLocation()));
|
||||
return Optional.of(BukkitData.Location.adapt(getPlayer().getLocation()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Statistics> getStatistics() {
|
||||
return Optional.of(BukkitData.Statistics.adapt(getBukkitPlayer()));
|
||||
return Optional.of(BukkitData.Statistics.adapt(getPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Health> getHealth() {
|
||||
return Optional.of(BukkitData.Health.adapt(getBukkitPlayer()));
|
||||
return Optional.of(BukkitData.Health.adapt(getPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Hunger> getHunger() {
|
||||
return Optional.of(BukkitData.Hunger.adapt(getBukkitPlayer()));
|
||||
return Optional.of(BukkitData.Hunger.adapt(getPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Attributes> getAttributes() {
|
||||
return Optional.of(BukkitData.Attributes.adapt(getPlayer(), getPlugin()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Experience> getExperience() {
|
||||
return Optional.of(BukkitData.Experience.adapt(getBukkitPlayer()));
|
||||
return Optional.of(BukkitData.Experience.adapt(getPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.GameMode> getGameMode() {
|
||||
return Optional.of(BukkitData.GameMode.adapt(getBukkitPlayer()));
|
||||
return Optional.of(BukkitData.GameMode.adapt(getPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.FlightStatus> getFlightStatus() {
|
||||
return Optional.of(BukkitData.FlightStatus.adapt(getPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.PersistentData> getPersistentData() {
|
||||
return Optional.of(BukkitData.PersistentData.adapt(getBukkitPlayer().getPersistentDataContainer()));
|
||||
return Optional.of(BukkitData.PersistentData.adapt(getPlayer().getPersistentDataContainer()));
|
||||
}
|
||||
|
||||
boolean isDead();
|
||||
|
||||
@NotNull
|
||||
Player getBukkitPlayer();
|
||||
Player getPlayer();
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getPlayer()} instead
|
||||
*/
|
||||
@Deprecated(since = "3.6")
|
||||
@NotNull
|
||||
Map<Identifier, Data> getCustomDataStore();
|
||||
default Player getBukkitPlayer() {
|
||||
return getPlayer();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default BukkitMapPersister getMapPersister() {
|
||||
|
||||
@@ -20,45 +20,57 @@
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Projectile;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.entity.EntityPickupItemEvent;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.entity.ProjectileLaunchEvent;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||
import org.bukkit.event.inventory.PrepareItemCraftEvent;
|
||||
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
||||
import org.bukkit.event.player.PlayerDropItemEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.event.server.MapInitializeEvent;
|
||||
import org.bukkit.event.world.WorldSaveEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
|
||||
BukkitDeathEventListener, Listener {
|
||||
protected final List<String> blacklistedCommands;
|
||||
|
||||
public BukkitEventListener(@NotNull BukkitHuskSync huskSync) {
|
||||
super(huskSync);
|
||||
this.blacklistedCommands = huskSync.getSettings().getSynchronization().getBlacklistedCommandsWhileLocked();
|
||||
Bukkit.getServer().getPluginManager().registerEvents(this, huskSync);
|
||||
protected LockedHandler lockedHandler;
|
||||
|
||||
public BukkitEventListener(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
public void onLoad() {
|
||||
this.lockedHandler = createLockedHandler((BukkitHuskSync) plugin);
|
||||
}
|
||||
|
||||
public void onEnable() {
|
||||
getPlugin().getServer().getPluginManager().registerEvents(this, getPlugin());
|
||||
lockedHandler.onEnable();
|
||||
}
|
||||
|
||||
public void handlePluginDisable() {
|
||||
super.handlePluginDisable();
|
||||
lockedHandler.onDisable();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private LockedHandler createLockedHandler(@NotNull BukkitHuskSync plugin) {
|
||||
if (!getPlugin().getSettings().isCancelPackets()) {
|
||||
return new BukkitLockedEventListener(plugin);
|
||||
}
|
||||
if (getPlugin().isDependencyLoaded("PacketEvents")) {
|
||||
return new BukkitPacketEventsLockedPacketListener(plugin);
|
||||
} else if (getPlugin().isDependencyLoaded("ProtocolLib")) {
|
||||
return new BukkitProtocolLibLockedPacketListener(plugin);
|
||||
}
|
||||
|
||||
return new BukkitLockedEventListener(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,9 +81,11 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
|
||||
@Override
|
||||
public void handlePlayerQuit(@NotNull BukkitUser bukkitUser) {
|
||||
final Player player = bukkitUser.getPlayer();
|
||||
if (!bukkitUser.isLocked() && !player.getItemOnCursor().getType().isAir()) {
|
||||
player.getWorld().dropItem(player.getLocation(), player.getItemOnCursor());
|
||||
final ItemStack itemOnCursor = player.getItemOnCursor();
|
||||
if (!bukkitUser.isLocked() && !itemOnCursor.getType().isAir()) {
|
||||
player.setItemOnCursor(null);
|
||||
player.getWorld().dropItem(player.getLocation(), itemOnCursor);
|
||||
plugin.debug("Dropped " + itemOnCursor + " for " + player.getName() + " on quit");
|
||||
}
|
||||
super.handlePlayerQuit(bukkitUser);
|
||||
}
|
||||
@@ -86,7 +100,7 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
|
||||
final OnlineUser user = BukkitUser.adapt(event.getEntity(), plugin);
|
||||
|
||||
// If the player is locked or the plugin disabling, clear their drops
|
||||
if (cancelPlayerEvent(user.getUuid())) {
|
||||
if (lockedHandler.cancelPlayerEvent(user.getUuid())) {
|
||||
event.getDrops().clear();
|
||||
return;
|
||||
}
|
||||
@@ -123,88 +137,21 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Events to cancel if the player has not been set yet
|
||||
*/
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onProjectileLaunch(@NotNull ProjectileLaunchEvent event) {
|
||||
final Projectile projectile = event.getEntity();
|
||||
if (projectile.getShooter() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onDropItem(@NotNull PlayerDropItemEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPickupItem(@NotNull EntityPickupItemEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteract(@NotNull PlayerInteractEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteractEntity(@NotNull PlayerInteractEntityEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockPlace(@NotNull BlockPlaceEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockBreak(@NotNull BlockBreakEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
|
||||
if (event.getPlayer() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryClick(@NotNull InventoryClickEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getWhoClicked().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onCraftItem(@NotNull PrepareItemCraftEvent event) {
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
|
||||
}
|
||||
}
|
||||
|
||||
// We handle commands here to allow specific command handling on ProtocolLib servers
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onPermissionCommand(@NotNull PlayerCommandPreprocessEvent event) {
|
||||
final String[] commandArgs = event.getMessage().substring(1).split(" ");
|
||||
final String commandLabel = commandArgs[0].toLowerCase(Locale.ENGLISH);
|
||||
|
||||
if (blacklistedCommands.contains("*") || blacklistedCommands.contains(commandLabel)) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
public void onCommandProcessed(@NotNull PlayerCommandPreprocessEvent event) {
|
||||
if (!lockedHandler.isCommandDisabled(event.getMessage().substring(1).split(" ")[0])) {
|
||||
return;
|
||||
}
|
||||
if (lockedHandler.cancelPlayerEvent(event.getPlayer().getUniqueId())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
public BukkitHuskSync getPlugin() {
|
||||
return (BukkitHuskSync) plugin;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Projectile;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.entity.EntityPickupItemEvent;
|
||||
import org.bukkit.event.entity.ProjectileLaunchEvent;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||
import org.bukkit.event.player.PlayerArmorStandManipulateEvent;
|
||||
import org.bukkit.event.player.PlayerDropItemEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
public class BukkitLockedEventListener implements LockedHandler, Listener {
|
||||
|
||||
protected final BukkitHuskSync plugin;
|
||||
|
||||
protected BukkitLockedEventListener(@NotNull BukkitHuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onProjectileLaunch(@NotNull ProjectileLaunchEvent event) {
|
||||
final Projectile projectile = event.getEntity();
|
||||
if (projectile.getShooter() instanceof Player player) {
|
||||
cancelPlayerEvent(player.getUniqueId(), event);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onDropItem(@NotNull PlayerDropItemEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPickupItem(@NotNull EntityPickupItemEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
cancelPlayerEvent(player.getUniqueId(), event);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteract(@NotNull PlayerInteractEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteractEntity(@NotNull PlayerInteractEntityEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteractArmorStand(@NotNull PlayerArmorStandManipulateEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockPlace(@NotNull BlockPlaceEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockBreak(@NotNull BlockBreakEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
|
||||
if (event.getPlayer() instanceof Player player) {
|
||||
cancelPlayerEvent(player.getUniqueId(), event);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryClick(@NotNull InventoryClickEvent event) {
|
||||
cancelPlayerEvent(event.getWhoClicked().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
cancelPlayerEvent(player.getUniqueId(), event);
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelPlayerEvent(@NotNull UUID uuid, @NotNull Cancellable event) {
|
||||
if (cancelPlayerEvent(uuid)) {
|
||||
event.setCancelled(true);
|
||||
plugin.debug("Cancelled event " + event.getClass().getSimpleName() + " from " + Objects.requireNonNull(plugin.getServer().getPlayer(uuid)).getName());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import com.github.retrooper.packetevents.PacketEvents;
|
||||
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
|
||||
import com.github.retrooper.packetevents.event.PacketListenerPriority;
|
||||
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
|
||||
import com.github.retrooper.packetevents.event.PacketSendEvent;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.google.common.collect.Sets;
|
||||
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
|
||||
|
||||
protected BukkitPacketEventsLockedPacketListener(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
super.onLoad();
|
||||
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(getPlugin()));
|
||||
PacketEvents.getAPI().getSettings().reEncodeByDefault(false)
|
||||
.checkForUpdates(false)
|
||||
.bStats(true);
|
||||
PacketEvents.getAPI().load();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
super.onEnable();
|
||||
PacketEvents.getAPI().getEventManager().registerListener(new PlayerPacketAdapter(this));
|
||||
PacketEvents.getAPI().init();
|
||||
plugin.log(Level.INFO, "Using PacketEvents to cancel packets for locked players");
|
||||
}
|
||||
|
||||
private static class PlayerPacketAdapter extends PacketListenerAbstract {
|
||||
|
||||
private static final Set<PacketType.Play.Client> ALLOWED_PACKETS = Set.of(
|
||||
PacketType.Play.Client.KEEP_ALIVE, PacketType.Play.Client.PONG, PacketType.Play.Client.PLUGIN_MESSAGE, // Connection packets
|
||||
PacketType.Play.Client.CHAT_MESSAGE, PacketType.Play.Client.CHAT_COMMAND, PacketType.Play.Client.CHAT_SESSION_UPDATE, // Chat / command packets
|
||||
PacketType.Play.Client.PLAYER_POSITION, PacketType.Play.Client.PLAYER_POSITION_AND_ROTATION, PacketType.Play.Client.PLAYER_ROTATION, // Movement packets
|
||||
PacketType.Play.Client.HELD_ITEM_CHANGE, PacketType.Play.Client.ANIMATION, PacketType.Play.Client.TELEPORT_CONFIRM, // Animation packets
|
||||
PacketType.Play.Client.CLIENT_SETTINGS // Video setting packets
|
||||
);
|
||||
|
||||
private static final Set<PacketType.Play.Client> CANCEL_PACKETS = getPacketsToListenFor();
|
||||
|
||||
|
||||
private final BukkitPacketEventsLockedPacketListener listener;
|
||||
|
||||
public PlayerPacketAdapter(@NotNull BukkitPacketEventsLockedPacketListener listener) {
|
||||
super(PacketListenerPriority.HIGH);
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
|
||||
return;
|
||||
}
|
||||
if (!CANCEL_PACKETS.contains(client)) {
|
||||
return;
|
||||
}
|
||||
if (listener.cancelPlayerEvent(event.getUser().getUUID())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketSend(PacketSendEvent event) {
|
||||
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
|
||||
return;
|
||||
}
|
||||
if (!CANCEL_PACKETS.contains(client)) {
|
||||
return;
|
||||
}
|
||||
if (listener.cancelPlayerEvent(event.getUser().getUUID())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the set of ALL Server packets, excluding the set of allowed packets
|
||||
@NotNull
|
||||
private static Set<PacketType.Play.Client> getPacketsToListenFor() {
|
||||
return Sets.difference(
|
||||
Sets.newHashSet(PacketType.Play.Client.values()),
|
||||
ALLOWED_PACKETS
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import com.comphenix.protocol.PacketType;
|
||||
import com.comphenix.protocol.ProtocolLibrary;
|
||||
import com.comphenix.protocol.events.ListenerPriority;
|
||||
import com.comphenix.protocol.events.PacketAdapter;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.google.common.collect.Sets;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.comphenix.protocol.PacketType.Play.Client;
|
||||
|
||||
public class BukkitProtocolLibLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
|
||||
|
||||
protected BukkitProtocolLibLockedPacketListener(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
super.onEnable();
|
||||
ProtocolLibrary.getProtocolManager().addPacketListener(new PlayerPacketAdapter(this));
|
||||
plugin.log(Level.INFO, "Using ProtocolLib to cancel packets for locked players");
|
||||
}
|
||||
|
||||
private static class PlayerPacketAdapter extends PacketAdapter {
|
||||
|
||||
// Packets we want the player to still be able to send/receiver to/from the server
|
||||
private static final Set<PacketType> ALLOWED_PACKETS = Set.of(
|
||||
Client.KEEP_ALIVE, Client.PONG, Client.CUSTOM_PAYLOAD, // Connection packets
|
||||
Client.CHAT_COMMAND, Client.CLIENT_COMMAND, Client.CHAT, Client.CHAT_SESSION_UPDATE, // Chat / command packets
|
||||
Client.POSITION, Client.POSITION_LOOK, Client.LOOK, // Movement packets
|
||||
Client.HELD_ITEM_SLOT, Client.ARM_ANIMATION, Client.TELEPORT_ACCEPT, // Animation packets
|
||||
Client.SETTINGS // Video setting packets
|
||||
);
|
||||
|
||||
private final BukkitProtocolLibLockedPacketListener listener;
|
||||
|
||||
public PlayerPacketAdapter(@NotNull BukkitProtocolLibLockedPacketListener listener) {
|
||||
super(listener.getPlugin(), ListenerPriority.HIGHEST, getPacketsToListenFor());
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketReceiving(@NotNull PacketEvent event) {
|
||||
if (listener.cancelPlayerEvent(event.getPlayer().getUniqueId()) && !event.isReadOnly()) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketSending(@NotNull PacketEvent event) {
|
||||
if (listener.cancelPlayerEvent(event.getPlayer().getUniqueId()) && !event.isReadOnly()) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the set of ALL Server packets, excluding the set of allowed packets
|
||||
@NotNull
|
||||
private static Set<PacketType> getPacketsToListenFor() {
|
||||
return Sets.difference(
|
||||
Client.getInstance().values().stream().filter(PacketType::isSupported).collect(Collectors.toSet()),
|
||||
ALLOWED_PACKETS
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -323,18 +323,18 @@ public class LegacyMigrator extends Migrator {
|
||||
|
||||
// Stats
|
||||
.statistics(BukkitData.Statistics.from(
|
||||
BukkitData.Statistics.createStatisticsMap(
|
||||
convertStatisticMap(stats.untypedStatisticValues()),
|
||||
convertMaterialStatisticMap(stats.blockStatisticValues()),
|
||||
convertMaterialStatisticMap(stats.itemStatisticValues()),
|
||||
convertEntityStatisticMap(stats.entityStatisticValues())
|
||||
)))
|
||||
))
|
||||
|
||||
// Health, hunger, experience & game mode
|
||||
.health(BukkitData.Health.from(health, maxHealth, healthScale))
|
||||
.health(BukkitData.Health.from(health, healthScale, false))
|
||||
.hunger(BukkitData.Hunger.from(hunger, saturation, saturationExhaustion))
|
||||
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
|
||||
.gameMode(BukkitData.GameMode.from(gameMode, isFlying, isFlying))
|
||||
.gameMode(BukkitData.GameMode.from(gameMode))
|
||||
.flightStatus(BukkitData.FlightStatus.from(isFlying, isFlying))
|
||||
|
||||
// Build & pack into new format
|
||||
.saveCause(DataSnapshot.SaveCause.LEGACY_MIGRATION).buildAndPack();
|
||||
|
||||
@@ -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!
|
||||
@@ -320,7 +320,7 @@ public class MpdbMigrator extends Migrator {
|
||||
.inventory(BukkitData.Items.Inventory.from(inventory.getContents(), 0))
|
||||
.enderChest(BukkitData.Items.EnderChest.adapt(enderChest))
|
||||
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
|
||||
.gameMode(BukkitData.GameMode.from("SURVIVAL", false, false))
|
||||
.gameMode(BukkitData.GameMode.from("SURVIVAL"))
|
||||
.saveCause(DataSnapshot.SaveCause.MPDB_MIGRATION)
|
||||
.buildAndPack();
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
@@ -62,37 +56,18 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
||||
return new BukkitUser(player, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Bukkit {@link Player} instance of this user
|
||||
*
|
||||
* @return the {@link Player} instance
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOffline() {
|
||||
return 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
|
||||
@@ -109,7 +84,7 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
||||
gui.setCloseGuiAction((close) -> onClose.accept(BukkitData.Items.ItemArray.adapt(
|
||||
Arrays.stream(close.getInventory().getContents()).limit(size).toArray(ItemStack[]::new)
|
||||
)));
|
||||
plugin.runSync(() -> gui.open(player));
|
||||
plugin.runSync(() -> gui.open(player), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -132,9 +107,14 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
||||
return player.hasMetadata("NPC");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Bukkit {@link Player} instance of this user
|
||||
*
|
||||
* @return the {@link Player} instance
|
||||
* @since 3.6
|
||||
*/
|
||||
@NotNull
|
||||
@Override
|
||||
public Player getBukkitPlayer() {
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,56 +19,44 @@
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import org.bukkit.Keyed;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
// Utility class for adapting "Keyed" Bukkit objects
|
||||
public final class BukkitKeyedAdapter {
|
||||
|
||||
@Nullable
|
||||
public static Statistic matchStatistic(@NotNull String key) {
|
||||
try {
|
||||
return Arrays.stream(Statistic.values())
|
||||
.filter(stat -> stat.getKey().toString().equals(key))
|
||||
.findFirst().orElse(null);
|
||||
} catch (Throwable e) {
|
||||
return null;
|
||||
}
|
||||
return getRegistryValue(Registry.STATISTIC, key);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static EntityType matchEntityType(@NotNull String key) {
|
||||
try {
|
||||
return Arrays.stream(EntityType.values())
|
||||
.filter(entityType -> entityType.getKey().toString().equals(key))
|
||||
.findFirst().orElse(null);
|
||||
} catch (Throwable e) {
|
||||
return null;
|
||||
}
|
||||
return getRegistryValue(Registry.ENTITY_TYPE, key);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Material matchMaterial(@NotNull String key) {
|
||||
try {
|
||||
return Material.matchMaterial(key);
|
||||
} catch (Throwable e) {
|
||||
return null;
|
||||
}
|
||||
return getRegistryValue(Registry.MATERIAL, key);
|
||||
}
|
||||
|
||||
public static Optional<String> getKeyName(@NotNull Keyed keyed) {
|
||||
try {
|
||||
return Optional.of(keyed.getKey().toString());
|
||||
} catch (Throwable e) {
|
||||
return Optional.empty();
|
||||
@Nullable
|
||||
public static Attribute matchAttribute(@NotNull String key) {
|
||||
return getRegistryValue(Registry.ATTRIBUTE, key);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static PotionEffectType matchEffectType(@NotNull String key) {
|
||||
return getRegistryValue(Registry.EFFECT, key);
|
||||
}
|
||||
|
||||
private static <T extends Keyed> T getRegistryValue(@NotNull Registry<T> registry, @NotNull String keyString) {
|
||||
final NamespacedKey key = NamespacedKey.fromString(keyString);
|
||||
return key != null ? registry.get(key) : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,9 +27,6 @@ import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -45,7 +42,8 @@ import java.time.OffsetDateTime;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static net.william278.husksync.util.BukkitKeyedAdapter.*;
|
||||
import static net.william278.husksync.util.BukkitKeyedAdapter.matchEntityType;
|
||||
import static net.william278.husksync.util.BukkitKeyedAdapter.matchMaterial;
|
||||
|
||||
public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
@@ -53,9 +51,9 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public DataSnapshot.Packed convert(byte[] data, @NotNull UUID id,
|
||||
@NotNull
|
||||
public DataSnapshot.Packed convert(byte @NotNull [] data, @NotNull UUID id,
|
||||
@NotNull OffsetDateTime timestamp) throws DataAdapter.AdaptionException {
|
||||
final JSONObject object = new JSONObject(plugin.getDataAdapter().bytesToString(data));
|
||||
final int version = object.getInt("format_version");
|
||||
@@ -84,30 +82,34 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
final JSONObject status = object.getJSONObject("status_data");
|
||||
final HashMap<Identifier, Data> containers = Maps.newHashMap();
|
||||
if (shouldImport(Identifier.HEALTH)) {
|
||||
if (Identifier.HEALTH.isEnabled()) {
|
||||
containers.put(Identifier.HEALTH, BukkitData.Health.from(
|
||||
status.getDouble("health"),
|
||||
status.getDouble("max_health"),
|
||||
status.getDouble("health_scale")
|
||||
status.getDouble("health_scale"),
|
||||
false
|
||||
));
|
||||
}
|
||||
if (shouldImport(Identifier.HUNGER)) {
|
||||
if (Identifier.HUNGER.isEnabled()) {
|
||||
containers.put(Identifier.HUNGER, BukkitData.Hunger.from(
|
||||
status.getInt("hunger"),
|
||||
status.getFloat("saturation"),
|
||||
status.getFloat("saturation_exhaustion")
|
||||
));
|
||||
}
|
||||
if (shouldImport(Identifier.EXPERIENCE)) {
|
||||
if (Identifier.EXPERIENCE.isEnabled()) {
|
||||
containers.put(Identifier.EXPERIENCE, BukkitData.Experience.from(
|
||||
status.getInt("total_experience"),
|
||||
status.getInt("experience_level"),
|
||||
status.getFloat("experience_progress")
|
||||
));
|
||||
}
|
||||
if (shouldImport(Identifier.GAME_MODE)) {
|
||||
if (Identifier.GAME_MODE.isEnabled()) {
|
||||
containers.put(Identifier.GAME_MODE, BukkitData.GameMode.from(
|
||||
status.getString("game_mode"),
|
||||
status.getString("game_mode")
|
||||
));
|
||||
}
|
||||
if (Identifier.FLIGHT_STATUS.isEnabled()) {
|
||||
containers.put(Identifier.FLIGHT_STATUS, BukkitData.FlightStatus.from(
|
||||
status.getBoolean("is_flying"),
|
||||
status.getBoolean("is_flying")
|
||||
));
|
||||
@@ -117,7 +119,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Items.Inventory> readInventory(@NotNull JSONObject object) {
|
||||
if (!object.has("inventory") || !shouldImport(Identifier.INVENTORY)) {
|
||||
if (!object.has("inventory") || !Identifier.INVENTORY.isEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -129,7 +131,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Items.EnderChest> readEnderChest(@NotNull JSONObject object) {
|
||||
if (!object.has("ender_chest") || !shouldImport(Identifier.ENDER_CHEST)) {
|
||||
if (!object.has("ender_chest") || !Identifier.ENDER_CHEST.isEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -141,7 +143,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Location> readLocation(@NotNull JSONObject object) {
|
||||
if (!object.has("location") || !shouldImport(Identifier.LOCATION)) {
|
||||
if (!object.has("location") || !Identifier.LOCATION.isEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -162,7 +164,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Advancements> readAdvancements(@NotNull JSONObject object) {
|
||||
if (!object.has("advancements") || !shouldImport(Identifier.ADVANCEMENTS)) {
|
||||
if (!object.has("advancements") || !Identifier.ADVANCEMENTS.isEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -185,7 +187,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Statistics> readStatistics(@NotNull JSONObject object) {
|
||||
if (!object.has("statistics") || !shouldImport(Identifier.ADVANCEMENTS)) {
|
||||
if (!object.has("statistics") || !Identifier.STATISTICS.isEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -202,39 +204,42 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
private BukkitData.Statistics readStatisticMaps(@NotNull JSONObject untyped, @NotNull JSONObject blocks,
|
||||
@NotNull JSONObject items, @NotNull JSONObject entities) {
|
||||
// Read generic stats
|
||||
final Map<Statistic, Integer> genericStats = Maps.newHashMap();
|
||||
untyped.keys().forEachRemaining(stat -> genericStats.put(matchStatistic(stat), untyped.getInt(stat)));
|
||||
final Map<String, Integer> genericStats = Maps.newHashMap();
|
||||
untyped.keys().forEachRemaining(stat -> genericStats.put(stat, untyped.getInt(stat)));
|
||||
|
||||
// Read block & item stats
|
||||
final Map<Statistic, Map<Material, Integer>> blockStats, itemStats;
|
||||
final Map<String, Map<String, Integer>> blockStats, itemStats, entityStats;
|
||||
blockStats = readMaterialStatistics(blocks);
|
||||
itemStats = readMaterialStatistics(items);
|
||||
|
||||
// Read entity stats
|
||||
final Map<Statistic, Map<EntityType, Integer>> entityStats = Maps.newHashMap();
|
||||
entityStats = Maps.newHashMap();
|
||||
entities.keys().forEachRemaining(stat -> {
|
||||
final JSONObject entityStat = entities.getJSONObject(stat);
|
||||
final Map<EntityType, Integer> entityMap = Maps.newHashMap();
|
||||
entityStat.keys().forEachRemaining(entity -> entityMap.put(matchEntityType(entity), entityStat.getInt(entity)));
|
||||
entityStats.put(matchStatistic(stat), entityMap);
|
||||
final Map<String, Integer> entityMap = Maps.newHashMap();
|
||||
entityStat.keys().forEachRemaining(entity -> {
|
||||
if (matchEntityType(entity) != null) {
|
||||
entityMap.put(entity, entityStat.getInt(entity));
|
||||
}
|
||||
});
|
||||
entityStats.put(stat, entityMap);
|
||||
});
|
||||
|
||||
return BukkitData.Statistics.from(genericStats, blockStats, itemStats, entityStats);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map<Statistic, Map<Material, Integer>> readMaterialStatistics(@NotNull JSONObject items) {
|
||||
final Map<Statistic, Map<Material, Integer>> itemStats = Maps.newHashMap();
|
||||
private Map<String, Map<String, Integer>> readMaterialStatistics(@NotNull JSONObject items) {
|
||||
final Map<String, Map<String, Integer>> itemStats = Maps.newHashMap();
|
||||
items.keys().forEachRemaining(stat -> {
|
||||
final JSONObject itemStat = items.getJSONObject(stat);
|
||||
final Map<Material, Integer> itemMap = Maps.newHashMap();
|
||||
final Map<String, Integer> itemMap = Maps.newHashMap();
|
||||
itemStat.keys().forEachRemaining(item -> {
|
||||
final Material material = matchMaterial(item);
|
||||
if (material != null) {
|
||||
itemMap.put(material, itemStat.getInt(item));
|
||||
if (matchMaterial(item) != null) {
|
||||
itemMap.put(item, itemStat.getInt(item));
|
||||
}
|
||||
});
|
||||
itemStats.put(matchStatistic(stat), itemMap);
|
||||
itemStats.put(stat, itemMap);
|
||||
});
|
||||
return itemStats;
|
||||
}
|
||||
@@ -276,11 +281,6 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
|
||||
}
|
||||
|
||||
|
||||
private boolean shouldImport(@NotNull Identifier type) {
|
||||
return plugin.getSettings().getSynchronization().isFeatureEnabled(type);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Date parseDate(@NotNull String dateString) {
|
||||
try {
|
||||
|
||||
@@ -25,18 +25,21 @@ 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.HuskSync;
|
||||
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.Container;
|
||||
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;
|
||||
@@ -75,8 +78,8 @@ public interface BukkitMapPersister {
|
||||
* @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
|
||||
*/
|
||||
@NotNull
|
||||
default ItemStack[] setMapViews(@NotNull ItemStack[] items) {
|
||||
@Nullable
|
||||
default ItemStack @NotNull [] setMapViews(@Nullable ItemStack @NotNull [] items) {
|
||||
if (!getPlugin().getSettings().getSynchronization().isPersistLockedMaps()) {
|
||||
return items;
|
||||
}
|
||||
@@ -85,7 +88,7 @@ public interface BukkitMapPersister {
|
||||
|
||||
// Perform an operation on each map in an array of ItemStacks
|
||||
@NotNull
|
||||
private ItemStack[] forEachMap(@NotNull ItemStack[] items, @NotNull Function<ItemStack, ItemStack> function) {
|
||||
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) {
|
||||
@@ -93,6 +96,9 @@ public interface BukkitMapPersister {
|
||||
}
|
||||
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) {
|
||||
forEachMap(box.getInventory().getContents(), function);
|
||||
b.setBlockState(box);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
@@ -148,7 +154,7 @@ public interface BukkitMapPersister {
|
||||
// Search for an existing map view
|
||||
Optional<String> world = Optional.empty();
|
||||
for (String worldUid : mapIds.getKeys()) {
|
||||
world = Bukkit.getWorlds().stream()
|
||||
world = getPlugin().getServer().getWorlds().stream()
|
||||
.map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid))
|
||||
.findFirst();
|
||||
if (world.isPresent()) {
|
||||
@@ -270,7 +276,7 @@ public interface BukkitMapPersister {
|
||||
|
||||
@NotNull
|
||||
private static World getDefaultMapWorld() {
|
||||
final World world = Bukkit.getWorlds().get(0);
|
||||
final World world = Bukkit.getWorlds().getFirst();
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("No worlds are loaded on the server!");
|
||||
}
|
||||
@@ -302,7 +308,7 @@ public interface BukkitMapPersister {
|
||||
// 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));
|
||||
canvas.setPixelColor(j, i, canvasData.getMapColorAt(i, j));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,20 +383,40 @@ public interface BukkitMapPersister {
|
||||
}
|
||||
|
||||
@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 getPixel(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPixelColor(int i, int i1, @Nullable Color color) {
|
||||
pixels[i][i1] = color == null ? 0 : color.getRGB();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Color getPixelColor(int x, int y) {
|
||||
return getBasePixelColor(x, y);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Color getBasePixelColor(int x, int y) {
|
||||
return new Color(pixels[x][y]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawImage(int x, int y, @NotNull Image image) {
|
||||
// Not implemented
|
||||
@@ -421,7 +447,7 @@ public interface BukkitMapPersister {
|
||||
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);
|
||||
final String type = cursor.getType().getKey().getKey();
|
||||
if (type.startsWith(BANNER_PREFIX)) {
|
||||
banners.add(new MapBanner(
|
||||
type.replaceAll(BANNER_PREFIX, ""),
|
||||
@@ -431,6 +457,7 @@ public interface BukkitMapPersister {
|
||||
cursor.getY()
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
return MapData.fromPixels(pixels, getDimension(), (byte) 2, banners, List.of());
|
||||
}
|
||||
@@ -441,6 +468,6 @@ public interface BukkitMapPersister {
|
||||
|
||||
@ApiStatus.Internal
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
BukkitHuskSync getPlugin();
|
||||
|
||||
}
|
||||
|
||||
@@ -21,8 +21,11 @@ package net.william278.husksync.util;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.UserDataHolder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
|
||||
import space.arim.morepaperlib.scheduling.AttachedScheduler;
|
||||
import space.arim.morepaperlib.scheduling.RegionalScheduler;
|
||||
import space.arim.morepaperlib.scheduling.ScheduledTask;
|
||||
|
||||
@@ -34,9 +37,12 @@ public interface BukkitTask extends Task {
|
||||
class Sync extends Task.Sync implements BukkitTask {
|
||||
|
||||
private ScheduledTask task;
|
||||
private final @Nullable UserDataHolder user;
|
||||
|
||||
protected Sync(@NotNull HuskSync plugin, @NotNull Runnable runnable, long delayTicks) {
|
||||
protected Sync(@NotNull HuskSync plugin, @NotNull Runnable runnable,
|
||||
@Nullable UserDataHolder user, long delayTicks) {
|
||||
super(plugin, runnable, delayTicks);
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -57,7 +63,19 @@ public interface BukkitTask extends Task {
|
||||
return;
|
||||
}
|
||||
|
||||
final RegionalScheduler scheduler = ((BukkitHuskSync) getPlugin()).getRegionalScheduler();
|
||||
// Use entity-specific scheduler if user is not null
|
||||
if (user != null) {
|
||||
final AttachedScheduler scheduler = ((BukkitHuskSync) getPlugin()).getUserSyncScheduler(user);
|
||||
if (delayTicks > 0) {
|
||||
this.task = scheduler.runDelayed(runnable, null, delayTicks);
|
||||
} else {
|
||||
this.task = scheduler.run(runnable, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Or default to the global scheduler
|
||||
final RegionalScheduler scheduler = ((BukkitHuskSync) getPlugin()).getSyncScheduler();
|
||||
if (delayTicks > 0) {
|
||||
this.task = scheduler.runDelayed(runnable, delayTicks);
|
||||
} else {
|
||||
@@ -146,8 +164,8 @@ public interface BukkitTask extends Task {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Task.Sync getSyncTask(@NotNull Runnable runnable, long delayTicks) {
|
||||
return new Sync(getPlugin(), runnable, delayTicks);
|
||||
default Task.Sync getSyncTask(@NotNull Runnable runnable, @Nullable UserDataHolder user, long delayTicks) {
|
||||
return new Sync(getPlugin(), runnable, user, delayTicks);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
inventory {
|
||||
name brigadier:string single_word;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
husksync {
|
||||
update;
|
||||
about;
|
||||
status;
|
||||
reload;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
enderchest {
|
||||
name brigadier:string single_word;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
userdata {
|
||||
view {
|
||||
name brigadier:string single_word {
|
||||
version brigadier:string single_word;
|
||||
}
|
||||
}
|
||||
list {
|
||||
name brigadier:string single_word {
|
||||
page brigadier:integer;
|
||||
}
|
||||
}
|
||||
delete {
|
||||
name brigadier:string single_word {
|
||||
version brigadier:string single_word;
|
||||
}
|
||||
}
|
||||
restore {
|
||||
name brigadier:string single_word {
|
||||
version brigadier:string single_word;
|
||||
}
|
||||
}
|
||||
pin {
|
||||
name brigadier:string single_word {
|
||||
version brigadier:string single_word;
|
||||
}
|
||||
}
|
||||
dump {
|
||||
name brigadier:string single_word {
|
||||
version brigadier:string single_word {
|
||||
web;
|
||||
file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,16 @@ api-version: 1.17
|
||||
author: 'William278'
|
||||
description: '${description}'
|
||||
website: 'https://william278.net'
|
||||
folia-supported: true
|
||||
softdepend:
|
||||
- 'packetevents'
|
||||
- 'ProtocolLib'
|
||||
- 'MysqlPlayerDataBridge'
|
||||
- 'Plan'
|
||||
libraries:
|
||||
- 'redis.clients:jedis:${jedis_version}'
|
||||
- 'com.mysql:mysql-connector-j:${mysql_driver_version}'
|
||||
- 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}'
|
||||
- 'org.postgresql:postgresql:${postgres_driver_version}'
|
||||
- 'org.mongodb:mongodb-driver-sync:${mongodb_driver_version}'
|
||||
- 'org.xerial.snappy:snappy-java:${snappy_version}'
|
||||
@@ -3,36 +3,40 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'commons-io:commons-io:2.15.1'
|
||||
api 'org.apache.commons:commons-text:1.11.0'
|
||||
api 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
|
||||
api 'org.json:json:20231013'
|
||||
api 'com.google.code.gson:gson:2.10.1'
|
||||
api 'commons-io:commons-io:2.17.0'
|
||||
api 'org.apache.commons:commons-text:1.12.0'
|
||||
api 'net.william278:minedown:1.8.2'
|
||||
api 'org.json:json:20240303'
|
||||
api 'com.google.code.gson:gson:2.11.0'
|
||||
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
|
||||
api 'com.github.Exlll.ConfigLib:configlib-yaml:v4.4.0'
|
||||
api 'de.exlll:configlib-yaml:4.5.0'
|
||||
api 'net.william278:paginedown:1.1.2'
|
||||
api 'net.william278:DesertWell:2.0.4'
|
||||
api 'net.william278:PagineDown:1.1'
|
||||
api('com.zaxxer:HikariCP:5.1.0') {
|
||||
api('com.zaxxer:HikariCP:6.0.0') {
|
||||
exclude module: 'slf4j-api'
|
||||
}
|
||||
|
||||
compileOnly 'org.projectlombok:lombok:1.18.30'
|
||||
compileOnly 'net.william278.uniform:uniform-common:1.2.1'
|
||||
compileOnly 'com.mojang:brigadier:1.1.8'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.34'
|
||||
compileOnly 'org.jetbrains:annotations:24.1.0'
|
||||
compileOnly 'net.kyori:adventure-api:4.15.0'
|
||||
compileOnly 'net.kyori:adventure-platform-api:4.3.2'
|
||||
compileOnly 'com.google.guava:guava:33.0.0-jre'
|
||||
compileOnly 'net.kyori:adventure-api:4.17.0'
|
||||
compileOnly 'net.kyori:adventure-platform-api:4.3.4'
|
||||
compileOnly 'com.google.guava:guava:33.3.1-jre'
|
||||
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
compileOnly "redis.clients:jedis:$jedis_version"
|
||||
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
|
||||
compileOnly "org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version"
|
||||
compileOnly "org.postgresql:postgresql:$postgres_driver_version"
|
||||
compileOnly "org.mongodb:mongodb-driver-sync:$mongodb_driver_version"
|
||||
compileOnly "org.xerial.snappy:snappy-java:$snappy_version"
|
||||
|
||||
testImplementation "redis.clients:jedis:$jedis_version"
|
||||
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
|
||||
testImplementation 'com.google.guava:guava:33.0.0-jre'
|
||||
testImplementation 'com.google.guava:guava:33.3.1-jre'
|
||||
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
testCompileOnly 'com.github.Exlll.ConfigLib:configlib-yaml:v4.4.0'
|
||||
testCompileOnly 'de.exlll:configlib-yaml:4.5.0'
|
||||
testCompileOnly 'org.jetbrains:annotations:24.1.0'
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.30'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.34'
|
||||
}
|
||||
@@ -29,12 +29,9 @@ import net.william278.desertwell.util.UpdateChecker;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.config.ConfigProvider;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.config.Server;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.Serializer;
|
||||
import net.william278.husksync.data.SerializerRegistry;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.event.EventDispatcher;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
@@ -42,11 +39,12 @@ 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.CompatibilityChecker;
|
||||
import net.william278.husksync.util.LegacyConverter;
|
||||
import net.william278.husksync.util.Task;
|
||||
import net.william278.uniform.Uniform;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
@@ -55,7 +53,8 @@ import java.util.logging.Level;
|
||||
/**
|
||||
* Abstract implementation of the HuskSync plugin.
|
||||
*/
|
||||
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider {
|
||||
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider, SerializerRegistry,
|
||||
CompatibilityChecker {
|
||||
|
||||
int SPIGOT_RESOURCE_ID = 97144;
|
||||
|
||||
@@ -89,7 +88,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
*
|
||||
* @return the {@link RedisManager} implementation
|
||||
*/
|
||||
|
||||
@NotNull
|
||||
RedisManager getRedisManager();
|
||||
|
||||
@@ -101,43 +99,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
@NotNull
|
||||
DataAdapter getDataAdapter();
|
||||
|
||||
/**
|
||||
* Returns the data serializer for the given {@link Identifier}
|
||||
*/
|
||||
@NotNull
|
||||
<T extends Data> Map<Identifier, Serializer<T>> getSerializers();
|
||||
|
||||
/**
|
||||
* Register a data serializer for the given {@link Identifier}
|
||||
*
|
||||
* @param identifier the {@link Identifier}
|
||||
* @param serializer the {@link Serializer}
|
||||
*/
|
||||
default void registerSerializer(@NotNull Identifier identifier,
|
||||
@NotNull Serializer<? extends Data> serializer) {
|
||||
if (identifier.isCustom()) {
|
||||
log(Level.INFO, String.format("Registered custom data type: %s", identifier));
|
||||
}
|
||||
getSerializers().put(identifier, (Serializer<Data>) serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Identifier} for the given key
|
||||
*/
|
||||
default Optional<Identifier> getIdentifier(@NotNull String key) {
|
||||
return getSerializers().keySet().stream().filter(identifier -> identifier.toString().equals(key)).findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the set of registered data types
|
||||
*
|
||||
* @return the set of registered data types
|
||||
*/
|
||||
@NotNull
|
||||
default Set<Identifier> getRegisteredDataTypes() {
|
||||
return getSerializers().keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data syncer implementation
|
||||
*
|
||||
@@ -153,6 +114,14 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
*/
|
||||
void setDataSyncer(@NotNull DataSyncer dataSyncer);
|
||||
|
||||
/**
|
||||
* Get the uniform command provider
|
||||
*
|
||||
* @return the command provider
|
||||
*/
|
||||
@NotNull
|
||||
Uniform getUniform();
|
||||
|
||||
/**
|
||||
* Returns a list of available data {@link Migrator}s
|
||||
*
|
||||
@@ -162,7 +131,17 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
List<Migrator> getAvailableMigrators();
|
||||
|
||||
@NotNull
|
||||
Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user);
|
||||
Map<UUID, Map<Identifier, Data>> getPlayerCustomDataStore();
|
||||
|
||||
@NotNull
|
||||
default Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
|
||||
if (getPlayerCustomDataStore().containsKey(user.getUuid())) {
|
||||
return getPlayerCustomDataStore().get(user.getUuid());
|
||||
}
|
||||
final Map<Identifier, Data> data = new HashMap<>();
|
||||
getPlayerCustomDataStore().put(user.getUuid(), data);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a faucet of the plugin.
|
||||
@@ -180,31 +159,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
log(Level.INFO, "Successfully initialized " + name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plugin {@link Settings}
|
||||
*
|
||||
* @return the {@link Settings}
|
||||
*/
|
||||
@NotNull
|
||||
Settings getSettings();
|
||||
|
||||
void setSettings(@NotNull Settings settings);
|
||||
|
||||
@NotNull
|
||||
String getServerName();
|
||||
|
||||
void setServerName(@NotNull Server serverName);
|
||||
|
||||
/**
|
||||
* Returns the plugin {@link Locales}
|
||||
*
|
||||
* @return the {@link Locales}
|
||||
*/
|
||||
@NotNull
|
||||
Locales getLocales();
|
||||
|
||||
void setLocales(@NotNull Locales locales);
|
||||
|
||||
/**
|
||||
* Returns if a dependency is loaded
|
||||
*
|
||||
@@ -221,14 +175,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
*/
|
||||
InputStream getResource(@NotNull String name);
|
||||
|
||||
/**
|
||||
* Returns the plugin data folder
|
||||
*
|
||||
* @return the plugin data folder as a {@link File}
|
||||
*/
|
||||
@NotNull
|
||||
File getDataFolder();
|
||||
|
||||
/**
|
||||
* Log a message to the console
|
||||
*
|
||||
@@ -311,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
|
||||
*
|
||||
@@ -379,14 +333,18 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
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 or MariaDB database details correctly in config.yml
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ public class GsonAdapter implements DataAdapter {
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public <A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException {
|
||||
public <A extends Adaptable> A fromBytes(byte[] data, @NotNull Class<A> type) throws AdaptionException {
|
||||
return this.fromJson(new String(data, StandardCharsets.UTF_8), type);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ public class SnappyGsonAdapter extends GsonAdapter {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public <A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException {
|
||||
try {
|
||||
@@ -43,7 +42,7 @@ public class SnappyGsonAdapter extends GsonAdapter {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public <A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException {
|
||||
public <A extends Adaptable> A fromBytes(byte[] data, @NotNull Class<A> type) throws AdaptionException {
|
||||
try {
|
||||
return super.fromBytes(decompressBytes(data), type);
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -31,11 +31,13 @@ import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* The common implementation of the HuskSync API, containing cross-platform API calls.
|
||||
@@ -172,6 +174,7 @@ public class HuskSyncAPI {
|
||||
public void editCurrentData(@NotNull User user, @NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
getCurrentData(user).thenAccept(optional -> optional.ifPresent(data -> {
|
||||
editor.accept(data);
|
||||
data.setId(UUID.randomUUID());
|
||||
setCurrentData(user, data);
|
||||
}));
|
||||
}
|
||||
@@ -262,13 +265,32 @@ public class HuskSyncAPI {
|
||||
*
|
||||
* @param user The user to save the data for
|
||||
* @param snapshot The snapshot to save
|
||||
* @param callback A callback to run after the data has been saved (if the DataSaveEvent was not canceled)
|
||||
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will be fired unless the
|
||||
* {@link DataSnapshot.SaveCause#fireDataSaveEvent()} is {@code false}
|
||||
* @since 3.3.2
|
||||
*/
|
||||
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot,
|
||||
@Nullable BiConsumer<User, DataSnapshot.Packed> callback) {
|
||||
plugin.runAsync(() -> plugin.getDataSyncer().saveData(
|
||||
user,
|
||||
snapshot instanceof DataSnapshot.Unpacked unpacked
|
||||
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot,
|
||||
callback
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a data snapshot to the database
|
||||
*
|
||||
* @param user The user to save the data for
|
||||
* @param snapshot The snapshot to save
|
||||
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will be fired unless the
|
||||
* {@link DataSnapshot.SaveCause#fireDataSaveEvent()} is {@code false}
|
||||
* @since 3.0
|
||||
*/
|
||||
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().addSnapshot(
|
||||
user, snapshot instanceof DataSnapshot.Unpacked unpacked
|
||||
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot
|
||||
));
|
||||
this.addSnapshot(user, snapshot, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,6 +378,17 @@ public class HuskSyncAPI {
|
||||
plugin.registerSerializer(identifier, serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a registered data serializer by its identifier
|
||||
*
|
||||
* @param identifier The identifier of the data type to get the serializer for
|
||||
* @return The serializer for the given identifier, or an empty optional if the serializer isn't registered
|
||||
* @since 3.5.4
|
||||
*/
|
||||
public Optional<Serializer<Data>> getDataSerializer(@NotNull Identifier identifier) {
|
||||
return plugin.getSerializer(identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link DataSnapshot.Unpacked} from a {@link DataSnapshot.Packed}
|
||||
*
|
||||
@@ -478,17 +511,19 @@ public class HuskSyncAPI {
|
||||
*/
|
||||
static final class NotRegisteredException extends IllegalStateException {
|
||||
|
||||
private static final String MESSAGE = """
|
||||
Could not access the HuskSync API as it has not yet been registered. This could be because:
|
||||
private static final String REASONS = """
|
||||
This may be because:
|
||||
1) HuskSync has failed to enable successfully
|
||||
2) Your plugin isn't set to load after HuskSync has
|
||||
(Check if it set as a (soft)depend in plugin.yml or to load: BEFORE in paper-plugin.yml?)
|
||||
3) You are attempting to access HuskSync on plugin construction/before your plugin has enabled.
|
||||
4) You have shaded HuskSync into your plugin jar and need to fix your maven/gradle/build script
|
||||
to only include HuskSync as a dependency and not as a shaded dependency.""";
|
||||
3) You are attempting to access HuskSync on plugin construction/before your plugin has enabled.""";
|
||||
|
||||
NotRegisteredException(@NotNull String reasons) {
|
||||
super("Could not access the HuskSync API as it has not yet been registered. %s".formatted(reasons));
|
||||
}
|
||||
|
||||
NotRegisteredException() {
|
||||
super(MESSAGE);
|
||||
this(REASONS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,84 +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.command;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class Command extends Node {
|
||||
|
||||
private final String usage;
|
||||
private final Map<String, Boolean> additionalPermissions;
|
||||
|
||||
protected Command(@NotNull String name, @NotNull List<String> aliases, @NotNull String usage,
|
||||
@NotNull HuskSync plugin) {
|
||||
super(name, aliases, plugin);
|
||||
this.usage = usage;
|
||||
this.additionalPermissions = Maps.newHashMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onExecuted(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
if (!executor.hasPermission(getPermission())) {
|
||||
plugin.getLocales().getLocale("error_no_permission")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.runAsync(() -> this.execute(executor, args));
|
||||
}
|
||||
|
||||
public abstract void execute(@NotNull CommandUser executor, @NotNull String[] args);
|
||||
|
||||
@NotNull
|
||||
public final String getRawUsage() {
|
||||
return usage;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public final String getUsage() {
|
||||
return "/" + getName() + " " + getRawUsage();
|
||||
}
|
||||
|
||||
public final void addAdditionalPermissions(@NotNull Map<String, Boolean> permissions) {
|
||||
permissions.forEach((permission, value) -> this.additionalPermissions.put(getPermission(permission), value));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public final Map<String, Boolean> getAdditionalPermissions() {
|
||||
return additionalPermissions;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getDescription() {
|
||||
return plugin.getLocales().getRawLocale(getName() + "_command_description")
|
||||
.orElse(getUsage());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public final HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,13 +30,14 @@ import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class EnderChestCommand extends ItemsCommand {
|
||||
|
||||
public EnderChestCommand(@NotNull HuskSync plugin) {
|
||||
super(plugin, List.of("enderchest", "echest", "openechest"));
|
||||
super("enderchest", List.of("echest", "openechest"), plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,7 +52,8 @@ public class EnderChestCommand extends ItemsCommand {
|
||||
|
||||
// Display opening message
|
||||
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(),
|
||||
snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("dd/MM/yyyy, HH:mm")))
|
||||
snapshot.getTimestamp().format(DateTimeFormatter
|
||||
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
|
||||
// Show GUI
|
||||
@@ -72,8 +74,8 @@ public class EnderChestCommand extends ItemsCommand {
|
||||
|
||||
// Creates a new snapshot with the updated enderChest
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User user) {
|
||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(user);
|
||||
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
|
||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
|
||||
if (latestData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
@@ -90,10 +92,12 @@ public class EnderChestCommand extends ItemsCommand {
|
||||
);
|
||||
});
|
||||
|
||||
// Save data
|
||||
final RedisManager redis = plugin.getRedisManager();
|
||||
plugin.getDatabase().addSnapshot(user, snapshot);
|
||||
redis.sendUserDataUpdate(user, snapshot);
|
||||
redis.getUserData(user).ifPresent(data -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
|
||||
plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
|
||||
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
|
||||
redis.sendUserDataUpdate(user, data);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.JoinConfiguration;
|
||||
@@ -28,35 +30,33 @@ import net.kyori.adventure.text.format.TextColor;
|
||||
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.user.OnlineUser;
|
||||
import net.william278.husksync.util.LegacyConverter;
|
||||
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 org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class HuskSyncCommand extends Command implements TabProvider {
|
||||
|
||||
private static final Map<String, Boolean> SUB_COMMANDS = Map.of(
|
||||
"about", false,
|
||||
"status", true,
|
||||
"reload", true,
|
||||
"migrate", true,
|
||||
"update", true
|
||||
);
|
||||
public class HuskSyncCommand extends PluginCommand {
|
||||
|
||||
private final UpdateChecker updateChecker;
|
||||
private final AboutMenu aboutMenu;
|
||||
|
||||
public HuskSyncCommand(@NotNull HuskSync plugin) {
|
||||
super("husksync", List.of(), "[" + String.join("|", SUB_COMMANDS.keySet()) + "]", plugin);
|
||||
addAdditionalPermissions(SUB_COMMANDS);
|
||||
|
||||
super("husksync", List.of(), Permission.Default.TRUE, ExecutionScope.ALL, plugin);
|
||||
this.updateChecker = plugin.getUpdateChecker();
|
||||
this.aboutMenu = AboutMenu.builder()
|
||||
.title(Component.text("HuskSync"))
|
||||
@@ -66,7 +66,10 @@ public class HuskSyncCommand extends Command implements TabProvider {
|
||||
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("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)"),
|
||||
@@ -81,7 +84,8 @@ public class HuskSyncCommand extends Command implements TabProvider {
|
||||
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("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)),
|
||||
@@ -90,132 +94,168 @@ public class HuskSyncCommand extends Command implements TabProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
final String subCommand = parseStringArg(args, 0).orElse("about").toLowerCase(Locale.ENGLISH);
|
||||
if (SUB_COMMANDS.containsKey(subCommand) && !executor.hasPermission(getPermission(subCommand))) {
|
||||
plugin.getLocales().getLocale("error_no_permission")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
public void provide(@NotNull BaseCommand<?> command) {
|
||||
command.setDefaultExecutor((ctx) -> about(command, ctx));
|
||||
command.addSubCommand("about", (sub) -> sub.setDefaultExecutor((ctx) -> about(command, ctx)));
|
||||
command.addSubCommand("status", needsOp("status"), status());
|
||||
command.addSubCommand("reload", needsOp("reload"), reload());
|
||||
command.addSubCommand("update", needsOp("update"), update());
|
||||
command.addSubCommand("forceupgrade", forceUpgrade());
|
||||
command.addSubCommand("migrate", migrate());
|
||||
}
|
||||
|
||||
switch (subCommand) {
|
||||
case "about" -> executor.sendMessage(aboutMenu.toComponent());
|
||||
case "status" -> {
|
||||
getPlugin().getLocales().getLocale("system_status_header").ifPresent(executor::sendMessage);
|
||||
executor.sendMessage(Component.join(
|
||||
private void about(@NotNull BaseCommand<?> c, @NotNull CommandContext<?> ctx) {
|
||||
user(c, ctx).getAudience().sendMessage(aboutMenu.toComponent());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider status() {
|
||||
return (sub) -> sub.setDefaultExecutor((ctx) -> {
|
||||
final CommandUser user = user(sub, ctx);
|
||||
plugin.getLocales().getLocale("system_status_header").ifPresent(user::sendMessage);
|
||||
user.sendMessage(Component.join(
|
||||
JoinConfiguration.newlines(),
|
||||
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList()
|
||||
));
|
||||
});
|
||||
}
|
||||
case "reload" -> {
|
||||
|
||||
@NotNull
|
||||
private CommandProvider reload() {
|
||||
return (sub) -> sub.setDefaultExecutor((ctx) -> {
|
||||
final CommandUser user = user(sub, ctx);
|
||||
try {
|
||||
plugin.loadSettings();
|
||||
plugin.loadLocales();
|
||||
plugin.loadServer();
|
||||
plugin.getLocales().getLocale("reload_complete").ifPresent(executor::sendMessage);
|
||||
plugin.getLocales().getLocale("reload_complete").ifPresent(user::sendMessage);
|
||||
} catch (Throwable e) {
|
||||
executor.sendMessage(new MineDown(
|
||||
user.sendMessage(new MineDown(
|
||||
"[Error:](#ff3300) [Failed to reload the plugin. Check console for errors.](#ff7e5e)"
|
||||
));
|
||||
plugin.log(Level.SEVERE, "Failed to reload the plugin", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
case "migrate" -> {
|
||||
if (executor instanceof OnlineUser) {
|
||||
plugin.getLocales().getLocale("error_console_command_only")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
this.handleMigrationCommand(args);
|
||||
}
|
||||
case "update" -> updateChecker.check().thenAccept(checked -> {
|
||||
|
||||
@NotNull
|
||||
private CommandProvider update() {
|
||||
return (sub) -> sub.setDefaultExecutor((ctx) -> updateChecker.check().thenAccept(checked -> {
|
||||
final CommandUser user = user(sub, ctx);
|
||||
if (checked.isUpToDate()) {
|
||||
plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString())
|
||||
.ifPresent(executor::sendMessage);
|
||||
.ifPresent(user::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getLocales().getLocale("update_available", checked.getLatestVersion().toString(),
|
||||
plugin.getPluginVersion().toString()).ifPresent(executor::sendMessage);
|
||||
});
|
||||
default -> plugin.getLocales().getLocale("error_invalid_syntax", getUsage())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
plugin.getPluginVersion().toString()).ifPresent(user::sendMessage);
|
||||
}));
|
||||
}
|
||||
|
||||
// Handle a migration console command input
|
||||
private void handleMigrationCommand(@NotNull String[] args) {
|
||||
if (args.length < 2) {
|
||||
plugin.log(Level.INFO,
|
||||
"Please choose a migrator, then run \"husksync migrate <migrator>\"");
|
||||
this.logMigratorList();
|
||||
return;
|
||||
}
|
||||
|
||||
final Optional<Migrator> selectedMigrator = plugin.getAvailableMigrators().stream()
|
||||
.filter(available -> available.getIdentifier().equalsIgnoreCase(args[1]))
|
||||
.findFirst();
|
||||
selectedMigrator.ifPresentOrElse(migrator -> {
|
||||
if (args.length < 3) {
|
||||
plugin.log(Level.INFO, migrator.getHelpMenu());
|
||||
return;
|
||||
}
|
||||
switch (args[2]) {
|
||||
case "start" -> migrator.start().thenAccept(succeeded -> {
|
||||
if (succeeded) {
|
||||
plugin.log(Level.INFO, "Migration completed successfully!");
|
||||
} else {
|
||||
plugin.log(Level.WARNING, "Migration failed!");
|
||||
}
|
||||
});
|
||||
case "set" -> migrator.handleConfigurationCommand(Arrays.copyOfRange(args, 3, args.length));
|
||||
default -> plugin.log(Level.INFO, String.format(
|
||||
"Invalid syntax. Console usage: \"husksync migrate %s <start/set>", args[1]
|
||||
));
|
||||
}
|
||||
}, () -> {
|
||||
plugin.log(Level.INFO,
|
||||
"Please specify a valid migrator.\n" +
|
||||
"If a migrator is not available, please verify that you meet the prerequisites to use it.");
|
||||
this.logMigratorList();
|
||||
});
|
||||
}
|
||||
|
||||
// Log the list of available migrators
|
||||
private void logMigratorList() {
|
||||
@NotNull
|
||||
private CommandProvider migrate() {
|
||||
return (sub) -> {
|
||||
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
|
||||
sub.setDefaultExecutor((ctx) -> {
|
||||
plugin.log(Level.INFO, "Please choose a migrator, then run \"husksync migrate start <migrator>\"");
|
||||
plugin.log(Level.INFO, String.format(
|
||||
"List of available migrators:\nMigrator ID / Migrator Name:\n%s",
|
||||
plugin.getAvailableMigrators().stream()
|
||||
.map(migrator -> String.format("%s - %s", migrator.getIdentifier(), migrator.getName()))
|
||||
.collect(Collectors.joining("\n"))
|
||||
));
|
||||
});
|
||||
sub.addSubCommand("help", (help) -> help.addSyntax((cmd) -> {
|
||||
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
|
||||
plugin.log(Level.INFO, migrator.getHelpMenu());
|
||||
}, migrator()));
|
||||
sub.addSubCommand("start", (start) -> start.addSyntax((cmd) -> {
|
||||
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
|
||||
migrator.start().thenAccept(succeeded -> {
|
||||
if (succeeded) {
|
||||
plugin.log(Level.INFO, "Migration completed successfully!");
|
||||
} else {
|
||||
plugin.log(Level.WARNING, "Migration failed!");
|
||||
}
|
||||
});
|
||||
}, migrator()));
|
||||
sub.addSubCommand("set", (set) -> set.addSyntax((cmd) -> {
|
||||
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
|
||||
final String[] args = cmd.getArgument("args", String.class).split(" ");
|
||||
migrator.handleConfigurationCommand(args);
|
||||
}, migrator(), BaseCommand.greedyString("args")));
|
||||
};
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> suggest(@NotNull CommandUser user, @NotNull String[] args) {
|
||||
return switch (args.length) {
|
||||
case 0, 1 -> SUB_COMMANDS.keySet().stream().sorted().toList();
|
||||
default -> null;
|
||||
@NotNull
|
||||
private CommandProvider forceUpgrade() {
|
||||
return (sub) -> {
|
||||
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
|
||||
sub.setDefaultExecutor((ctx) -> {
|
||||
final LegacyConverter converter = plugin.getLegacyConverter().orElse(null);
|
||||
if (converter == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.runAsync(() -> {
|
||||
final Database database = plugin.getDatabase();
|
||||
plugin.log(Level.INFO, "Beginning forced legacy data upgrade for all users...");
|
||||
database.getAllUsers().forEach(user -> database.getLatestSnapshot(user).ifPresent(snapshot -> {
|
||||
final DataSnapshot.Packed upgraded = converter.convert(
|
||||
snapshot.asBytes(plugin),
|
||||
UUID.randomUUID(),
|
||||
OffsetDateTime.now()
|
||||
);
|
||||
upgraded.setSaveCause(DataSnapshot.SaveCause.CONVERTED_FROM_V2);
|
||||
plugin.getDatabase().addSnapshot(user, upgraded);
|
||||
plugin.getRedisManager().clearUserData(user);
|
||||
}));
|
||||
plugin.log(Level.INFO, "Legacy data upgrade complete!");
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private <S> ArgumentElement<S, Migrator> migrator() {
|
||||
return new ArgumentElement<>("migrator", reader -> {
|
||||
final String id = reader.readString();
|
||||
final Migrator migrator = plugin.getAvailableMigrators().stream()
|
||||
.filter(m -> m.getIdentifier().equalsIgnoreCase(id)).findFirst().orElse(null);
|
||||
if (migrator == null) {
|
||||
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
|
||||
}
|
||||
return migrator;
|
||||
}, (context, builder) -> {
|
||||
for (Migrator material : plugin.getAvailableMigrators()) {
|
||||
builder.suggest(material.getIdentifier());
|
||||
}
|
||||
return builder.buildFuture();
|
||||
});
|
||||
}
|
||||
|
||||
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()))),
|
||||
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"
|
||||
)),
|
||||
SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
|
||||
DATABASE_TYPE(plugin -> Component.text(plugin.getSettings().getDatabase().getType().getDisplayName())),
|
||||
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()
|
||||
@@ -231,13 +271,19 @@ public class HuskSyncCommand extends Command implements TabProvider {
|
||||
)),
|
||||
DATA_TYPES(plugin -> Component.join(
|
||||
JoinConfiguration.commas(true),
|
||||
plugin.getRegisteredDataTypes().stream().map(i -> {
|
||||
boolean enabled = plugin.getSettings().getSynchronization().isFeatureEnabled(i);
|
||||
return Component.textOfChildren(Component
|
||||
.text(i.toString()).appendSpace().append(Component.text(enabled ? '✔' : '❌')))
|
||||
.color(enabled ? NamedTextColor.GREEN : NamedTextColor.RED)
|
||||
.hoverEvent(HoverEvent.showText(Component.text(enabled ? "Enabled" : "Disabled")));
|
||||
}).toList()
|
||||
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;
|
||||
|
||||
@@ -30,13 +30,14 @@ import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class InventoryCommand extends ItemsCommand {
|
||||
|
||||
public InventoryCommand(@NotNull HuskSync plugin) {
|
||||
super(plugin, List.of("inventory", "invsee", "openinv"));
|
||||
super("inventory", List.of("invsee", "openinv"), plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -44,6 +45,7 @@ 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);
|
||||
return;
|
||||
@@ -51,7 +53,8 @@ public class InventoryCommand extends ItemsCommand {
|
||||
|
||||
// Display opening message
|
||||
plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(),
|
||||
snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("dd/MM/yyyy, HH:mm")))
|
||||
snapshot.getTimestamp().format(DateTimeFormatter
|
||||
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
|
||||
// Show GUI
|
||||
@@ -72,8 +75,8 @@ public class InventoryCommand extends ItemsCommand {
|
||||
|
||||
// Creates a new snapshot with the updated inventory
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User user) {
|
||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(user);
|
||||
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
|
||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
|
||||
if (latestData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
@@ -90,10 +93,12 @@ public class InventoryCommand extends ItemsCommand {
|
||||
);
|
||||
});
|
||||
|
||||
// Save data
|
||||
final RedisManager redis = plugin.getRedisManager();
|
||||
plugin.getDatabase().addSnapshot(user, snapshot);
|
||||
redis.sendUserDataUpdate(user, snapshot);
|
||||
redis.getUserData(user).ifPresent(data -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
|
||||
plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
|
||||
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
|
||||
redis.sendUserDataUpdate(user, data);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,85 +24,90 @@ import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import net.william278.uniform.BaseCommand;
|
||||
import net.william278.uniform.Permission;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class ItemsCommand extends Command implements TabProvider {
|
||||
public abstract class ItemsCommand extends PluginCommand {
|
||||
|
||||
protected ItemsCommand(@NotNull HuskSync plugin, @NotNull List<String> aliases) {
|
||||
super(aliases.get(0), aliases.subList(1, aliases.size()), "<player> [version_uuid]", plugin);
|
||||
setOperatorCommand(true);
|
||||
addAdditionalPermissions(Map.of("edit", true));
|
||||
protected ItemsCommand(@NotNull String name, @NotNull List<String> aliases, @NotNull HuskSync plugin) {
|
||||
super(name, aliases, Permission.Default.IF_OP, ExecutionScope.IN_GAME, plugin);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
if (!(executor instanceof OnlineUser player)) {
|
||||
public void provide(@NotNull BaseCommand<?> command) {
|
||||
command.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final UUID version = ctx.getArgument("version", UUID.class);
|
||||
final CommandUser executor = user(command, ctx);
|
||||
if (!(executor instanceof OnlineUser online)) {
|
||||
plugin.getLocales().getLocale("error_in_game_command_only")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the user to view the items for
|
||||
final Optional<User> optionalUser = parseStringArg(args, 0)
|
||||
.flatMap(name -> plugin.getDatabase().getUserByName(name));
|
||||
if (optionalUser.isEmpty()) {
|
||||
plugin.getLocales().getLocale(
|
||||
args.length >= 1 ? "error_invalid_player" : "error_invalid_syntax", getUsage()
|
||||
).ifPresent(player::sendMessage);
|
||||
this.showSnapshotItems(online, user, version);
|
||||
}, user("username"), uuid("version"));
|
||||
command.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final CommandUser executor = user(command, ctx);
|
||||
if (!(executor instanceof OnlineUser online)) {
|
||||
plugin.getLocales().getLocale("error_in_game_command_only")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the user data
|
||||
final User user = optionalUser.get();
|
||||
parseUUIDArg(args, 1).ifPresentOrElse(
|
||||
version -> this.showSnapshotItems(player, user, version),
|
||||
() -> this.showLatestItems(player, user)
|
||||
);
|
||||
this.showLatestItems(online, user);
|
||||
}, user("username"));
|
||||
}
|
||||
|
||||
// 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))
|
||||
.ifPresentOrElse(
|
||||
snapshot -> this.showItems(
|
||||
viewer, snapshot.unpack(plugin), user,
|
||||
viewer.hasPermission(getPermission("edit"))
|
||||
),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage)
|
||||
));
|
||||
.or(() -> {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return Optional.empty();
|
||||
})
|
||||
.flatMap(packed -> {
|
||||
if (packed.isInvalid()) {
|
||||
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(packed.unpack(plugin));
|
||||
})
|
||||
.ifPresent(snapshot -> this.showItems(
|
||||
viewer, snapshot, user, viewer.hasPermission(getPermission("edit"))
|
||||
)));
|
||||
}
|
||||
|
||||
// View a specific version of the user data
|
||||
private void showSnapshotItems(@NotNull OnlineUser viewer, @NotNull User user, @NotNull UUID version) {
|
||||
plugin.getDatabase().getSnapshot(user, version)
|
||||
.ifPresentOrElse(
|
||||
snapshot -> this.showItems(
|
||||
viewer, snapshot.unpack(plugin), user, false
|
||||
),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(viewer::sendMessage)
|
||||
);
|
||||
.or(() -> {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return Optional.empty();
|
||||
})
|
||||
.flatMap(packed -> {
|
||||
if (packed.isInvalid()) {
|
||||
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(packed.unpack(plugin));
|
||||
})
|
||||
.ifPresent(snapshot -> this.showItems(
|
||||
viewer, snapshot, user, false
|
||||
));
|
||||
}
|
||||
|
||||
// Show a GUI menu with the correct item data from the snapshot
|
||||
protected abstract void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
|
||||
@NotNull User user, boolean allowEdit);
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> suggest(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
return switch (args.length) {
|
||||
case 0, 1 -> plugin.getOnlineUsers().stream().map(User::getUsername).toList();
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,105 +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.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class Node implements Executable {
|
||||
|
||||
protected static final String PERMISSION_PREFIX = "husksync.command";
|
||||
|
||||
protected final HuskSync plugin;
|
||||
private final String name;
|
||||
private final List<String> aliases;
|
||||
private boolean operatorCommand = false;
|
||||
|
||||
protected Node(@NotNull String name, @NotNull List<String> aliases, @NotNull HuskSync plugin) {
|
||||
if (name.isBlank()) {
|
||||
throw new IllegalArgumentException("Command name cannot be blank");
|
||||
}
|
||||
this.name = name;
|
||||
this.aliases = aliases;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<String> getAliases() {
|
||||
return aliases;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getPermission(@NotNull String... child) {
|
||||
final StringJoiner joiner = new StringJoiner(".")
|
||||
.add(PERMISSION_PREFIX)
|
||||
.add(getName());
|
||||
for (final String node : child) {
|
||||
joiner.add(node);
|
||||
}
|
||||
return joiner.toString().trim();
|
||||
}
|
||||
|
||||
public boolean isOperatorCommand() {
|
||||
return operatorCommand;
|
||||
}
|
||||
|
||||
public void setOperatorCommand(boolean operatorCommand) {
|
||||
this.operatorCommand = operatorCommand;
|
||||
}
|
||||
|
||||
protected Optional<String> parseStringArg(@NotNull String[] args, int index) {
|
||||
if (args.length > index) {
|
||||
return Optional.of(args[index]);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
protected Optional<Integer> parseIntArg(@NotNull String[] args, int index) {
|
||||
return parseStringArg(args, index).flatMap(arg -> {
|
||||
try {
|
||||
return Optional.of(Integer.parseInt(arg));
|
||||
} catch (NumberFormatException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected Optional<UUID> parseUUIDArg(@NotNull String[] args, int index) {
|
||||
return parseStringArg(args, index).flatMap(arg -> {
|
||||
try {
|
||||
return Optional.of(UUID.fromString(arg));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import net.william278.uniform.BaseCommand;
|
||||
import net.william278.uniform.Command;
|
||||
import net.william278.uniform.Permission;
|
||||
import net.william278.uniform.element.ArgumentElement;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
public abstract class PluginCommand extends Command {
|
||||
|
||||
protected final HuskSync plugin;
|
||||
|
||||
protected PluginCommand(@NotNull String name, @NotNull List<String> aliases, @NotNull Permission.Default defPerm,
|
||||
@NotNull ExecutionScope scope, @NotNull HuskSync plugin) {
|
||||
super(name, aliases, getDescription(plugin, name), new Permission(createPermission(name), defPerm), scope);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
private static String getDescription(@NotNull HuskSync plugin, @NotNull String name) {
|
||||
return plugin.getLocales().getRawLocale("%s_command_description".formatted(name)).orElse("");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String createPermission(@NotNull String name, @NotNull String... sub) {
|
||||
return "husksync.command." + name + (sub.length > 0 ? "." + String.join(".", sub) : "");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected String getPermission(@NotNull String... sub) {
|
||||
return createPermission(this.getName(), sub);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@SuppressWarnings("rawtypes")
|
||||
protected CommandUser user(@NotNull BaseCommand base, @NotNull CommandContext context) {
|
||||
return adapt(base.getUser(context.getSource()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected Permission needsOp(@NotNull String... nodes) {
|
||||
return new Permission(getPermission(nodes), Permission.Default.IF_OP);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected CommandUser adapt(net.william278.uniform.CommandUser user) {
|
||||
return user.getUuid() == null ? plugin.getConsole() : plugin.getOnlineUser(user.getUuid()).orElseThrow();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected <S> ArgumentElement<S, User> user(@NotNull String name) {
|
||||
return new ArgumentElement<>(name, reader -> {
|
||||
final String username = reader.readString();
|
||||
return plugin.getDatabase().getUserByName(username).orElseThrow(
|
||||
() -> CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader)
|
||||
);
|
||||
}, (context, builder) -> {
|
||||
plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getUsername()));
|
||||
return builder.buildFuture();
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected <S> ArgumentElement<S, UUID> uuid(@NotNull String name) {
|
||||
return new ArgumentElement<>(name, reader -> {
|
||||
try {
|
||||
return UUID.fromString(reader.readString());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
|
||||
}
|
||||
}, (context, builder) -> builder.buildFuture());
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
|
||||
HUSKSYNC_COMMAND(HuskSyncCommand::new),
|
||||
USERDATA_COMMAND(UserDataCommand::new),
|
||||
INVENTORY_COMMAND(InventoryCommand::new),
|
||||
ENDER_CHEST_COMMAND(EnderChestCommand::new);
|
||||
|
||||
public final Function<HuskSync, PluginCommand> commandSupplier;
|
||||
|
||||
Type(@NotNull Function<HuskSync, PluginCommand> supplier) {
|
||||
this.commandSupplier = supplier;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public PluginCommand supply(@NotNull HuskSync plugin) {
|
||||
return commandSupplier.apply(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static PluginCommand[] create(@NotNull HuskSync plugin) {
|
||||
return Arrays.stream(values()).map(type -> type.supply(plugin))
|
||||
.filter(command -> !plugin.getSettings().isCommandDisabled(command))
|
||||
.toArray(PluginCommand[]::new);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,50 +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.command;
|
||||
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TabProvider {
|
||||
|
||||
@Nullable
|
||||
List<String> suggest(@NotNull CommandUser user, @NotNull String[] args);
|
||||
|
||||
@NotNull
|
||||
default List<String> getSuggestions(@NotNull CommandUser user, @NotNull String[] args) {
|
||||
List<String> suggestions = suggest(user, args);
|
||||
if (suggestions == null) {
|
||||
suggestions = List.of();
|
||||
}
|
||||
return filter(suggestions, args);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default List<String> filter(@NotNull List<String> suggestions, @NotNull String[] args) {
|
||||
return suggestions.stream()
|
||||
.filter(suggestion -> args.length == 0 || suggestion.toLowerCase()
|
||||
.startsWith(args[args.length - 1].toLowerCase().trim()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,105 +19,97 @@
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
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.User;
|
||||
import net.william278.husksync.util.DataDumper;
|
||||
import net.william278.husksync.util.DataSnapshotList;
|
||||
import net.william278.husksync.util.DataSnapshotOverview;
|
||||
import net.william278.uniform.BaseCommand;
|
||||
import net.william278.uniform.CommandProvider;
|
||||
import net.william278.uniform.Permission;
|
||||
import net.william278.uniform.element.ArgumentElement;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class UserDataCommand extends Command implements TabProvider {
|
||||
|
||||
private static final Map<String, Boolean> SUB_COMMANDS = Map.of(
|
||||
"view", false,
|
||||
"list", false,
|
||||
"delete", true,
|
||||
"restore", true,
|
||||
"pin", true,
|
||||
"dump", true
|
||||
);
|
||||
public class UserDataCommand extends PluginCommand {
|
||||
|
||||
public UserDataCommand(@NotNull HuskSync plugin) {
|
||||
super("userdata", List.of("playerdata"), String.format(
|
||||
"<%s> [username] [version_uuid]", String.join("/", SUB_COMMANDS.keySet())
|
||||
), plugin);
|
||||
setOperatorCommand(true);
|
||||
addAdditionalPermissions(SUB_COMMANDS);
|
||||
super("userdata", List.of("playerdata"), Permission.Default.IF_OP, ExecutionScope.ALL, plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
final String subCommand = parseStringArg(args, 0).orElse("view").toLowerCase(Locale.ENGLISH);
|
||||
final Optional<User> optionalUser = parseStringArg(args, 1)
|
||||
.flatMap(name -> plugin.getDatabase().getUserByName(name))
|
||||
.or(() -> parseStringArg(args, 0).flatMap(name -> plugin.getDatabase().getUserByName(name)))
|
||||
.or(() -> args.length < 2 && executor instanceof User userExecutor
|
||||
? Optional.of(userExecutor) : Optional.empty());
|
||||
final Optional<UUID> optionalUuid = parseUUIDArg(args, 2).or(() -> parseUUIDArg(args, 1));
|
||||
if (optionalUser.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_player")
|
||||
public void provide(@NotNull BaseCommand<?> command) {
|
||||
command.addSubCommand("view", needsOp("view"), view());
|
||||
command.addSubCommand("list", needsOp("list"), list());
|
||||
command.addSubCommand("delete", needsOp("delete"), delete());
|
||||
command.addSubCommand("restore", needsOp("restore"), restore());
|
||||
command.addSubCommand("pin", needsOp("pin"), pin());
|
||||
command.addSubCommand("dump", needsOp("dump"), dump());
|
||||
}
|
||||
|
||||
// Show the latest snapshot
|
||||
private void viewLatestSnapshot(@NotNull CommandUser executor, @NotNull User user) {
|
||||
plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse(
|
||||
data -> {
|
||||
if (data.isInvalid()) {
|
||||
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
final User user = optionalUser.get();
|
||||
switch (subCommand) {
|
||||
case "view" -> optionalUuid.ifPresentOrElse(
|
||||
// Show the specified snapshot
|
||||
version -> plugin.getDatabase().getSnapshot(user, version).ifPresentOrElse(
|
||||
data -> DataSnapshotOverview.of(
|
||||
data.unpack(plugin), data.getFileSize(plugin), user, plugin
|
||||
).show(executor),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage)),
|
||||
|
||||
// Show the latest snapshot
|
||||
() -> plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse(
|
||||
data -> DataSnapshotOverview.of(
|
||||
data.unpack(plugin), data.getFileSize(plugin), user, plugin
|
||||
).show(executor),
|
||||
DataSnapshotOverview.of(data.unpack(plugin), data.getFileSize(plugin), user, plugin)
|
||||
.show(executor);
|
||||
},
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(executor::sendMessage))
|
||||
.ifPresent(executor::sendMessage)
|
||||
);
|
||||
}
|
||||
|
||||
case "list" -> {
|
||||
// Check if there is data to display
|
||||
// Show the specified snapshot
|
||||
private void viewSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
||||
plugin.getDatabase().getSnapshot(user, version).ifPresentOrElse(
|
||||
data -> {
|
||||
if (data.isInvalid()) {
|
||||
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
DataSnapshotOverview.of(data.unpack(plugin), data.getFileSize(plugin), user, plugin)
|
||||
.show(executor);
|
||||
},
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage)
|
||||
);
|
||||
}
|
||||
|
||||
// View a list of snapshots
|
||||
private void listSnapshots(@NotNull CommandUser executor, @NotNull User user, int page) {
|
||||
final List<DataSnapshot.Packed> dataList = plugin.getDatabase().getAllSnapshots(user);
|
||||
if (dataList.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the list to the player
|
||||
DataSnapshotList.create(dataList, user, plugin).displayPage(
|
||||
executor,
|
||||
parseIntArg(args, 2).or(() -> parseIntArg(args, 1)).orElse(1)
|
||||
);
|
||||
DataSnapshotList.create(dataList, user, plugin).displayPage(executor, page);
|
||||
}
|
||||
|
||||
case "delete" -> {
|
||||
if (optionalUuid.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata delete <username> <version_uuid>")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete user data by specified UUID
|
||||
final UUID version = optionalUuid.get();
|
||||
// Delete a snapshot
|
||||
private void deleteSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
||||
if (!plugin.getDatabase().deleteSnapshot(user, version)) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getRedisManager().clearUserData(user);
|
||||
plugin.getLocales().getLocale("data_deleted",
|
||||
version.toString().split("-")[0],
|
||||
version.toString(),
|
||||
@@ -126,16 +118,9 @@ public class UserDataCommand extends Command implements TabProvider {
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
|
||||
case "restore" -> {
|
||||
if (optionalUuid.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata restore <username> <version_uuid>")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore user data by specified UUID
|
||||
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, optionalUuid.get());
|
||||
// Restore a snapshot
|
||||
private void restoreSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
||||
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, version);
|
||||
if (optionalData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
@@ -144,6 +129,11 @@ public class UserDataCommand extends Command implements TabProvider {
|
||||
|
||||
// Restore users with a minimum of one health (prevent restoring players with <= 0 health)
|
||||
final DataSnapshot.Packed data = optionalData.get().copy();
|
||||
if (data.isInvalid()) {
|
||||
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
data.edit(plugin, (unpacked -> {
|
||||
unpacked.getHealth().ifPresent(status -> status.setHealth(Math.max(1, status.getHealth())));
|
||||
unpacked.setSaveCause(DataSnapshot.SaveCause.BACKUP_RESTORE);
|
||||
@@ -152,23 +142,19 @@ public class UserDataCommand extends Command implements TabProvider {
|
||||
);
|
||||
}));
|
||||
|
||||
// Set the user's data and send a message
|
||||
plugin.getDatabase().addSnapshot(user, data);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, data);
|
||||
plugin.getLocales().getLocale("data_restored", user.getUsername(), user.getUuid().toString(),
|
||||
data.getShortId(), data.getId().toString()).ifPresent(executor::sendMessage);
|
||||
// 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.sendUserDataUpdate(u, s);
|
||||
plugin.getLocales().getLocale("data_restored", u.getUsername(), u.getUuid().toString(),
|
||||
s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage);
|
||||
});
|
||||
}
|
||||
|
||||
case "pin" -> {
|
||||
if (optionalUuid.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata pin <username> <version_uuid>")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that the data exists
|
||||
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, optionalUuid.get());
|
||||
// Pin a snapshot
|
||||
private void pinSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
||||
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, version);
|
||||
if (optionalData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
@@ -187,19 +173,10 @@ public class UserDataCommand extends Command implements TabProvider {
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
|
||||
case "dump" -> {
|
||||
if (optionalUuid.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata dump <username> <version_uuid>")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine dump type
|
||||
final boolean webDump = parseStringArg(args, 3)
|
||||
.map(arg -> arg.equalsIgnoreCase("web"))
|
||||
.orElse(false);
|
||||
final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, optionalUuid.get());
|
||||
// Dump a snapshot
|
||||
private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version,
|
||||
@NotNull DumpType type) {
|
||||
final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, version);
|
||||
if (data.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
@@ -211,27 +188,99 @@ public class UserDataCommand extends Command implements TabProvider {
|
||||
final DataDumper dumper = DataDumper.create(userData, user, plugin);
|
||||
try {
|
||||
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getUsername(),
|
||||
(webDump ? dumper.toWeb() : dumper.toFile())).ifPresent(executor::sendMessage);
|
||||
(type == DumpType.WEB ? dumper.toWeb() : dumper.toFile()))
|
||||
.ifPresent(executor::sendMessage);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "Failed to dump user data", e);
|
||||
}
|
||||
}
|
||||
|
||||
default -> plugin.getLocales().getLocale("error_invalid_syntax", getUsage())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> suggest(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
return switch (args.length) {
|
||||
case 0, 1 -> SUB_COMMANDS.keySet().stream().sorted().toList();
|
||||
case 2 -> plugin.getOnlineUsers().stream().map(User::getUsername).toList();
|
||||
case 4 -> parseStringArg(args, 0)
|
||||
.map(arg -> arg.equalsIgnoreCase("dump") ? List.of("web", "file") : null)
|
||||
.orElse(null);
|
||||
default -> null;
|
||||
@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"));
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider list() {
|
||||
return (sub) -> {
|
||||
sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
listSnapshots(user(sub, ctx), user, 1);
|
||||
}, user("username"));
|
||||
sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final int page = ctx.getArgument("page", Integer.class);
|
||||
listSnapshots(user(sub, ctx), user, page);
|
||||
}, user("username"), BaseCommand.intNum("page", 1));
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider delete() {
|
||||
return (sub) -> sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final UUID version = ctx.getArgument("version", UUID.class);
|
||||
deleteSnapshot(user(sub, ctx), user, version);
|
||||
}, user("username"), uuid("version"));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider restore() {
|
||||
return (sub) -> sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final UUID version = ctx.getArgument("version", UUID.class);
|
||||
restoreSnapshot(user(sub, ctx), user, version);
|
||||
}, user("username"), uuid("version"));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider pin() {
|
||||
return (sub) -> sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final UUID version = ctx.getArgument("version", UUID.class);
|
||||
pinSnapshot(user(sub, ctx), user, version);
|
||||
}, user("username"), uuid("version"));
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
private <S> ArgumentElement<S, DumpType> dumpType() {
|
||||
return new ArgumentElement<>("type", reader -> {
|
||||
final String type = reader.readString();
|
||||
return switch (type.toLowerCase(Locale.ENGLISH)) {
|
||||
case "web" -> DumpType.WEB;
|
||||
case "file" -> DumpType.FILE;
|
||||
default -> throw CommandSyntaxException.BUILT_IN_EXCEPTIONS
|
||||
.dispatcherUnknownArgument().createWithContext(reader);
|
||||
};
|
||||
}, (context, builder) -> {
|
||||
builder.suggest("web");
|
||||
builder.suggest("file");
|
||||
return builder.buildFuture();
|
||||
});
|
||||
}
|
||||
|
||||
enum DumpType {
|
||||
WEB,
|
||||
FILE
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ public class Locales {
|
||||
Map<String, String> locales = Maps.newTreeMap();
|
||||
|
||||
/**
|
||||
* Returns a raw, un-formatted locale loaded from the locales file
|
||||
* Returns a raw, unformatted locale loaded from the locale file
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @return An {@link Optional} containing the locale corresponding to the id, if it exists
|
||||
@@ -152,7 +152,7 @@ public class Locales {
|
||||
|
||||
value.append(c);
|
||||
}
|
||||
return value.toString().replace("__", "_\\_");
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -25,6 +25,7 @@ import de.exlll.configlib.Configuration;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.william278.husksync.command.PluginCommand;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.database.Database;
|
||||
@@ -69,12 +70,15 @@ public class Settings {
|
||||
@Comment("Enable development debug logging")
|
||||
private boolean debugLogging = false;
|
||||
|
||||
@Comment("Whether to provide modern, rich TAB suggestions for commands (if available)")
|
||||
private boolean brigadierTabCompletion = false;
|
||||
|
||||
@Comment({"Whether to enable the Player Analytics hook.", "Docs: https://william278.net/docs/husksync/plan-hook"})
|
||||
private boolean enablePlanHook = true;
|
||||
|
||||
@Comment("Whether to cancel game event packets directly when handling locked players if ProtocolLib or PacketEvents is installed")
|
||||
private boolean cancelPackets = true;
|
||||
|
||||
@Comment("Add HuskSync commands to this list to prevent them from being registered (e.g. ['userdata'])")
|
||||
@Getter(AccessLevel.NONE)
|
||||
private List<String> disabledCommands = Lists.newArrayList();
|
||||
|
||||
// Database settings
|
||||
@Comment("Database settings")
|
||||
@@ -85,10 +89,10 @@ public class Settings {
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class DatabaseSettings {
|
||||
|
||||
@Comment("Type of database to use (MYSQL, MARIADB)")
|
||||
@Comment("Type of database to use (MYSQL, MARIADB, POSTGRES, MONGO)")
|
||||
private Database.Type type = Database.Type.MYSQL;
|
||||
|
||||
@Comment("Specify credentials here for your MYSQL or MARIADB database")
|
||||
@Comment("Specify credentials here for your MYSQL, MARIADB, POSTGRES OR MONGO database")
|
||||
private DatabaseCredentials credentials = new DatabaseCredentials();
|
||||
|
||||
@Getter
|
||||
@@ -100,12 +104,13 @@ public class Settings {
|
||||
private String database = "HuskSync";
|
||||
private String username = "root";
|
||||
private String password = "pa55w0rd";
|
||||
@Comment("Only change this if you're using MARIADB or POSTGRES")
|
||||
private String parameters = String.join("&",
|
||||
"?autoReconnect=true", "useSSL=false",
|
||||
"useUnicode=true", "characterEncoding=UTF-8");
|
||||
}
|
||||
|
||||
@Comment("MYSQL / MARIADB database Hikari connection pool properties. Don't modify this unless you know what you're doing!")
|
||||
@Comment("MYSQL, MARIADB, POSTGRES database Hikari connection pool properties. Don't modify this unless you know what you're doing!")
|
||||
private PoolSettings connectionPool = new PoolSettings();
|
||||
|
||||
@Getter
|
||||
@@ -119,17 +124,33 @@ public class Settings {
|
||||
private long connectionTimeout = 5000;
|
||||
}
|
||||
|
||||
@Comment("Advanced MongoDB settings. Don't modify unless you know what you're doing!")
|
||||
private MongoSettings mongoSettings = new MongoSettings();
|
||||
|
||||
@Getter
|
||||
@Configuration
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class MongoSettings {
|
||||
private boolean usingAtlas = false;
|
||||
private String parameters = String.join("&",
|
||||
"?retryWrites=true", "w=majority",
|
||||
"authSource=HuskSync");
|
||||
}
|
||||
|
||||
@Comment("Names of tables to use on your database. Don't modify this unless you know what you're doing!")
|
||||
@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());
|
||||
}
|
||||
}
|
||||
|
||||
// Redis settings
|
||||
// 𝓡𝓮𝓭𝓲𝓼 settings
|
||||
@Comment("Redis settings")
|
||||
private RedisSettings redis = new RedisSettings();
|
||||
|
||||
@@ -168,7 +189,7 @@ public class Settings {
|
||||
}
|
||||
|
||||
// Synchronization settings
|
||||
@Comment("Redis settings")
|
||||
@Comment("Data syncing settings")
|
||||
private SynchronizationSettings synchronization = new SynchronizationSettings();
|
||||
|
||||
@Getter
|
||||
@@ -232,15 +253,12 @@ 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("Whether to synchronize player max health (requires health syncing to be enabled)")
|
||||
private boolean synchronizeMaxHealth = 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).")
|
||||
private int networkLatencyMilliseconds = 500;
|
||||
@@ -252,6 +270,53 @@ 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("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();
|
||||
@@ -274,4 +339,10 @@ public class Settings {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCommandDisabled(@NotNull PluginCommand command) {
|
||||
return disabledCommands.stream().map(c -> c.startsWith("/") ? c.substring(1) : c)
|
||||
.anyMatch(c -> c.equalsIgnoreCase(command.getName()) || command.getAliases().contains(c));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -19,7 +19,13 @@
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -51,8 +57,8 @@ public interface Data {
|
||||
*/
|
||||
interface Items extends Data {
|
||||
|
||||
@NotNull
|
||||
Stack[] getStack();
|
||||
@Nullable
|
||||
Stack @NotNull [] getStack();
|
||||
|
||||
default int getSlotCount() {
|
||||
return getStack().length;
|
||||
@@ -76,6 +82,10 @@ public interface Data {
|
||||
*/
|
||||
interface Inventory extends Items {
|
||||
|
||||
int INVENTORY_SLOT_COUNT = 41;
|
||||
String ITEMS_TAG = "items";
|
||||
String HELD_ITEM_SLOT_TAG = "held_item_slot";
|
||||
|
||||
int getHeldItemSlot();
|
||||
|
||||
void setHeldItemSlot(int heldItemSlot) throws IllegalArgumentException;
|
||||
@@ -105,7 +115,7 @@ public interface Data {
|
||||
* Data container holding data for ender chests
|
||||
*/
|
||||
interface EnderChest extends Items {
|
||||
|
||||
int ENDER_CHEST_SLOT_COUNT = 27;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -121,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
|
||||
@@ -144,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);
|
||||
@@ -283,15 +293,148 @@ public interface Data {
|
||||
|
||||
void setHealth(double health);
|
||||
|
||||
double getMaxHealth();
|
||||
/**
|
||||
* @deprecated Use {@link Attributes#getMaxHealth()} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
default double getMaxHealth() {
|
||||
return getHealth();
|
||||
}
|
||||
|
||||
void setMaxHealth(double maxHealth);
|
||||
/**
|
||||
* @deprecated Use {@link Attributes#setMaxHealth(double)} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
default void setMaxHealth(double maxHealth) {
|
||||
}
|
||||
|
||||
double getHealthScale();
|
||||
|
||||
void setHealthScale(double healthScale);
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding player attribute data
|
||||
*/
|
||||
interface Attributes extends Data {
|
||||
|
||||
Key MAX_HEALTH_KEY = Key.key("generic.max_health");
|
||||
|
||||
List<Attribute> getAttributes();
|
||||
|
||||
record Attribute(
|
||||
@NotNull String name,
|
||||
double baseValue,
|
||||
@NotNull Set<Modifier> modifiers
|
||||
) {
|
||||
|
||||
public double getValue() {
|
||||
double value = baseValue;
|
||||
for (Modifier modifier : modifiers) {
|
||||
value = modifier.modify(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Accessors(fluent = true)
|
||||
@RequiredArgsConstructor
|
||||
final class Modifier {
|
||||
final static String ANY_EQUIPMENT_SLOT_GROUP = "any";
|
||||
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Nullable
|
||||
@SerializedName("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 operation;
|
||||
|
||||
@SerializedName("equipment_slot")
|
||||
@Deprecated(since = "3.7")
|
||||
private 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.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) {
|
||||
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 (operation) {
|
||||
case 0 -> value + amount;
|
||||
case 1 -> value * amount;
|
||||
case 2 -> value * (1 + amount);
|
||||
default -> value;
|
||||
};
|
||||
}
|
||||
|
||||
public boolean hasUuid() {
|
||||
return uuid != null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public UUID uuid() {
|
||||
return uuid != null ? uuid : UUID.nameUUIDFromBytes(name.getBytes());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
default Optional<Attribute> getAttribute(@NotNull Key key) {
|
||||
return getAttributes().stream()
|
||||
.filter(attribute -> attribute.name().equals(key.asString()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
default void removeAttribute(@NotNull Key key) {
|
||||
getAttributes().removeIf(attribute -> attribute.name().equals(key.asString()));
|
||||
}
|
||||
|
||||
default double getMaxHealth() {
|
||||
return getAttribute(MAX_HEALTH_KEY)
|
||||
.map(Attribute::getValue)
|
||||
.orElse(20.0);
|
||||
}
|
||||
|
||||
default void setMaxHealth(double maxHealth) {
|
||||
removeAttribute(MAX_HEALTH_KEY);
|
||||
getAttributes().add(new Attribute(MAX_HEALTH_KEY.asString(), maxHealth, Sets.newHashSet()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
@@ -341,12 +484,7 @@ public interface Data {
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
* <li>Game mode</li>
|
||||
* <li>Allow flight</li>
|
||||
* <li>Is flying</li>
|
||||
* </ul>
|
||||
* Data container holding data for the player's current game mode
|
||||
*/
|
||||
interface GameMode extends Data {
|
||||
|
||||
@@ -355,13 +493,65 @@ public interface Data {
|
||||
|
||||
void setGameMode(@NotNull String gameMode);
|
||||
|
||||
boolean getAllowFlight();
|
||||
/**
|
||||
* Get if the player can fly.
|
||||
*
|
||||
* @return {@code false} since v3.5
|
||||
* @deprecated Moved to its own data type. This will always return {@code false}.
|
||||
* Use {@link FlightStatus#isAllowFlight()} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
default boolean getAllowFlight() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if the player can fly.
|
||||
*
|
||||
* @deprecated Moved to its own data type.
|
||||
* Use {@link FlightStatus#setAllowFlight(boolean)} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
default void setAllowFlight(boolean allowFlight) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if the player is flying.
|
||||
*
|
||||
* @return {@code false} since v3.5
|
||||
* @deprecated Moved to its own data type. This will always return {@code false}.
|
||||
* Use {@link FlightStatus#isFlying()} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
default boolean getIsFlying() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if the player is flying.
|
||||
*
|
||||
* @deprecated Moved to its own data type.
|
||||
* Use {@link FlightStatus#setFlying(boolean)} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
default void setIsFlying(boolean isFlying) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for the player's flight status
|
||||
*
|
||||
* @since 3.5
|
||||
*/
|
||||
interface FlightStatus extends Data {
|
||||
boolean isAllowFlight();
|
||||
|
||||
void setAllowFlight(boolean allowFlight);
|
||||
|
||||
boolean getIsFlying();
|
||||
boolean isFlying();
|
||||
|
||||
void setIsFlying(boolean isFlying);
|
||||
void setFlying(boolean isFlying);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* An exception related to {@link DataSnapshot} formatting, thrown if an exception occurs when unpacking a snapshot
|
||||
*/
|
||||
@Getter
|
||||
public class DataException extends IllegalStateException {
|
||||
|
||||
private final Reason reason;
|
||||
|
||||
private DataException(@NotNull DataException.Reason reason, @NotNull DataSnapshot data, @NotNull HuskSync plugin) {
|
||||
super(reason.getMessage(plugin, data));
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reasons why {@link DataException}s were thrown
|
||||
*/
|
||||
@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.",
|
||||
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.",
|
||||
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.",
|
||||
snapshot.getPlatformType(), plugin.getPlatformType())),
|
||||
NO_LEGACY_CONVERTER((plugin, snapshot) -> String.format("No legacy converter to convert format version: %s",
|
||||
snapshot.getFormatVersion()));
|
||||
|
||||
private final BiFunction<HuskSync, DataSnapshot, String> exception;
|
||||
|
||||
@NotNull
|
||||
String getMessage(@NotNull HuskSync plugin, @NotNull DataSnapshot data) {
|
||||
return exception.apply(plugin, data);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
DataException toException(@NotNull DataSnapshot data, @NotNull HuskSync plugin) {
|
||||
return new DataException(this, data, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,8 +30,8 @@ public interface DataHolder {
|
||||
@NotNull
|
||||
Map<Identifier, Data> getData();
|
||||
|
||||
default Optional<? extends Data> getData(@NotNull Identifier identifier) {
|
||||
return Optional.ofNullable(getData().get(identifier));
|
||||
default Optional<? extends Data> getData(@NotNull Identifier id) {
|
||||
return getData().entrySet().stream().filter(e -> e.getKey().equals(id)).map(Map.Entry::getValue).findFirst();
|
||||
}
|
||||
|
||||
default void setData(@NotNull Identifier identifier, @NotNull Data data) {
|
||||
@@ -110,6 +110,15 @@ public interface DataHolder {
|
||||
getData().put(Identifier.HUNGER, hunger);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Attributes> getAttributes() {
|
||||
return Optional.ofNullable((Data.Attributes) getData().get(Identifier.ATTRIBUTES));
|
||||
}
|
||||
|
||||
default void setAttributes(@NotNull Data.Attributes attributes) {
|
||||
getData().put(Identifier.ATTRIBUTES, attributes);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Experience> getExperience() {
|
||||
return Optional.ofNullable((Data.Experience) getData().get(Identifier.EXPERIENCE));
|
||||
@@ -128,6 +137,15 @@ public interface DataHolder {
|
||||
getData().put(Identifier.GAME_MODE, gameMode);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.FlightStatus> getFlightStatus() {
|
||||
return Optional.ofNullable((Data.FlightStatus) getData().get(Identifier.FLIGHT_STATUS));
|
||||
}
|
||||
|
||||
default void setFlightStatus(@NotNull Data.FlightStatus flightStatus) {
|
||||
getData().put(Identifier.FLIGHT_STATUS, flightStatus);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.PersistentData> getPersistentData() {
|
||||
return Optional.ofNullable((Data.PersistentData) getData().get(Identifier.PERSISTENT_DATA));
|
||||
|
||||
@@ -23,10 +23,16 @@ import com.google.common.collect.Maps;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import org.apache.commons.text.WordUtils;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -41,6 +47,8 @@ import java.util.stream.Collectors;
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
@SuppressWarnings({"LombokSetterMayBeUsed", "LombokGetterMayBeUsed"})
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class DataSnapshot {
|
||||
|
||||
/*
|
||||
@@ -59,7 +67,7 @@ public class DataSnapshot {
|
||||
protected OffsetDateTime timestamp;
|
||||
|
||||
@SerializedName("save_cause")
|
||||
protected SaveCause saveCause;
|
||||
protected String saveCause;
|
||||
|
||||
@SerializedName("server_name")
|
||||
protected String serverName;
|
||||
@@ -76,8 +84,13 @@ public class DataSnapshot {
|
||||
@SerializedName("data")
|
||||
protected Map<String, String> data;
|
||||
|
||||
// If the snapshot is invalid, this will be set to the validation exception
|
||||
@Nullable
|
||||
@Expose(serialize = false, deserialize = false)
|
||||
transient DataException.Reason exception = null;
|
||||
|
||||
private DataSnapshot(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
this.id = id;
|
||||
this.pinned = pinned;
|
||||
@@ -90,10 +103,6 @@ public class DataSnapshot {
|
||||
this.formatVersion = formatVersion;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private DataSnapshot() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public static DataSnapshot.Builder builder(@NotNull HuskSync plugin) {
|
||||
@@ -104,37 +113,25 @@ public class DataSnapshot {
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public static DataSnapshot.Packed deserialize(@NotNull HuskSync plugin, byte[] data, @Nullable UUID id,
|
||||
@Nullable OffsetDateTime timestamp) throws IllegalStateException {
|
||||
@Nullable OffsetDateTime timestamp) {
|
||||
final DataSnapshot.Packed snapshot = plugin.getDataAdapter().fromBytes(data, DataSnapshot.Packed.class);
|
||||
if (snapshot.getMinecraftVersion().compareTo(plugin.getMinecraftVersion()) > 0) {
|
||||
throw new IllegalStateException(String.format("Cannot set data for user because the Minecraft version of " +
|
||||
"their user data (%s) is newer than the server's Minecraft version (%s)." +
|
||||
"Please ensure each server is running the same version of Minecraft.",
|
||||
snapshot.getMinecraftVersion(), plugin.getMinecraftVersion()));
|
||||
return snapshot.invalid(DataException.Reason.INVALID_MINECRAFT_VERSION);
|
||||
}
|
||||
if (snapshot.getFormatVersion() > CURRENT_FORMAT_VERSION) {
|
||||
throw new IllegalStateException(String.format("Cannot set data for user because the format version of " +
|
||||
"their user data (%s) is newer than the current format version (%s). " +
|
||||
"Please ensure each server is running the latest version of HuskSync.",
|
||||
snapshot.getFormatVersion(), CURRENT_FORMAT_VERSION));
|
||||
return snapshot.invalid(DataException.Reason.INVALID_FORMAT_VERSION);
|
||||
}
|
||||
if (snapshot.getFormatVersion() < 4) {
|
||||
if (plugin.getLegacyConverter().isPresent()) {
|
||||
return plugin.getLegacyConverter().get().convert(
|
||||
data,
|
||||
Objects.requireNonNull(id, "Attempted legacy conversion with null UUID!"),
|
||||
data, Objects.requireNonNull(id, "Attempted legacy conversion with null UUID!"),
|
||||
Objects.requireNonNull(timestamp, "Attempted legacy conversion with null timestamp!")
|
||||
);
|
||||
}
|
||||
throw new IllegalStateException(String.format(
|
||||
"No legacy converter to convert format version: %s", snapshot.getFormatVersion()
|
||||
));
|
||||
return snapshot.invalid(DataException.Reason.NO_LEGACY_CONVERTER);
|
||||
}
|
||||
if (!snapshot.getPlatformType().equalsIgnoreCase(plugin.getPlatformType())) {
|
||||
throw new IllegalStateException(String.format("Cannot set data for user because the platform type of " +
|
||||
"their user data (%s) is different to the server platform type (%s). " +
|
||||
"Please ensure each server is running the same platform type.",
|
||||
snapshot.getPlatformType(), plugin.getPlatformType()));
|
||||
return snapshot.invalid(DataException.Reason.INVALID_PLATFORM_TYPE);
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
@@ -157,6 +154,17 @@ public class DataSnapshot {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>Internal use only</b> Set the ID of the snapshot
|
||||
*
|
||||
* @param id The snapshot ID
|
||||
* @since 3.0
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public void setId(@NotNull UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the short display ID of the snapshot
|
||||
*
|
||||
@@ -196,7 +204,7 @@ public class DataSnapshot {
|
||||
*/
|
||||
@NotNull
|
||||
public SaveCause getSaveCause() {
|
||||
return saveCause;
|
||||
return SaveCause.of(saveCause);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,7 +227,7 @@ public class DataSnapshot {
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setSaveCause(@NotNull SaveCause saveCause) {
|
||||
this.saveCause = saveCause;
|
||||
this.saveCause = saveCause.name();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -270,16 +278,39 @@ public class DataSnapshot {
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class Packed extends DataSnapshot implements Adaptable {
|
||||
|
||||
protected Packed(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
super(id, pinned, timestamp, saveCause, serverName, data, minecraftVersion, platformType, formatVersion);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private Packed() {
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
DataSnapshot.Packed invalid(@NotNull DataException.Reason reason) {
|
||||
this.exception = reason;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isInvalid() {
|
||||
return exception != null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getInvalidReason(@NotNull HuskSync plugin) {
|
||||
if (exception == null) {
|
||||
throw new IllegalStateException("Attempted to get an invalid reason for a valid snapshot!");
|
||||
}
|
||||
return exception.getMessage(plugin, this);
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
void validate(@NotNull HuskSync plugin) throws DataException {
|
||||
if (exception != null) {
|
||||
throw exception.toException(this, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
@@ -287,7 +318,7 @@ public class DataSnapshot {
|
||||
final Unpacked data = unpack(plugin);
|
||||
editor.accept(data);
|
||||
this.pinned = data.isPinned();
|
||||
this.saveCause = data.getSaveCause();
|
||||
this.saveCause = data.getSaveCause().name();
|
||||
this.data = data.serializeData(plugin);
|
||||
}
|
||||
|
||||
@@ -321,7 +352,8 @@ public class DataSnapshot {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public DataSnapshot.Unpacked unpack(@NotNull HuskSync plugin) {
|
||||
public DataSnapshot.Unpacked unpack(@NotNull HuskSync plugin) throws DataException {
|
||||
this.validate(plugin);
|
||||
return new Unpacked(
|
||||
id, pinned, timestamp, saveCause, serverName, data,
|
||||
getMinecraftVersion(), platformType, formatVersion, plugin
|
||||
@@ -338,10 +370,10 @@ public class DataSnapshot {
|
||||
public static class Unpacked extends DataSnapshot implements DataHolder {
|
||||
|
||||
@Expose(serialize = false, deserialize = false)
|
||||
private final Map<Identifier, Data> deserialized;
|
||||
private final TreeMap<Identifier, Data> deserialized;
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion,
|
||||
@NotNull HuskSync plugin) {
|
||||
super(id, pinned, timestamp, saveCause, serverName, data, minecraftVersion, platformType, formatVersion);
|
||||
@@ -349,7 +381,7 @@ public class DataSnapshot {
|
||||
}
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull String serverName, @NotNull Map<Identifier, Data> data,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull TreeMap<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;
|
||||
@@ -357,25 +389,25 @@ public class DataSnapshot {
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
private Map<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
|
||||
private TreeMap<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
|
||||
return data.entrySet().stream()
|
||||
.map((entry) -> plugin.getIdentifier(entry.getKey()).map(id -> Map.entry(
|
||||
id, plugin.getSerializers().get(id).deserialize(entry.getValue())
|
||||
)).orElse(null))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
.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(), getMinecraftVersion()),
|
||||
(a, b) -> b, () -> Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR)
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
private Map<String, String> serializeData(@NotNull HuskSync plugin) {
|
||||
return deserialized.entrySet().stream()
|
||||
.map((entry) -> Map.entry(entry.getKey().toString(),
|
||||
Objects.requireNonNull(
|
||||
plugin.getSerializers().get(entry.getKey()),
|
||||
String.format("No serializer found for %s", entry.getKey())
|
||||
).serialize(entry.getValue())))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
.collect(Collectors.toMap(
|
||||
entry -> entry.getKey().toString(),
|
||||
entry -> plugin.serializeData(entry.getKey(), entry.getValue())
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -421,12 +453,12 @@ public class DataSnapshot {
|
||||
private String serverName;
|
||||
private boolean pinned;
|
||||
private OffsetDateTime timestamp;
|
||||
private final Map<Identifier, Data> data;
|
||||
private final TreeMap<Identifier, Data> data;
|
||||
|
||||
private Builder(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
this.pinned = false;
|
||||
this.data = Maps.newHashMap();
|
||||
this.data = Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR);
|
||||
this.timestamp = OffsetDateTime.now();
|
||||
this.id = UUID.randomUUID();
|
||||
this.serverName = plugin.getServerName();
|
||||
@@ -658,6 +690,21 @@ public class DataSnapshot {
|
||||
return data(Identifier.HUNGER, hunger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the attributes of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.ATTRIBUTES, attributes)}
|
||||
* </p>
|
||||
*
|
||||
* @param attributes The user's attributes
|
||||
* @return The builder
|
||||
* @since 3.5
|
||||
*/
|
||||
@NotNull
|
||||
public Builder attributes(@NotNull Data.Attributes attributes) {
|
||||
return data(Identifier.ATTRIBUTES, attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the experience of the snapshot
|
||||
* <p>
|
||||
@@ -688,6 +735,21 @@ public class DataSnapshot {
|
||||
return data(Identifier.GAME_MODE, gameMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the flight status of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.FLIGHT_STATUS, flightStatus)}
|
||||
* </p>
|
||||
*
|
||||
* @param flightStatus The flight status
|
||||
* @return The builder
|
||||
* @since 3.5
|
||||
*/
|
||||
@NotNull
|
||||
public Builder flightStatus(@NotNull Data.FlightStatus flightStatus) {
|
||||
return data(Identifier.FLIGHT_STATUS, flightStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the persistent data container of the snapshot
|
||||
* <p>
|
||||
@@ -719,7 +781,7 @@ public class DataSnapshot {
|
||||
id,
|
||||
pinned || plugin.getSettings().getSynchronization().doAutoPin(saveCause),
|
||||
timestamp,
|
||||
saveCause,
|
||||
saveCause.name(),
|
||||
serverName,
|
||||
data,
|
||||
plugin.getMinecraftVersion(),
|
||||
@@ -742,138 +804,267 @@ public class DataSnapshot {
|
||||
|
||||
}
|
||||
|
||||
public interface Cause {
|
||||
|
||||
@NotNull
|
||||
String name();
|
||||
|
||||
/**
|
||||
* Identifies the cause of a player data save.
|
||||
* Returns the capitalized display name of the cause.
|
||||
*
|
||||
* @implNote This enum is saved in the database.
|
||||
* @return the cause display name
|
||||
*/
|
||||
@NotNull
|
||||
default String getDisplayName() {
|
||||
return WordUtils.capitalizeFully(name().replaceAll("_", " "));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A string wrapper, for identifying the cause of a player data save.
|
||||
* </p>
|
||||
* Cause names have a max length of 32 characters.
|
||||
*/
|
||||
public enum SaveCause {
|
||||
@Accessors(fluent = true)
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class SaveCause implements Cause {
|
||||
|
||||
/**
|
||||
* Indicates data saved when a player disconnected from the server (either to change servers, or to log off)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
DISCONNECT,
|
||||
public static final SaveCause DISCONNECT = of("DISCONNECT");
|
||||
|
||||
/**
|
||||
* Indicates data saved when the world saved
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
WORLD_SAVE,
|
||||
public static final SaveCause WORLD_SAVE = of("WORLD_SAVE");
|
||||
|
||||
/**
|
||||
* Indicates data saved when the user died
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
DEATH,
|
||||
public static final SaveCause DEATH = of("DEATH");
|
||||
|
||||
/**
|
||||
* Indicates data saved when the server shut down
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
SERVER_SHUTDOWN,
|
||||
public static final SaveCause SERVER_SHUTDOWN = of("SERVER_SHUTDOWN", false);
|
||||
|
||||
/**
|
||||
* Indicates data was saved by editing inventory contents via the {@code /inventory} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
INVENTORY_COMMAND,
|
||||
public static final SaveCause INVENTORY_COMMAND = of("INVENTORY_COMMAND");
|
||||
|
||||
/**
|
||||
* Indicates data was saved by editing Ender Chest contents via the {@code /enderchest} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
ENDERCHEST_COMMAND,
|
||||
public static final SaveCause ENDERCHEST_COMMAND = of("ENDERCHEST_COMMAND");
|
||||
|
||||
/**
|
||||
* Indicates data was saved by restoring it from a previous version
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
BACKUP_RESTORE,
|
||||
public static final SaveCause BACKUP_RESTORE = of("BACKUP_RESTORE");
|
||||
|
||||
/**
|
||||
* Indicates data was saved by an API call
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
API,
|
||||
public static final SaveCause API = of("API");
|
||||
|
||||
/**
|
||||
* Indicates data was saved from being imported from MySQLPlayerDataBridge
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
MPDB_MIGRATION,
|
||||
public static final SaveCause MPDB_MIGRATION = of("MPDB_MIGRATION", false);
|
||||
|
||||
/**
|
||||
* Indicates data was saved from being imported from a legacy version (v1.x -> v2.x)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
LEGACY_MIGRATION,
|
||||
public static final SaveCause LEGACY_MIGRATION = of("LEGACY_MIGRATION", false);
|
||||
|
||||
/**
|
||||
* Indicates data was saved from being imported from a legacy version (v2.x -> v3.x)
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
CONVERTED_FROM_V2;
|
||||
public static final SaveCause CONVERTED_FROM_V2 = of("CONVERTED_FROM_V2", false);
|
||||
|
||||
@NotNull
|
||||
public String getDisplayName() {
|
||||
return name().toLowerCase(Locale.ENGLISH);
|
||||
private final String name;
|
||||
|
||||
private final boolean fireDataSaveEvent;
|
||||
|
||||
/**
|
||||
* Get or create a {@link SaveCause} from a name
|
||||
*
|
||||
* @param name the name to be displayed
|
||||
* @return the cause
|
||||
*/
|
||||
@NotNull
|
||||
public static SaveCause of(@NotNull String name) {
|
||||
return new SaveCause(name.length() > 32 ? name.substring(0, 31) : name, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a {@link SaveCause} from a name and whether it should fire a save event
|
||||
*
|
||||
* @param name the name to be displayed
|
||||
* @param firesSaveEvent whether the cause should fire a save event
|
||||
* @return the cause
|
||||
*/
|
||||
@NotNull
|
||||
public static SaveCause of(@NotNull String name, boolean firesSaveEvent) {
|
||||
return new SaveCause(name.length() > 32 ? name.substring(0, 31) : name, firesSaveEvent);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getLocale(@NotNull HuskSync plugin) {
|
||||
return plugin.getLocales().getRawLocale("save_cause_" + name().toLowerCase())
|
||||
return plugin.getLocales()
|
||||
.getRawLocale("save_cause_%s".formatted(name().toLowerCase(Locale.ENGLISH)))
|
||||
.orElse(getDisplayName());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the cause of a player having their data updated.
|
||||
* A string wrapper, for identifying the cause of a player data update.
|
||||
* </p>
|
||||
* Cause names have a max length of 32 characters.
|
||||
*/
|
||||
public enum UpdateCause {
|
||||
@Accessors(fluent = true)
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class UpdateCause implements Cause {
|
||||
|
||||
/**
|
||||
* Indicates the data was updated by a synchronization process
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
SYNCHRONIZED("synchronization_complete", "synchronization_failed"),
|
||||
public static final UpdateCause SYNCHRONIZED = of("SYNCHRONIZED",
|
||||
"synchronization_complete", "synchronization_failed"
|
||||
);
|
||||
|
||||
/**
|
||||
* Indicates the data was updated by a user joining the server
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
NEW_USER("user_registration_complete", null),
|
||||
public static final UpdateCause NEW_USER = of("NEW_USER",
|
||||
"user_registration_complete", null
|
||||
);
|
||||
|
||||
/**
|
||||
* Indicates the data was updated by a data update process (management command, API, etc.)
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
UPDATED("data_update_complete", "data_update_failed");
|
||||
public static final UpdateCause UPDATED = of("UPDATED",
|
||||
"data_update_complete", "data_update_failed"
|
||||
);
|
||||
|
||||
private final String completedLocale;
|
||||
private final String failureLocale;
|
||||
/**
|
||||
* Indicates data was saved by an API call
|
||||
*
|
||||
* @since 3.3.3
|
||||
*/
|
||||
public static final UpdateCause API = of("API");
|
||||
|
||||
UpdateCause(@Nullable String completedLocale, @Nullable String failureLocale) {
|
||||
this.completedLocale = completedLocale;
|
||||
this.failureLocale = failureLocale;
|
||||
@NotNull
|
||||
private final String name;
|
||||
@Nullable
|
||||
private String completedLocale;
|
||||
@Nullable
|
||||
private String failureLocale;
|
||||
|
||||
/**
|
||||
* Get or create a {@link UpdateCause} from a name and completed/failure locales
|
||||
*
|
||||
* @param name the name to be displayed
|
||||
* @param completedLocale the locale to be displayed on successful update,
|
||||
* or {@code null} if none is to be shown
|
||||
* @param failureLocale the locale to be displayed on a failed update,
|
||||
* or {@code null} if none is to be shown
|
||||
* @return the cause
|
||||
*/
|
||||
public static UpdateCause of(@NotNull String name, @Nullable String completedLocale,
|
||||
@Nullable String failureLocale) {
|
||||
return new UpdateCause(
|
||||
name.length() > 32 ? name.substring(0, 31) : name,
|
||||
completedLocale, failureLocale
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a {@link UpdateCause} from a name
|
||||
*
|
||||
* @param name the name to be displayed
|
||||
* @return the cause
|
||||
*/
|
||||
@NotNull
|
||||
public static UpdateCause of(@NotNull String name) {
|
||||
return of(name, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message to be displayed when a user's data is successfully updated.
|
||||
*
|
||||
* @param plugin plugin instance
|
||||
* @return the message
|
||||
*/
|
||||
public Optional<MineDown> getCompletedLocale(@NotNull HuskSync plugin) {
|
||||
if (completedLocale != null) {
|
||||
return plugin.getLocales().getLocale(completedLocale);
|
||||
if (completedLocale() != null) {
|
||||
return Optional.of(plugin.getLocales().getLocale(completedLocale())
|
||||
.orElse(plugin.getLocales().format(getDisplayName())));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message to be displayed when a user's data fails to update.
|
||||
*
|
||||
* @param plugin plugin instance
|
||||
* @return the message
|
||||
*/
|
||||
public Optional<MineDown> getFailedLocale(@NotNull HuskSync plugin) {
|
||||
if (failureLocale != null) {
|
||||
return plugin.getLocales().getLocale(failureLocale);
|
||||
if (failureLocale() != null) {
|
||||
return Optional.of(plugin.getLocales().getLocale(failureLocale())
|
||||
.orElse(plugin.getLocales().format(failureLocale())));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static UpdateCause[] values() {
|
||||
return new UpdateCause[]{
|
||||
SYNCHRONIZED, NEW_USER, UPDATED
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,38 +19,83 @@
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import lombok.*;
|
||||
import net.kyori.adventure.key.InvalidKeyException;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.intellij.lang.annotations.Subst;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Identifiers of different types of {@link Data}s
|
||||
*/
|
||||
@Getter
|
||||
public class Identifier {
|
||||
|
||||
public static Identifier INVENTORY = huskSync("inventory", true);
|
||||
public static Identifier ENDER_CHEST = huskSync("ender_chest", true);
|
||||
public static Identifier POTION_EFFECTS = huskSync("potion_effects", true);
|
||||
public static Identifier ADVANCEMENTS = huskSync("advancements", true);
|
||||
public static Identifier LOCATION = huskSync("location", false);
|
||||
public static Identifier STATISTICS = huskSync("statistics", true);
|
||||
public static Identifier HEALTH = huskSync("health", true);
|
||||
public static Identifier HUNGER = huskSync("hunger", true);
|
||||
public static Identifier EXPERIENCE = huskSync("experience", true);
|
||||
public static Identifier GAME_MODE = huskSync("game_mode", true);
|
||||
public static Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
|
||||
// Built-in identifiers
|
||||
public static final Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
|
||||
public static final Identifier INVENTORY = huskSync("inventory", true);
|
||||
public static final Identifier ENDER_CHEST = huskSync("ender_chest", true);
|
||||
public static final Identifier ADVANCEMENTS = huskSync("advancements", true);
|
||||
public static final Identifier STATISTICS = huskSync("statistics", true);
|
||||
public static final Identifier POTION_EFFECTS = huskSync("potion_effects", true);
|
||||
public static final Identifier GAME_MODE = huskSync("game_mode", true);
|
||||
public static final Identifier FLIGHT_STATUS = huskSync("flight_status", true,
|
||||
Dependency.optional("game_mode")
|
||||
);
|
||||
public static final Identifier ATTRIBUTES = huskSync("attributes", true,
|
||||
Dependency.optional("inventory"),
|
||||
Dependency.optional("potion_effects")
|
||||
);
|
||||
public static final Identifier HEALTH = huskSync("health", true,
|
||||
Dependency.optional("attributes")
|
||||
);
|
||||
public static final Identifier HUNGER = huskSync("hunger", true,
|
||||
Dependency.optional("attributes")
|
||||
);
|
||||
public static final Identifier EXPERIENCE = huskSync("experience", true,
|
||||
Dependency.optional("advancements")
|
||||
);
|
||||
public static final Identifier LOCATION = huskSync("location", false,
|
||||
Dependency.optional("flight_status"),
|
||||
Dependency.optional("potion_effects")
|
||||
);
|
||||
|
||||
private final Key key;
|
||||
private final boolean configDefault;
|
||||
private final boolean enabledByDefault;
|
||||
@Getter
|
||||
private final Set<Dependency> dependencies;
|
||||
@Setter
|
||||
@Getter
|
||||
public boolean enabled;
|
||||
|
||||
private Identifier(@NotNull Key key, boolean configDefault) {
|
||||
private Identifier(@NotNull Key key, boolean enabledByDefault, @NotNull Set<Dependency> dependencies) {
|
||||
this.key = key;
|
||||
this.configDefault = configDefault;
|
||||
this.enabledByDefault = enabledByDefault;
|
||||
this.enabled = enabledByDefault;
|
||||
this.dependencies = dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an identifier from a {@link Key}
|
||||
*
|
||||
* @param key the key
|
||||
* @param dependencies the dependencies
|
||||
* @return the identifier
|
||||
* @since 3.5.4
|
||||
*/
|
||||
@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!");
|
||||
}
|
||||
return new Identifier(key, true, dependencies);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,10 +107,7 @@ public class Identifier {
|
||||
*/
|
||||
@NotNull
|
||||
public static Identifier from(@NotNull Key key) {
|
||||
if (key.namespace().equals("husksync")) {
|
||||
throw new IllegalArgumentException("You cannot register a key with \"husksync\" as the namespace!");
|
||||
}
|
||||
return new Identifier(key, true);
|
||||
return from(key, Collections.emptySet());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,25 +123,34 @@ public class Identifier {
|
||||
return from(Key.key(plugin, name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an identifier from a namespace, value, and dependencies
|
||||
*
|
||||
* @param plugin the namespace
|
||||
* @param name the value
|
||||
* @param dependencies the dependencies
|
||||
* @return the identifier
|
||||
* @since 3.5.4
|
||||
*/
|
||||
@NotNull
|
||||
public static Identifier from(@Subst("plugin") @NotNull String plugin, @Subst("null") @NotNull String name,
|
||||
@NotNull Set<Dependency> dependencies) {
|
||||
return from(Key.key(plugin, name), dependencies);
|
||||
}
|
||||
|
||||
// Return an identifier with a HuskSync namespace
|
||||
@NotNull
|
||||
private static Identifier huskSync(@Subst("null") @NotNull String name,
|
||||
boolean configDefault) throws InvalidKeyException {
|
||||
return new Identifier(Key.key("husksync", name), configDefault);
|
||||
return new Identifier(Key.key("husksync", name), configDefault, Collections.emptySet());
|
||||
}
|
||||
|
||||
// Return an identifier with a HuskSync namespace
|
||||
@NotNull
|
||||
@SuppressWarnings("unused")
|
||||
private static Identifier parse(@NotNull String key) throws InvalidKeyException {
|
||||
return huskSync(key, true);
|
||||
}
|
||||
|
||||
public boolean isEnabledByDefault() {
|
||||
return configDefault;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map.Entry<String, Boolean> getConfigEntry() {
|
||||
return Map.entry(getKeyValue(), configDefault);
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,13 +164,24 @@ public class Identifier {
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, Boolean> getConfigMap() {
|
||||
return Map.ofEntries(Stream.of(
|
||||
INVENTORY, ENDER_CHEST, POTION_EFFECTS, ADVANCEMENTS, LOCATION,
|
||||
STATISTICS, HEALTH, HUNGER, EXPERIENCE, GAME_MODE, PERSISTENT_DATA
|
||||
INVENTORY, ENDER_CHEST, POTION_EFFECTS, ADVANCEMENTS, LOCATION, STATISTICS,
|
||||
HEALTH, HUNGER, ATTRIBUTES, EXPERIENCE, GAME_MODE, FLIGHT_STATUS, PERSISTENT_DATA
|
||||
)
|
||||
.map(Identifier::getConfigEntry)
|
||||
.toArray(Map.Entry[]::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the identifier depends on the given identifier
|
||||
*
|
||||
* @param identifier the identifier to check
|
||||
* @return {@code true} if the identifier depends on the given identifier
|
||||
* @since 3.5.4
|
||||
*/
|
||||
public boolean dependsOn(@NotNull Identifier identifier) {
|
||||
return dependencies.contains(Dependency.required(identifier.key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the namespace of the identifier
|
||||
*
|
||||
@@ -174,4 +236,85 @@ public class Identifier {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the config entry for the identifier
|
||||
@NotNull
|
||||
private Map.Entry<String, Boolean> getConfigEntry() {
|
||||
return Map.entry(getKeyValue(), enabledByDefault);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two identifiers based on their dependencies.
|
||||
* <p>
|
||||
* If this identifier contains a dependency on the other, it should come after & vice versa
|
||||
*
|
||||
* @since 3.5.4
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
static class DependencyOrderComparator implements Comparator<Identifier> {
|
||||
|
||||
@Override
|
||||
public int compare(@NotNull Identifier i1, @NotNull Identifier i2) {
|
||||
if (i1.equals(i2)) {
|
||||
return 0;
|
||||
}
|
||||
if (i1.dependsOn(i2)) {
|
||||
if (i2.dependsOn(i1)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Found circular dependency between %s and %s".formatted(i1.getKey(), i2.getKey())
|
||||
);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a data dependency of an identifier, used to determine the order in which data is applied to users
|
||||
*
|
||||
* @since 3.5.4
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class Dependency {
|
||||
/**
|
||||
* Key of the data dependency see {@code Identifier#key()}
|
||||
*/
|
||||
private Key key;
|
||||
/**
|
||||
* Whether the data dependency is required to be present & enabled for the dependant data to enabled
|
||||
*/
|
||||
private boolean required;
|
||||
|
||||
@NotNull
|
||||
protected static Dependency required(@NotNull Key identifier) {
|
||||
return new Dependency(identifier, true);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Dependency optional(@NotNull Key identifier) {
|
||||
return new Dependency(identifier, false);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static Dependency required(@Subst("null") @NotNull String identifier) {
|
||||
return required(Key.key("husksync", identifier));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Dependency optional(@Subst("null") @NotNull String identifier) {
|
||||
return optional(Key.key("husksync", identifier));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Dependency other) {
|
||||
return key.equals(other.key);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,26 +19,55 @@
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface Serializer<T extends Data> {
|
||||
|
||||
T deserialize(@NotNull String serialized) throws DeserializationException;
|
||||
T deserialize(@NotNull String serialized);
|
||||
|
||||
default T deserialize(@NotNull String serialized, @NotNull Version dataMcVersion) throws DeserializationException {
|
||||
return deserialize(serialized);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
String serialize(@NotNull T element) throws SerializationException;
|
||||
|
||||
static final class DeserializationException extends IllegalStateException {
|
||||
final class DeserializationException extends IllegalStateException {
|
||||
DeserializationException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
static final class SerializationException extends IllegalStateException {
|
||||
final class SerializationException extends IllegalStateException {
|
||||
SerializationException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Json<T extends Data & Adaptable> implements Serializer<T> {
|
||||
|
||||
private final HuskSync plugin;
|
||||
private final Class<T> type;
|
||||
|
||||
public Json(@NotNull HuskSync plugin, @NotNull Class<T> type) {
|
||||
this.type = type;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
return plugin.getDataAdapter().fromJson(serialized, type);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull T element) throws SerializationException {
|
||||
return plugin.getDataAdapter().toJson(element);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.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;
|
||||
|
||||
public interface SerializerRegistry {
|
||||
|
||||
// Comparator for ordering identifiers based on dependency
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
Comparator<Identifier> DEPENDENCY_ORDER_COMPARATOR = new Identifier.DependencyOrderComparator();
|
||||
|
||||
/**
|
||||
* Returns the data serializer for the given {@link Identifier}
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
<T extends Data> TreeMap<Identifier, Serializer<T>> getSerializers();
|
||||
|
||||
/**
|
||||
* Register a data serializer for the given {@link Identifier}
|
||||
*
|
||||
* @param id the {@link Identifier}
|
||||
* @param serializer the {@link Serializer}
|
||||
* @since 3.0
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
default void registerSerializer(@NotNull Identifier id, @NotNull Serializer<? extends Data> serializer) {
|
||||
if (id.isCustom()) {
|
||||
getPlugin().log(Level.INFO, "Registered custom data type: %s".formatted(id));
|
||||
}
|
||||
id.setEnabled(id.isCustom() || getPlugin().getSettings().getSynchronization().isFeatureEnabled(id));
|
||||
getSerializers().put(id, (Serializer<Data>) serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure dependencies for identifiers that have required dependencies are met
|
||||
* <p>
|
||||
* This checks the dependencies of all registered identifiers and throws an {@link IllegalStateException}
|
||||
* if a dependency has not been registered or enabled via the config
|
||||
*
|
||||
* @since 3.5.4
|
||||
*/
|
||||
default void validateDependencies() throws IllegalStateException {
|
||||
getSerializers().keySet().stream().filter(Identifier::isEnabled)
|
||||
.forEach(identifier -> {
|
||||
final List<String> unmet = identifier.getDependencies().stream()
|
||||
.filter(Identifier.Dependency::isRequired)
|
||||
.filter(dep -> !isDataTypeAvailable(dep.getKey().asString()))
|
||||
.map(dep -> dep.getKey().asString()).toList();
|
||||
if (!unmet.isEmpty()) {
|
||||
identifier.setEnabled(false);
|
||||
getPlugin().log(Level.WARNING, "Disabled %s syncing as the following types need to be on: %s"
|
||||
.formatted(identifier, String.join(", ", unmet)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Identifier} for the given key
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
default Optional<Identifier> getIdentifier(@NotNull String key) {
|
||||
return getSerializers().keySet().stream()
|
||||
.filter(id -> id.getKey().asString().equals(key)).findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a data serializer for the given {@link Identifier}
|
||||
*
|
||||
* @param identifier the {@link Identifier} to get the serializer for
|
||||
* @return the {@link Serializer} for the given {@link Identifier}
|
||||
* @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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize data for the given {@link Identifier}
|
||||
*
|
||||
* @param identifier the {@link Identifier} to serialize data for
|
||||
* @param data the data to serialize
|
||||
* @return the serialized data
|
||||
* @throws IllegalArgumentException if no serializer is found for the given {@link Identifier}
|
||||
* @since 3.5.4
|
||||
*/
|
||||
@NotNull
|
||||
default String serializeData(@NotNull Identifier identifier, @NotNull Data data) throws IllegalStateException {
|
||||
return getSerializer(identifier).map(serializer -> serializer.serialize(data))
|
||||
.orElseThrow(() -> new IllegalStateException("No serializer found for %s".formatted(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 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.6.4
|
||||
*/
|
||||
@NotNull
|
||||
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))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize data for the given {@link Identifier data identifier}
|
||||
*
|
||||
* @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 set of registered data types
|
||||
*
|
||||
* @return the set of registered data types
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
default Set<Identifier> getRegisteredDataTypes() {
|
||||
return getSerializers().keySet();
|
||||
}
|
||||
|
||||
// Returns if a data type is available and enabled in the config
|
||||
private boolean isDataTypeAvailable(@NotNull String key) {
|
||||
return getIdentifier(key).map(Identifier::isEnabled).orElse(false);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
@@ -34,7 +34,7 @@ import java.util.logging.Level;
|
||||
public interface UserDataHolder extends DataHolder {
|
||||
|
||||
/**
|
||||
* Get the data that is enabled for syncing in the config
|
||||
* Get the data enabled for syncing in the config
|
||||
*
|
||||
* @return the data that is enabled for syncing
|
||||
* @since 3.0
|
||||
@@ -43,7 +43,7 @@ public interface UserDataHolder extends DataHolder {
|
||||
@NotNull
|
||||
default Map<Identifier, Data> getData() {
|
||||
return getPlugin().getRegisteredDataTypes().stream()
|
||||
.filter(type -> type.isCustom() || getPlugin().getSettings().getSynchronization().isFeatureEnabled(type))
|
||||
.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);
|
||||
@@ -60,7 +60,7 @@ public interface UserDataHolder extends DataHolder {
|
||||
*/
|
||||
@Override
|
||||
default void setData(@NotNull Identifier identifier, @NotNull Data data) {
|
||||
getPlugin().runSync(() -> data.apply(this, getPlugin()));
|
||||
getPlugin().runSync(() -> data.apply(this, getPlugin()), this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,7 +79,8 @@ public interface UserDataHolder extends DataHolder {
|
||||
* Deserialize and apply a data snapshot to this data owner
|
||||
* <p>
|
||||
* This method will deserialize the data on the current thread, then synchronously apply it on
|
||||
* the main server thread.
|
||||
* the main server thread. The order data will be applied is determined based on the dependencies of
|
||||
* each data type (see {@link Identifier.Dependency}).
|
||||
* </p>
|
||||
* The {@code runAfter} callback function will be run after the snapshot has been applied.
|
||||
*
|
||||
@@ -97,6 +98,7 @@ public interface UserDataHolder extends DataHolder {
|
||||
unpacked = snapshot.unpack(plugin);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, String.format("Failed to unpack data snapshot for %s", getUsername()), e);
|
||||
runAfter.accept(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,20 +107,23 @@ public interface UserDataHolder extends DataHolder {
|
||||
try {
|
||||
for (Map.Entry<Identifier, Data> entry : unpacked.getData().entrySet()) {
|
||||
final Identifier identifier = entry.getKey();
|
||||
if (plugin.getSettings().getSynchronization().isFeatureEnabled(identifier)) {
|
||||
if (!identifier.isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply the identified data
|
||||
if (identifier.isCustom()) {
|
||||
getCustomDataStore().put(identifier, entry.getValue());
|
||||
}
|
||||
entry.getValue().apply(this, plugin);
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, String.format("Failed to apply data snapshot to %s", getUsername()), e);
|
||||
plugin.runAsync(() -> runAfter.accept(false));
|
||||
return;
|
||||
}
|
||||
plugin.runAsync(() -> runAfter.accept(true));
|
||||
});
|
||||
}, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -171,6 +176,11 @@ public interface UserDataHolder extends DataHolder {
|
||||
this.setData(Identifier.GAME_MODE, gameMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setFlightStatus(@NotNull Data.FlightStatus flightStatus) {
|
||||
this.setData(Identifier.FLIGHT_STATUS, flightStatus);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setPersistentData(@NotNull Data.PersistentData persistentData) {
|
||||
this.setData(Identifier.PERSISTENT_DATA, persistentData);
|
||||
|
||||
@@ -23,8 +23,6 @@ import lombok.Getter;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.DataSnapshot.SaveCause;
|
||||
import net.william278.husksync.data.UserDataHolder;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -33,6 +31,7 @@ import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* An abstract representation of the plugin database, storing player data.
|
||||
@@ -108,6 +107,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.
|
||||
@@ -156,42 +163,23 @@ public abstract class Database {
|
||||
@Blocking
|
||||
public abstract boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid);
|
||||
|
||||
/**
|
||||
* Save user data to the database
|
||||
* </p>
|
||||
* This will remove the oldest data for the user if the amount of data exceeds the limit as configured
|
||||
*
|
||||
* @param user The user to add data for
|
||||
* @param snapshot The {@link DataSnapshot} to set.
|
||||
* The implementation should version it with a random UUID and the current timestamp during insertion.
|
||||
* @see UserDataHolder#createSnapshot(SaveCause)
|
||||
*/
|
||||
@Blocking
|
||||
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot) {
|
||||
if (snapshot.getSaveCause() != SaveCause.SERVER_SHUTDOWN) {
|
||||
plugin.fireEvent(
|
||||
plugin.getDataSaveEvent(user, snapshot),
|
||||
(event) -> this.addAndRotateSnapshot(user, snapshot)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.addAndRotateSnapshot(user, snapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>Internal</b> - Save user data to the database. This will:
|
||||
* Save user data to the database, doing the following (in order):
|
||||
* <ol>
|
||||
* <li>Delete their most recent snapshot, if it was created before the backup frequency time</li>
|
||||
* <li>Create the snapshot</li>
|
||||
* <li>Rotate snapshot backups</li>
|
||||
* </ol>
|
||||
* This is an expensive blocking method and should be run off the main thread.
|
||||
*
|
||||
* @param user The user to add data for
|
||||
* @param snapshot The {@link DataSnapshot} to set.
|
||||
* @apiNote Prefer {@link net.william278.husksync.sync.DataSyncer#saveData(User, DataSnapshot.Packed, BiConsumer)}.
|
||||
* </p>This method will not fire the {@link net.william278.husksync.event.DataSaveEvent}
|
||||
*/
|
||||
@Blocking
|
||||
private void addAndRotateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot) {
|
||||
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot) {
|
||||
final int backupFrequency = plugin.getSettings().getSynchronization().getSnapshotBackupFrequency();
|
||||
if (!snapshot.isPinned() && backupFrequency > 0) {
|
||||
this.rotateLatestSnapshot(user, snapshot.getTimestamp().minusHours(backupFrequency));
|
||||
@@ -273,9 +261,12 @@ public abstract class Database {
|
||||
/**
|
||||
* Identifies types of databases
|
||||
*/
|
||||
@Getter
|
||||
public enum Type {
|
||||
MYSQL("MySQL", "mysql"),
|
||||
MARIADB("MariaDB", "mariadb");
|
||||
MARIADB("MariaDB", "mariadb"),
|
||||
POSTGRES("PostgreSQL", "postgresql"),
|
||||
MONGO("MongoDB", "mongo");
|
||||
|
||||
private final String displayName;
|
||||
private final String protocol;
|
||||
@@ -284,16 +275,6 @@ public abstract class Database {
|
||||
this.displayName = displayName;
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
/*
|
||||
* 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.database;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.mongodb.ConnectionString;
|
||||
import com.mongodb.MongoException;
|
||||
import com.mongodb.client.FindIterable;
|
||||
import com.mongodb.client.model.Updates;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.database.mongo.MongoCollectionHelper;
|
||||
import net.william278.husksync.database.mongo.MongoConnectionHandler;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.bson.types.Binary;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
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.logging.Level;
|
||||
|
||||
public class MongoDbDatabase extends Database {
|
||||
private MongoConnectionHandler mongoConnectionHandler;
|
||||
private MongoCollectionHelper mongoCollectionHelper;
|
||||
|
||||
private final String usersTable;
|
||||
private final String userDataTable;
|
||||
|
||||
public MongoDbDatabase(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
this.usersTable = plugin.getSettings().getDatabase().getTableName(TableName.USERS);
|
||||
this.userDataTable = plugin.getSettings().getDatabase().getTableName(TableName.USER_DATA);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() throws IllegalStateException {
|
||||
final Settings.DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
|
||||
try {
|
||||
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);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ConnectionString createConnectionURI(Settings.DatabaseSettings.DatabaseCredentials credentials) {
|
||||
String baseURI = plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ?
|
||||
"mongodb+srv://{0}:{1}@{2}/{4}{5}" : "mongodb://{0}:{1}@{2}:{3}/{4}{5}";
|
||||
baseURI = baseURI.replace("{0}", credentials.getUsername());
|
||||
baseURI = baseURI.replace("{1}", credentials.getPassword());
|
||||
baseURI = baseURI.replace("{2}", credentials.getHost());
|
||||
baseURI = baseURI.replace("{3}", String.valueOf(credentials.getPort()));
|
||||
baseURI = baseURI.replace("{4}", credentials.getDatabase());
|
||||
baseURI = baseURI.replace("{5}", plugin.getSettings().getDatabase().getMongoSettings().getParameters());
|
||||
return new ConnectionString(baseURI);
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public void ensureUser(@NotNull User user) {
|
||||
try {
|
||||
getUser(user.getUuid()).ifPresentOrElse(
|
||||
existingUser -> {
|
||||
if (!existingUser.getUsername().equals(user.getUsername())) {
|
||||
// Update a user's name if it has changed in the database
|
||||
try {
|
||||
Document filter = new Document("uuid", existingUser.getUuid());
|
||||
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
||||
if (doc == null) {
|
||||
throw new MongoException("User document returned null!");
|
||||
}
|
||||
|
||||
Bson updates = Updates.set("username", user.getUsername());
|
||||
mongoCollectionHelper.updateDocument(usersTable, doc, updates);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
// Insert new player data into the database
|
||||
try {
|
||||
Document doc = new Document("uuid", user.getUuid()).append("username", user.getUsername());
|
||||
mongoCollectionHelper.insertDocument(usersTable, doc);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to ensure user data is in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<User> getUser(@NotNull UUID uuid) {
|
||||
try {
|
||||
Document filter = new Document("uuid", uuid);
|
||||
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
||||
if (doc != null) {
|
||||
return Optional.of(new User(uuid, doc.getString("username")));
|
||||
}
|
||||
return Optional.empty();
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get user data from the database", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<User> getUserByName(@NotNull String username) {
|
||||
try {
|
||||
Document filter = new Document("username", username);
|
||||
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
||||
if (doc != null) {
|
||||
return Optional.of(new User(doc.get("uuid", UUID.class),
|
||||
doc.getString("username")));
|
||||
}
|
||||
return Optional.empty();
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get user data from the database", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@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) {
|
||||
try {
|
||||
Document filter = new Document("player_uuid", user.getUuid());
|
||||
Document sort = new Document("timestamp", -1); // -1 = Descending
|
||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
||||
Document doc = iterable.first();
|
||||
if (doc != null) {
|
||||
final UUID versionUuid = doc.get("version_uuid", UUID.class);
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
|
||||
final Binary bin = doc.get("data", Binary.class);
|
||||
final byte[] dataByteArray = bin.getData();
|
||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
return Optional.empty();
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get latest snapshot from the database", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
@NotNull
|
||||
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
|
||||
try {
|
||||
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
|
||||
Document filter = new Document("player_uuid", user.getUuid());
|
||||
Document sort = new Document("timestamp", -1); // -1 = Descending
|
||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
||||
for (Document doc : iterable) {
|
||||
final UUID versionUuid = doc.get("version_uuid", UUID.class);
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
|
||||
final Binary bin = doc.get("data", Binary.class);
|
||||
final byte[] dataByteArray = bin.getData();
|
||||
retrievedData.add(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
return retrievedData;
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get all snapshots from the database", e);
|
||||
return Lists.newArrayList();
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
try {
|
||||
Document filter = new Document("player_uuid", user.getUuid()).append("version_uuid", versionUuid);
|
||||
Document sort = new Document("timestamp", -1); // -1 = Descending
|
||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
||||
Document doc = iterable.first();
|
||||
if (doc != null) {
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
|
||||
final Binary bin = doc.get("data", Binary.class);
|
||||
final byte[] dataByteArray = bin.getData();
|
||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
return Optional.empty();
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get snapshot from the database", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateSnapshots(@NotNull User user) {
|
||||
try {
|
||||
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
|
||||
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
|
||||
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
|
||||
if (unpinnedUserData.size() > 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);
|
||||
|
||||
for (Document doc : iterable) {
|
||||
mongoCollectionHelper.deleteDocument(userDataTable, doc);
|
||||
}
|
||||
}
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to rotate snapshots", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
try {
|
||||
Document filter = new Document("player_uuid", user.getUuid()).append("version_uuid", versionUuid);
|
||||
Document doc = mongoCollectionHelper.getCollection(userDataTable).find(filter).first();
|
||||
if (doc == null) {
|
||||
return false;
|
||||
}
|
||||
mongoCollectionHelper.deleteDocument(userDataTable, doc);
|
||||
return true;
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to delete specific user data from the database", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
|
||||
try {
|
||||
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);
|
||||
|
||||
for (Document doc : iterable) {
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(
|
||||
Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId()
|
||||
);
|
||||
if (timestamp.isAfter(within)) {
|
||||
mongoCollectionHelper.deleteDocument(userDataTable, doc);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to rotate latest snapshot from the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try {
|
||||
Document doc = new Document("player_uuid", user.getUuid())
|
||||
.append("version_uuid", data.getId())
|
||||
.append("timestamp", data.getTimestamp().toInstant().toEpochMilli())
|
||||
.append("save_cause", data.getSaveCause().name())
|
||||
.append("pinned", data.isPinned())
|
||||
.append("data", new Binary(data.asBytes(plugin)));
|
||||
mongoCollectionHelper.insertDocument(userDataTable, doc);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to set user data in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try {
|
||||
Document doc = new Document("player_uuid", user.getUuid()).append("version_uuid", data.getId());
|
||||
Bson updates = Updates.combine(
|
||||
Updates.set("save_cause", data.getSaveCause().name()),
|
||||
Updates.set("pinned", data.isPinned()),
|
||||
Updates.set("data", new Binary(data.asBytes(plugin)))
|
||||
);
|
||||
mongoCollectionHelper.updateDocument(userDataTable, doc, updates);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to update snapshot in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public void wipeDatabase() {
|
||||
try {
|
||||
mongoCollectionHelper.deleteCollection(usersTable);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to wipe the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
if (mongoConnectionHandler != null) {
|
||||
mongoConnectionHandler.closeConnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,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));
|
||||
@@ -218,6 +221,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) {
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
/*
|
||||
* 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.database;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.*;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static net.william278.husksync.config.Settings.DatabaseSettings;
|
||||
|
||||
public class PostgresDatabase extends Database {
|
||||
|
||||
private static final String DATA_POOL_NAME = "HuskSyncHikariPool";
|
||||
private final String flavor;
|
||||
private final String driverClass;
|
||||
private HikariDataSource dataSource;
|
||||
|
||||
public PostgresDatabase(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
|
||||
final Type type = plugin.getSettings().getDatabase().getType();
|
||||
this.flavor = type.getProtocol();
|
||||
this.driverClass = "org.postgresql.Driver";
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@NotNull
|
||||
private Connection getConnection() throws SQLException {
|
||||
if (dataSource == null) {
|
||||
throw new IllegalStateException("The database has not been initialized");
|
||||
}
|
||||
return dataSource.getConnection();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public void initialize() throws IllegalStateException {
|
||||
// Initialize the Hikari pooled connection
|
||||
final DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
|
||||
dataSource = new HikariDataSource();
|
||||
dataSource.setDriverClassName(driverClass);
|
||||
dataSource.setJdbcUrl(String.format("jdbc:%s://%s:%s/%s%s",
|
||||
flavor,
|
||||
credentials.getHost(),
|
||||
credentials.getPort(),
|
||||
credentials.getDatabase(),
|
||||
credentials.getParameters()
|
||||
));
|
||||
|
||||
// Authenticate with the database
|
||||
dataSource.setUsername(credentials.getUsername());
|
||||
dataSource.setPassword(credentials.getPassword());
|
||||
|
||||
// Set connection pool options
|
||||
final DatabaseSettings.PoolSettings pool = plugin.getSettings().getDatabase().getConnectionPool();
|
||||
dataSource.setMaximumPoolSize(pool.getMaximumPoolSize());
|
||||
dataSource.setMinimumIdle(pool.getMinimumIdle());
|
||||
dataSource.setMaxLifetime(pool.getMaximumLifetime());
|
||||
dataSource.setKeepaliveTime(pool.getKeepaliveTime());
|
||||
dataSource.setConnectionTimeout(pool.getConnectionTimeout());
|
||||
dataSource.setPoolName(DATA_POOL_NAME);
|
||||
|
||||
// Set additional connection pool properties
|
||||
final Properties properties = new Properties();
|
||||
properties.putAll(
|
||||
Map.of("cachePrepStmts", "true",
|
||||
"prepStmtCacheSize", "250",
|
||||
"prepStmtCacheSqlLimit", "2048",
|
||||
"useServerPrepStmts", "true",
|
||||
"useLocalSessionState", "true",
|
||||
"useLocalTransactionState", "true"
|
||||
));
|
||||
properties.putAll(
|
||||
Map.of(
|
||||
"rewriteBatchedStatements", "true",
|
||||
"cacheResultSetMetadata", "true",
|
||||
"cacheServerConfiguration", "true",
|
||||
"elideSetAutoCommits", "true",
|
||||
"maintainTimeStats", "false")
|
||||
);
|
||||
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));
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
for (String tableCreationStatement : databaseSchema) {
|
||||
statement.execute(tableCreationStatement);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new IllegalStateException("Failed to create database tables. Please ensure you are running PostgreSQL " +
|
||||
"and that your connecting user account has privileges to create tables.", e);
|
||||
}
|
||||
} catch (SQLException | IOException e) {
|
||||
throw new IllegalStateException("Failed to establish a connection to the PostgreSQL database. " +
|
||||
"Please check the supplied database credentials in the config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public void ensureUser(@NotNull User user) {
|
||||
getUser(user.getUuid()).ifPresentOrElse(
|
||||
existingUser -> {
|
||||
if (!existingUser.getUsername().equals(user.getUsername())) {
|
||||
// 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=?;"""))) {
|
||||
|
||||
statement.setString(1, user.getUsername());
|
||||
statement.setObject(2, existingUser.getUuid());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
plugin.log(Level.INFO, "Updated " + user.getUsername() + "'s name in the database (" + existingUser.getUsername() + " -> " + user.getUsername() + ")");
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
// Insert new player data into the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO %users_table% (uuid,username)
|
||||
VALUES (?,?);"""))) {
|
||||
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.setString(2, user.getUsername());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
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=?;"""))) {
|
||||
|
||||
statement.setObject(1, uuid);
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return Optional.of(new User((UUID) resultSet.getObject("uuid"),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user from uuid from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
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=?;"""))) {
|
||||
statement.setString(1, username);
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return Optional.of(new User((UUID) resultSet.getObject("uuid"),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user by name from the database", e);
|
||||
}
|
||||
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
|
||||
LIMIT 1;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final UUID versionUuid = (UUID) resultSet.getObject("version_uuid");
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(
|
||||
resultSet.getTimestamp("timestamp").toInstant(), TimeZone.getDefault().toZoneId()
|
||||
);
|
||||
final byte[] dataByteArray = resultSet.getBytes("data");
|
||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
@NotNull
|
||||
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
|
||||
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;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
final UUID versionUuid = (UUID) resultSet.getObject("version_uuid");
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(
|
||||
resultSet.getTimestamp("timestamp").toInstant(), TimeZone.getDefault().toZoneId()
|
||||
);
|
||||
final byte[] dataByteArray = resultSet.getBytes("data");
|
||||
retrievedData.add(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
return retrievedData;
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
}
|
||||
return retrievedData;
|
||||
}
|
||||
|
||||
@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
|
||||
LIMIT 1;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.setObject(2, versionUuid);
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(
|
||||
resultSet.getTimestamp("timestamp").toInstant(), TimeZone.getDefault().toZoneId()
|
||||
);
|
||||
final byte[] dataByteArray = resultSet.getBytes("data");
|
||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch specific user data by UUID from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateSnapshots(@NotNull User user) {
|
||||
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
|
||||
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
|
||||
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
|
||||
if (unpinnedUserData.size() > maxSnapshots) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
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(unpinnedUserData.size() - maxSnapshots))))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to prune user data from the database", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
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=?;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.setObject(2, versionUuid);
|
||||
return statement.executeUpdate() > 0;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to delete specific user data from the database", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
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
|
||||
LIMIT 1
|
||||
);"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.setObject(2, user.getUuid());
|
||||
statement.setTimestamp(3, Timestamp.from(within.toInstant()));
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to delete a user's data from the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
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)
|
||||
VALUES (?,?,?,?,?,?);"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.setObject(2, data.getId());
|
||||
statement.setTimestamp(3, Timestamp.from(data.getTimestamp().toInstant()));
|
||||
statement.setString(4, data.getSaveCause().name());
|
||||
statement.setBoolean(5, data.isPinned());
|
||||
statement.setBytes(6, data.asBytes(plugin));
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to set user data in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
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;"""))) {
|
||||
statement.setString(1, data.getSaveCause().name());
|
||||
statement.setBoolean(2, data.isPinned());
|
||||
statement.setBytes(3, data.asBytes(plugin));
|
||||
statement.setObject(4, user.getUuid());
|
||||
statement.setObject(5, data.getId());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to pin user data in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void wipeDatabase() {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
statement.executeUpdate(formatStatementTables("DELETE FROM %user_data_table%;"));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to wipe the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
if (dataSource != null) {
|
||||
if (!dataSource.isClosed()) {
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.database.mongo;
|
||||
|
||||
import com.mongodb.client.MongoCollection;
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class MongoCollectionHelper {
|
||||
private final MongoConnectionHandler database;
|
||||
|
||||
/**
|
||||
* Initialize the collection helper
|
||||
*
|
||||
* @param database Instance of {@link MongoConnectionHandler}
|
||||
*/
|
||||
public MongoCollectionHelper(@NotNull MongoConnectionHandler database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a collection
|
||||
*
|
||||
* @param collectionName the collection name
|
||||
*/
|
||||
public void createCollection(@NotNull String collectionName) {
|
||||
database.getDatabase().createCollection(collectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a collection
|
||||
*
|
||||
* @param collectionName the collection name
|
||||
*/
|
||||
public void deleteCollection(@NotNull String collectionName) {
|
||||
database.getDatabase().getCollection(collectionName).drop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a collection
|
||||
*
|
||||
* @param collectionName the collection name
|
||||
* @return MongoCollection<Document>
|
||||
*/
|
||||
public MongoCollection<Document> getCollection(@NotNull String collectionName) {
|
||||
return database.getDatabase().getCollection(collectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a document to a collection
|
||||
*
|
||||
* @param collectionName collection to add to
|
||||
* @param document Document to add
|
||||
*/
|
||||
public void insertDocument(@NotNull String collectionName, @NotNull Document document) {
|
||||
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
||||
collection.insertOne(document);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a document
|
||||
*
|
||||
* @param collectionName collection the document is in
|
||||
* @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);
|
||||
collection.updateOne(document, updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a document
|
||||
*
|
||||
* @param collectionName collection the document is in
|
||||
* @param document filter to remove
|
||||
*/
|
||||
public void deleteDocument(@NotNull String collectionName, @NotNull Document document) {
|
||||
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
||||
collection.deleteOne(document);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.database.mongo;
|
||||
|
||||
import com.mongodb.ConnectionString;
|
||||
import com.mongodb.MongoClientSettings;
|
||||
import com.mongodb.client.MongoClient;
|
||||
import com.mongodb.client.MongoClients;
|
||||
import com.mongodb.client.MongoDatabase;
|
||||
import lombok.Getter;
|
||||
import org.bson.UuidRepresentation;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Getter
|
||||
public class MongoConnectionHandler {
|
||||
private final MongoClient mongoClient;
|
||||
private final MongoDatabase database;
|
||||
|
||||
/**
|
||||
* Initiate a connection to a Mongo Server
|
||||
*
|
||||
* @param uri The connection string
|
||||
*/
|
||||
public MongoConnectionHandler(@NotNull ConnectionString uri, @NotNull String databaseName) {
|
||||
try {
|
||||
final MongoClientSettings settings = MongoClientSettings.builder()
|
||||
.applyConnectionString(uri)
|
||||
.uuidRepresentation(UuidRepresentation.STANDARD)
|
||||
.build();
|
||||
|
||||
this.mongoClient = MongoClients.create(settings);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the connection with the database
|
||||
*/
|
||||
public void closeConnection() {
|
||||
if (this.mongoClient != null) {
|
||||
this.mongoClient.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface Cancellable extends Event {
|
||||
|
||||
default boolean isCancelled() {
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface PreSyncEvent extends PlayerEvent {
|
||||
public interface PreSyncEvent extends PlayerEvent, Cancellable {
|
||||
|
||||
@NotNull
|
||||
DataSnapshot.Packed getData();
|
||||
|
||||
@@ -184,7 +184,7 @@ public class PlanHook {
|
||||
public String getHealth(@NotNull UUID uuid) {
|
||||
return getLatestSnapshot(uuid)
|
||||
.flatMap(DataHolder::getHealth)
|
||||
.map(health -> String.format("%s / %s", health.getHealth(), health.getMaxHealth()))
|
||||
.map(health -> String.format("%s", health.getHealth()))
|
||||
.orElse(UNKNOWN_STRING);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.william278.husksync.config.Settings.SynchronizationSettings.SaveOnDeathSettings;
|
||||
|
||||
@@ -54,7 +53,7 @@ public abstract class EventListener {
|
||||
return;
|
||||
}
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
plugin.getDataSyncer().setUserData(user);
|
||||
plugin.getDataSyncer().syncApplyUserData(user);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,7 +66,7 @@ public abstract class EventListener {
|
||||
return;
|
||||
}
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
plugin.getDataSyncer().saveUserData(user);
|
||||
plugin.getDataSyncer().syncSaveUserData(user);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,7 +80,7 @@ public abstract class EventListener {
|
||||
}
|
||||
usersInWorld.stream()
|
||||
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
|
||||
.forEach(user -> plugin.getDatabase().addSnapshot(
|
||||
.forEach(user -> plugin.getDataSyncer().saveData(
|
||||
user, user.createSnapshot(DataSnapshot.SaveCause.WORLD_SAVE)
|
||||
));
|
||||
}
|
||||
@@ -101,29 +100,24 @@ public abstract class EventListener {
|
||||
|
||||
final DataSnapshot.Packed snapshot = user.createSnapshot(DataSnapshot.SaveCause.DEATH);
|
||||
snapshot.edit(plugin, (data -> data.getInventory().ifPresent(inventory -> inventory.setContents(items))));
|
||||
plugin.getDatabase().addSnapshot(user, snapshot);
|
||||
plugin.getDataSyncer().saveData(user, snapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a player event should be canceled
|
||||
*
|
||||
* @param userUuid The UUID of the user to check
|
||||
* @return Whether the event should be canceled
|
||||
*/
|
||||
protected final boolean cancelPlayerEvent(@NotNull UUID userUuid) {
|
||||
return plugin.isDisabling() || plugin.isLocked(userUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the plugin disabling
|
||||
*/
|
||||
public final void handlePluginDisable() {
|
||||
public void handlePluginDisable() {
|
||||
// Save for all online players
|
||||
plugin.getOnlineUsers().stream()
|
||||
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
|
||||
.forEach(user -> {
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
plugin.getDatabase().addSnapshot(user, user.createSnapshot(DataSnapshot.SaveCause.SERVER_SHUTDOWN));
|
||||
plugin.getDataSyncer().saveData(
|
||||
user,
|
||||
user.createSnapshot(DataSnapshot.SaveCause.SERVER_SHUTDOWN),
|
||||
(saved, data) -> plugin.getRedisManager().clearUserData(saved)
|
||||
);
|
||||
});
|
||||
|
||||
// Close outstanding connections
|
||||
@@ -168,7 +162,6 @@ public abstract class EventListener {
|
||||
return Map.entry(name().toLowerCase(), defaultPriority.name());
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@NotNull
|
||||
public static Map<String, String> getDefaults() {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Interface for doing stuff with locked users or when the plugin is disabled
|
||||
*/
|
||||
public interface LockedHandler {
|
||||
|
||||
/**
|
||||
* Get if a command should be disabled while the user is locked
|
||||
*/
|
||||
default boolean isCommandDisabled(@NotNull String label) {
|
||||
final List<String> blocked = getPlugin().getSettings().getSynchronization().getBlacklistedCommandsWhileLocked();
|
||||
return blocked.contains("*") || blocked.contains(label.toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a player event should be canceled
|
||||
*
|
||||
* @param userUuid The UUID of the user to check
|
||||
* @return Whether the event should be canceled
|
||||
*/
|
||||
default boolean cancelPlayerEvent(@NotNull UUID userUuid) {
|
||||
return getPlugin().isDisabling() || getPlugin().isLocked(userUuid);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
HuskSync getPlugin();
|
||||
|
||||
default void onLoad() {
|
||||
|
||||
}
|
||||
|
||||
default void onEnable() {
|
||||
|
||||
}
|
||||
|
||||
default void onDisable() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -158,10 +158,16 @@ public class RedisManager extends JedisPubSub {
|
||||
final RedisMessage redisMessage = RedisMessage.fromJson(plugin, message);
|
||||
switch (messageType) {
|
||||
case UPDATE_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
|
||||
user -> user.applySnapshot(
|
||||
DataSnapshot.deserialize(plugin, redisMessage.getPayload()),
|
||||
DataSnapshot.UpdateCause.UPDATED
|
||||
)
|
||||
user -> {
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
try {
|
||||
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(
|
||||
user -> RedisMessage.create(
|
||||
@@ -174,7 +180,13 @@ public class RedisManager extends JedisPubSub {
|
||||
redisMessage.getTargetUuid()
|
||||
);
|
||||
if (future != null) {
|
||||
future.complete(Optional.of(DataSnapshot.deserialize(plugin, redisMessage.getPayload())));
|
||||
try {
|
||||
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload());
|
||||
future.complete(Optional.of(data));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred returning user data from Redis", e);
|
||||
future.complete(Optional.empty());
|
||||
}
|
||||
pendingRequests.remove(redisMessage.getTargetUuid());
|
||||
}
|
||||
}
|
||||
@@ -255,19 +267,36 @@ public class RedisManager extends JedisPubSub {
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
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));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred clearing user data on Redis", e);
|
||||
}
|
||||
}
|
||||
|
||||
@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),
|
||||
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.getUsername(), 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.getUsername(),
|
||||
checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT, checkedOut ? "to" : "from", key));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred setting checkout to", e);
|
||||
}
|
||||
@@ -395,7 +424,12 @@ 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ 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 org.jetbrains.annotations.NotNull;
|
||||
@@ -34,6 +36,8 @@ public class RedisMessage implements Adaptable {
|
||||
|
||||
@SerializedName("target_uuid")
|
||||
private UUID targetUuid;
|
||||
@Getter
|
||||
@Setter
|
||||
@SerializedName("payload")
|
||||
private byte[] payload;
|
||||
|
||||
@@ -72,14 +76,6 @@ public class RedisMessage implements Adaptable {
|
||||
this.targetUuid = targetUuid;
|
||||
}
|
||||
|
||||
public byte[] getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
public void setPayload(byte[] payload) {
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
|
||||
UPDATE_USER_DATA,
|
||||
|
||||
@@ -22,16 +22,23 @@ package net.william278.husksync.sync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.api.HuskSyncAPI;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import net.william278.husksync.util.Task;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Handles the synchronization of data when a player changes servers or logs in
|
||||
@@ -74,18 +81,70 @@ 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 {@link DataSnapshot.Packed user's data snapshot} to the database,
|
||||
* first firing the {@link net.william278.husksync.event.DataSaveEvent}. This will not update data on Redis.
|
||||
*
|
||||
* @param user the user to save the data for
|
||||
* @param data the data to save
|
||||
* @param after a consumer to run after data has been saved. Will be run async (off the main thread).
|
||||
* @apiNote Data will not be saved if the {@link net.william278.husksync.event.DataSaveEvent} is canceled.
|
||||
* Note that this method can also edit the data before saving it.
|
||||
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will <b>not</b> be fired if
|
||||
* {@link DataSnapshot.SaveCause#fireDataSaveEvent()} is {@code false} (e.g., with the SERVER_SHUTDOWN cause).
|
||||
* @since 3.3.2
|
||||
*/
|
||||
@Blocking
|
||||
public void saveData(@NotNull User user, @NotNull DataSnapshot.Packed data,
|
||||
@Nullable BiConsumer<User, DataSnapshot.Packed> after) {
|
||||
if (!data.getSaveCause().fireDataSaveEvent()) {
|
||||
addSnapshotToDatabase(user, data, after);
|
||||
return;
|
||||
}
|
||||
plugin.fireEvent(
|
||||
plugin.getDataSaveEvent(user, data),
|
||||
(event) -> addSnapshotToDatabase(user, data, after)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a {@link DataSnapshot.Packed user's data snapshot} to the database,
|
||||
* first firing the {@link net.william278.husksync.event.DataSaveEvent}. This will not update data on Redis.
|
||||
*
|
||||
* @param user the user to save the data for
|
||||
* @param data the data to save
|
||||
* @apiNote Data will not be saved if the {@link net.william278.husksync.event.DataSaveEvent} is canceled.
|
||||
* Note that this method can also edit the data before saving it.
|
||||
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will <b>not</b> be fired if
|
||||
* {@link DataSnapshot.SaveCause#fireDataSaveEvent()} is {@code false} (e.g., with the SERVER_SHUTDOWN cause).
|
||||
* @since 3.3.3
|
||||
*/
|
||||
public void saveData(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
saveData(user, data, null);
|
||||
}
|
||||
|
||||
// Adds a snapshot to the database and runs the after consumer
|
||||
@Blocking
|
||||
private void addSnapshotToDatabase(@NotNull User user, @NotNull DataSnapshot.Packed data,
|
||||
@Nullable BiConsumer<User, DataSnapshot.Packed> after) {
|
||||
getDatabase().addSnapshot(user, data);
|
||||
if (after != null) {
|
||||
after.accept(user, data);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculates the max attempts the system should listen for user data for based on the latency value
|
||||
private long getMaxListenAttempts() {
|
||||
@@ -98,10 +157,15 @@ public abstract class DataSyncer {
|
||||
// Set a user's data from the database, or set them as a new user
|
||||
@ApiStatus.Internal
|
||||
protected void setUserFromDatabase(@NotNull OnlineUser user) {
|
||||
plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse(
|
||||
try {
|
||||
getDatabase().getLatestSnapshot(user).ifPresentOrElse(
|
||||
snapshot -> user.applySnapshot(snapshot, DataSnapshot.UpdateCause.SYNCHRONIZED),
|
||||
() -> 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);
|
||||
user.completeSync(false, DataSnapshot.UpdateCause.SYNCHRONIZED, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
// Continuously listen for data from Redis
|
||||
@@ -139,6 +203,16 @@ public abstract class DataSyncer {
|
||||
task.get().run();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected RedisManager getRedis() {
|
||||
return plugin.getRedisManager();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected Database getDatabase() {
|
||||
return plugin.getDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the different available default modes of {@link DataSyncer}
|
||||
*
|
||||
|
||||
@@ -35,11 +35,11 @@ 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
|
||||
if (!plugin.getRedisManager().getUserServerSwitch(user)) {
|
||||
if (!getRedis().getUserServerSwitch(user)) {
|
||||
this.setUserFromDatabase(user);
|
||||
return;
|
||||
}
|
||||
@@ -47,7 +47,7 @@ public class DelayDataSyncer extends DataSyncer {
|
||||
// Listen for the data to be updated
|
||||
this.listenForRedisData(
|
||||
user,
|
||||
() -> plugin.getRedisManager().getUserData(user).map(data -> {
|
||||
() -> getRedis().getUserData(user).map(data -> {
|
||||
user.applySnapshot(data, DataSnapshot.UpdateCause.SYNCHRONIZED);
|
||||
return true;
|
||||
}).orElse(false)
|
||||
@@ -58,12 +58,13 @@ public class DelayDataSyncer extends DataSyncer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveUserData(@NotNull OnlineUser user) {
|
||||
public void syncSaveUserData(@NotNull OnlineUser onlineUser) {
|
||||
plugin.runAsync(() -> {
|
||||
plugin.getRedisManager().setUserServerSwitch(user);
|
||||
final DataSnapshot.Packed data = user.createSnapshot(DataSnapshot.SaveCause.DISCONNECT);
|
||||
plugin.getRedisManager().setUserData(user, data, RedisKeyType.TTL_10_SECONDS);
|
||||
plugin.getDatabase().addSnapshot(user, data);
|
||||
getRedis().setUserServerSwitch(onlineUser);
|
||||
saveData(
|
||||
onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
|
||||
(user, data) -> getRedis().setUserData(user, data, RedisKeyType.TTL_10_SECONDS)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -33,38 +33,39 @@ public class LockstepDataSyncer extends DataSyncer {
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
plugin.getRedisManager().clearUsersCheckedOutOnServer();
|
||||
getRedis().clearUsersCheckedOutOnServer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
plugin.getRedisManager().clearUsersCheckedOutOnServer();
|
||||
getRedis().clearUsersCheckedOutOnServer();
|
||||
}
|
||||
|
||||
// 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 (plugin.getRedisManager().getUserCheckedOut(user).isEmpty()) {
|
||||
plugin.getRedisManager().setUserCheckedOut(user, true);
|
||||
plugin.getRedisManager().getUserData(user).ifPresentOrElse(
|
||||
if (getRedis().getUserCheckedOut(user).isPresent()) {
|
||||
return false;
|
||||
}
|
||||
getRedis().setUserCheckedOut(user, true);
|
||||
getRedis().getUserData(user).ifPresentOrElse(
|
||||
data -> user.applySnapshot(data, DataSnapshot.UpdateCause.SYNCHRONIZED),
|
||||
() -> this.setUserFromDatabase(user)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveUserData(@NotNull OnlineUser user) {
|
||||
plugin.runAsync(() -> {
|
||||
final DataSnapshot.Packed data = user.createSnapshot(DataSnapshot.SaveCause.DISCONNECT);
|
||||
plugin.getRedisManager().setUserData(user, data, RedisKeyType.TTL_1_YEAR);
|
||||
plugin.getRedisManager().setUserCheckedOut(user, false);
|
||||
plugin.getDatabase().addSnapshot(user, data);
|
||||
});
|
||||
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().setUserCheckedOut(user, false);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -42,4 +42,6 @@ public final class ConsoleUser implements CommandUser {
|
||||
public boolean hasPermission(@NotNull String permission) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
package net.william278.husksync.user;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import de.themoep.minedown.adventure.MineDownParser;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.william278.husksync.HuskSync;
|
||||
@@ -71,9 +70,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
||||
* @param mineDown the parsed {@link MineDown} to send
|
||||
*/
|
||||
public void sendMessage(@NotNull MineDown mineDown) {
|
||||
sendMessage(mineDown
|
||||
.disable(MineDownParser.Option.SIMPLE_FORMATTING)
|
||||
.replace().toComponent());
|
||||
sendMessage(mineDown.toComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,9 +79,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
||||
* @param mineDown the parsed {@link MineDown} to send
|
||||
*/
|
||||
public void sendActionBar(@NotNull MineDown mineDown) {
|
||||
getAudience().sendActionBar(mineDown
|
||||
.disable(MineDownParser.Option.SIMPLE_FORMATTING)
|
||||
.replace().toComponent());
|
||||
getAudience().sendActionBar(mineDown.toComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +89,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);
|
||||
|
||||
@@ -130,7 +127,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
||||
getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> {
|
||||
if (!isOffline()) {
|
||||
getPlugin().debug(String.format("Applying snapshot (%s) to %s (cause: %s)",
|
||||
snapshot.getShortId(), getUsername(), cause
|
||||
snapshot.getShortId(), getUsername(), cause.getDisplayName()
|
||||
));
|
||||
UserDataHolder.super.applySnapshot(
|
||||
event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin())
|
||||
@@ -150,12 +147,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),
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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 net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
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 Version compatible;
|
||||
|
||||
// Load compatibility file
|
||||
try (InputStream input = getResource(COMPATIBILITY_FILE)) {
|
||||
final CompatibilityConfig compat = new YamlConfigurationStore<>(CompatibilityConfig.class, p).read(input);
|
||||
compatible = Objects.requireNonNull(compat.getCompatibleWith());
|
||||
} catch (Throwable e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to load compatibility config, skipping check.", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check compatibility
|
||||
if (compatible.compareTo(getPlugin().getMinecraftVersion()) != 0) {
|
||||
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(compatible.toString(), getPlugin().getMinecraftVersion().toString()));
|
||||
}
|
||||
}
|
||||
|
||||
InputStream getResource(@NotNull String name);
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
@Configuration
|
||||
record CompatibilityConfig(@NotNull String minecraftVersion) {
|
||||
|
||||
@NotNull
|
||||
public Version getCompatibleWith() {
|
||||
return Version.fromString(minecraftVersion);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,9 +28,12 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.StringJoiner;
|
||||
@@ -80,7 +83,7 @@ public class DataDumper {
|
||||
@NotNull
|
||||
public String toWeb() {
|
||||
try {
|
||||
final URL url = new URL(LOGS_SITE_ENDPOINT);
|
||||
final URL url = URI.create(LOGS_SITE_ENDPOINT).toURL();
|
||||
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setDoOutput(true);
|
||||
@@ -133,16 +136,13 @@ public class DataDumper {
|
||||
*/
|
||||
@NotNull
|
||||
public String toFile() throws IOException {
|
||||
final File filePath = getFilePath();
|
||||
|
||||
// Write the data from #getString to the file using a writer
|
||||
try (final FileWriter writer = new FileWriter(filePath, StandardCharsets.UTF_8, false)) {
|
||||
writer.write(toString());
|
||||
final Path filePath = getFilePath();
|
||||
try (final FileWriter writer = new FileWriter(filePath.toFile(), StandardCharsets.UTF_8, false)) {
|
||||
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);
|
||||
}
|
||||
|
||||
return "~/plugins/HuskSync/dumps/" + filePath.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,8 +152,8 @@ public class DataDumper {
|
||||
* @throws IOException if the prerequisite dumps parent folder could not be created
|
||||
*/
|
||||
@NotNull
|
||||
private File getFilePath() throws IOException {
|
||||
return new File(getDumpsFolder(), getFileName());
|
||||
private Path getFilePath() throws IOException {
|
||||
return getDumpsFolder().resolve(getFileName());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,14 +163,12 @@ public class DataDumper {
|
||||
* @throws IOException if the folder could not be created
|
||||
*/
|
||||
@NotNull
|
||||
private File getDumpsFolder() throws IOException {
|
||||
final File dumpsFolder = new File(plugin.getDataFolder(), "dumps");
|
||||
if (!dumpsFolder.exists()) {
|
||||
if (!dumpsFolder.mkdirs()) {
|
||||
throw new IOException("Failed to create user data dumps folder");
|
||||
private Path getDumpsFolder() throws IOException {
|
||||
final Path dumps = plugin.getConfigDirectory().resolve("dumps");
|
||||
if (!Files.exists(dumps)) {
|
||||
Files.createDirectory(dumps);
|
||||
}
|
||||
}
|
||||
return dumpsFolder;
|
||||
return dumps;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,7 @@ import net.william278.paginedown.PaginatedList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@@ -46,18 +47,19 @@ public class DataSnapshotList {
|
||||
final AtomicInteger snapshotNumber = new AtomicInteger(1);
|
||||
this.paginatedList = PaginatedList.of(snapshots.stream()
|
||||
.map(snapshot -> plugin.getLocales()
|
||||
.getRawLocale("data_list_item",
|
||||
.getRawLocale(!snapshot.isInvalid() ? "data_list_item" : "data_list_item_invalid",
|
||||
getNumberIcon(snapshotNumber.getAndIncrement()),
|
||||
dataOwner.getUsername(),
|
||||
snapshot.getId().toString(),
|
||||
snapshot.getShortId(),
|
||||
snapshot.isPinned() ? "※" : " ",
|
||||
snapshot.getTimestamp().format(DateTimeFormatter
|
||||
.ofPattern("dd/MM/yyyy, HH:mm")),
|
||||
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)),
|
||||
snapshot.getTimestamp().format(DateTimeFormatter
|
||||
.ofPattern("MMM dd yyyy, HH:mm:ss.SSS")),
|
||||
.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.MEDIUM)),
|
||||
snapshot.getSaveCause().getLocale(plugin),
|
||||
String.format("%.2fKiB", snapshot.getFileSize(plugin) / 1024f))
|
||||
String.format("%.2fKiB", snapshot.getFileSize(plugin) / 1024f),
|
||||
snapshot.isInvalid() ? snapshot.getInvalidReason(plugin) : "")
|
||||
.orElse("• " + snapshot.getId())).toList(),
|
||||
plugin.getLocales().getBaseChatList(6)
|
||||
.setHeaderFormat(plugin.getLocales()
|
||||
|
||||
@@ -28,6 +28,7 @@ import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
@@ -61,7 +62,8 @@ public class DataSnapshotOverview {
|
||||
dataOwner.getUsername(), dataOwner.getUuid().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
locales.getLocale("data_manager_timestamp",
|
||||
snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("MMM dd yyyy, HH:mm:ss.SSS")),
|
||||
snapshot.getTimestamp().format(DateTimeFormatter
|
||||
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)),
|
||||
snapshot.getTimestamp().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
if (snapshot.isPinned()) {
|
||||
@@ -75,16 +77,17 @@ public class DataSnapshotOverview {
|
||||
|
||||
// User status data, if present in the snapshot
|
||||
final Optional<Data.Health> health = snapshot.getHealth();
|
||||
final Optional<Data.Attributes> attributes = snapshot.getAttributes();
|
||||
final Optional<Data.Hunger> food = snapshot.getHunger();
|
||||
final Optional<Data.Experience> experience = snapshot.getExperience();
|
||||
final Optional<Data.GameMode> gameMode = snapshot.getGameMode();
|
||||
if (health.isPresent() && food.isPresent() && experience.isPresent() && gameMode.isPresent()) {
|
||||
final Optional<Data.Experience> exp = snapshot.getExperience();
|
||||
final Optional<Data.GameMode> mode = snapshot.getGameMode();
|
||||
if (health.isPresent() && attributes.isPresent() && food.isPresent() && exp.isPresent() && mode.isPresent()) {
|
||||
locales.getLocale("data_manager_status",
|
||||
Integer.toString((int) health.get().getHealth()),
|
||||
Integer.toString((int) health.get().getMaxHealth()),
|
||||
Integer.toString((int) attributes.get().getMaxHealth()),
|
||||
Integer.toString(food.get().getFoodLevel()),
|
||||
Integer.toString(experience.get().getExpLevel()),
|
||||
gameMode.get().getGameMode().toLowerCase(Locale.ENGLISH))
|
||||
Integer.toString(exp.get().getExpLevel()),
|
||||
mode.get().getGameMode().toLowerCase(Locale.ENGLISH))
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ public abstract class LegacyConverter {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public abstract DataSnapshot.Packed convert(@NotNull byte[] data, @NotNull UUID id,
|
||||
public abstract DataSnapshot.Packed convert(byte @NotNull [] data, @NotNull UUID id,
|
||||
@NotNull OffsetDateTime timestamp) throws DataAdapter.AdaptionException;
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.UserDataHolder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@@ -86,7 +88,7 @@ public interface Task extends Runnable {
|
||||
interface Supplier {
|
||||
|
||||
@NotNull
|
||||
Task.Sync getSyncTask(@NotNull Runnable runnable, long delayTicks);
|
||||
Task.Sync getSyncTask(@NotNull Runnable runnable, @Nullable UserDataHolder user, long delayTicks);
|
||||
|
||||
@NotNull
|
||||
Task.Async getAsyncTask(@NotNull Runnable runnable, long delayTicks);
|
||||
@@ -95,8 +97,8 @@ public interface Task extends Runnable {
|
||||
Task.Repeating getRepeatingTask(@NotNull Runnable runnable, long repeatingTicks);
|
||||
|
||||
@NotNull
|
||||
default Task.Sync runSyncDelayed(@NotNull Runnable runnable, long delayTicks) {
|
||||
final Task.Sync task = getSyncTask(runnable, delayTicks);
|
||||
default Task.Sync runSyncDelayed(@NotNull Runnable runnable, @Nullable UserDataHolder user, long delayTicks) {
|
||||
final Task.Sync task = getSyncTask(runnable, user, delayTicks);
|
||||
task.run();
|
||||
return task;
|
||||
}
|
||||
@@ -109,7 +111,12 @@ public interface Task extends Runnable {
|
||||
|
||||
@NotNull
|
||||
default Task.Sync runSync(@NotNull Runnable runnable) {
|
||||
return runSyncDelayed(runnable, 0);
|
||||
return runSyncDelayed(runnable, null, 0);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Task.Sync runSync(@NotNull Runnable runnable, @NotNull UserDataHolder user) {
|
||||
return runSyncDelayed(runnable, user, 0);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
2
common/src/main/resources/compatibility.yml
Normal file
2
common/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: '${minecraft_version}'
|
||||
22
common/src/main/resources/database/postgresql_schema.sql
Normal file
22
common/src/main/resources/database/postgresql_schema.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Create the users table if it does not exist
|
||||
CREATE TABLE IF NOT EXISTS "%users_table%"
|
||||
(
|
||||
uuid uuid NOT NULL UNIQUE,
|
||||
username varchar(16) NOT NULL,
|
||||
|
||||
PRIMARY KEY (uuid)
|
||||
);
|
||||
|
||||
-- Create the user data table if it does not exist
|
||||
CREATE TABLE IF NOT EXISTS "%user_data_table%"
|
||||
(
|
||||
version_uuid uuid NOT NULL UNIQUE,
|
||||
player_uuid uuid NOT NULL,
|
||||
timestamp timestamp NOT NULL,
|
||||
save_cause varchar(32) NOT NULL,
|
||||
pinned boolean NOT NULL DEFAULT FALSE,
|
||||
data bytea NOT NULL,
|
||||
|
||||
PRIMARY KEY (version_uuid, player_uuid),
|
||||
FOREIGN KEY (player_uuid) REFERENCES "%users_table%" (uuid) ON DELETE CASCADE
|
||||
);
|
||||
@@ -21,7 +21,8 @@ locales:
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'и още %1%…'
|
||||
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%&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: '[%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_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%)'
|
||||
@@ -52,6 +53,7 @@ locales:
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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%)'
|
||||
error_no_permission: '[Грешка:](#ff3300) [Нямате право да използвате тази команда](#ff7e5e)'
|
||||
error_console_command_only: '[Грешка:](#ff3300) [Тази команда може да бъде използвана единствено през конзолата](#ff7e5e)'
|
||||
error_in_game_command_only: 'Грешка: Тази команда може да бъде използвана само от играта.'
|
||||
|
||||
@@ -22,6 +22,7 @@ locales:
|
||||
data_manager_advancements_preview_remaining: 'und %1% weitere…'
|
||||
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_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%)'
|
||||
@@ -52,6 +53,7 @@ locales:
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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%)'
|
||||
error_no_permission: '[Fehler:](#ff3300) [Du hast nicht die benötigten Berechtigungen um diesen Befehl auszuführen](#ff7e5e)'
|
||||
error_console_command_only: '[Fehler:](#ff3300) [Dieser Befehl kann nur über die Konsole ausgeführt werden.](#ff7e5e)'
|
||||
error_in_game_command_only: 'Fehler: Dieser Befehl kann nur im Spiel genutzt werden.'
|
||||
|
||||
@@ -21,7 +21,8 @@ locales:
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'and %1% more…'
|
||||
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%&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: '[%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_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%)'
|
||||
@@ -52,6 +53,7 @@ locales:
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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%)'
|
||||
error_no_permission: '[Error:](#ff3300) [You do not have permission to execute this command](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [That command can only be run through console](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: That command can only be used in-game.'
|
||||
|
||||
@@ -21,7 +21,8 @@ locales:
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'y %1% más…'
|
||||
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%&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: '[%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_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%)'
|
||||
@@ -52,6 +53,7 @@ locales:
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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%)'
|
||||
error_no_permission: '[Error:](#ff3300) [No tienes permisos para ejecutar este comando.](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [Este comando solo se puede ejecutar desde la consola.](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: Ese comando solo se puede utilizar desde el juego.'
|
||||
|
||||
65
common/src/main/resources/locales/fr-fr.yml
Normal file
65
common/src/main/resources/locales/fr-fr.yml
Normal file
@@ -0,0 +1,65 @@
|
||||
locales:
|
||||
synchronization_complete: '[⏵ Données synchronisées!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Impossible de synchroniser vos données! Veuillez contacter un administrateur.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0Inventaire de %1%'
|
||||
ender_chest_viewer_menu_title: '&0Coffre de l''Ender de %1%'
|
||||
inventory_viewer_opened: '[Visualisation de l''instantané de](#00fb9a) [%1%](#00fb9a bold)[''s inventaire à partir de ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Visualisation de l''instantané du](#00fb9a) [%1%](#00fb9a bold)[''s coffre de l''Ender à partir de ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Vos données ont été mises à jour!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Échec de la mise à jour de vos données! Veuillez contacter un administrateur.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ Inscription de l''utilisateur complète!](#00fb9a)'
|
||||
data_manager_title: '[Visualisation de l''instantané des données utilisateur](#00fb9a) [%1%](#00fb9a show_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%](#00fb9a bold show_text=&7UUID du joueur:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Horodatage de la version:\n&8Quand les données ont été enregistrées)'
|
||||
data_manager_pinned: '[※ Instantané épinglé](#d8ff2b show_text=&7Épinglé:\n&8Cet instantané des données utilisateur ne sera pas automatiquement supprimé.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Cause de la sauvegarde:\n&8Ce qui a causé l''enregistrement des données)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Serveur:\n&8Nom du serveur sur lequel les données ont été enregistrées)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Taille de l''instantané:\n&8Taille du fichier estimée de l''instantané (en KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Points de vie) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Points de faim) [ʟᴠ](green)[.](gray)[%4%](greenshow_text=&7Niveau XP) [🏹 %5%](dark_aqua show_text=&7Mode de jeu)'
|
||||
data_manager_advancements_statistics: '[⭐ Avancements: %1%](color=#ffc43b-#f5c962show_text=&7Avancements dans lesquels vous avez progressé:\n&8%2%) [⌛ Temps de jeu: %3%ʜʀs](color=#62a9f5-#7ab8fashow_text=&7Temps de jeu en jeu\n&8⚠ Basé sur les statistiques en jeu)\n'
|
||||
data_manager_item_buttons: '[Voir:](gray) [[🪣 Inventaire…]](color=#a17b5f-#f5b98cshow_text=&7Cliquez pour voir run_command=/inventory %1% %2%) [[⌀ Coffre de l''Ender…]](#b649c4-#d254ffshow_text=&7Cliquez pour voir run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Gérer:](gray) [[❌ Supprimer…]](#ff3300 show_text=&7Cliquezpour supprimer cet instantané des données utilisateur.\n&8Cela n''affectera pas les données actuelles de l''utilisateur.\n&#ff3300&⚠ Cette action est irréversible! suggest_command=/husksync:userdata delete%1% %2%) [[⏪ Restaurer…]](#00fb9a show_text=&7Cliquez pour restaurer ces données utilisateur.\n&8Cela définira les données de l''utilisateur sur cet instantané.\n&#ff3300&⚠ Les données actuelles de %1% serontremplacées! suggest_command=/husksync:userdata restore %1% %2%) [[※ Épingler/Détacher…]](#d8ff2bshow_text=&7Cliquez pour épingler ou détacher cet instantané des données utilisateur\n&8Les instantanés épinglés ne seront pas automatiquement supprimés run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[Système:](gray) [[⏷ Export fichier…]](dark_gray show_text=&7Cliquezpour exporter cet instantané des données utilisateur à un fichier.\n&8Les exports de données peuvent être trouvés dans ~/plugins/HuskSync/dumps/run_command=/husksync:userdata dump %1% %2% file) [[☂ Export web…]](dark_grayshow_text=&7Cliquez pour exporter cet instantané des données utilisateur au service mc-logs\n&8Vousobtiendrez une URL contenant les données. run_command=/husksync:userdatadump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'et %1% autres…'
|
||||
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_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%'
|
||||
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%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Aller à la page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: 'déconnexion'
|
||||
save_cause_world_save: 'sauvegarde du monde'
|
||||
save_cause_death: 'mort'
|
||||
save_cause_server_shutdown: 'arrêt du serveur'
|
||||
save_cause_inventory_command: 'commande d''inventaire'
|
||||
save_cause_enderchest_command: 'commande du coffre de l''Ender'
|
||||
save_cause_backup_restore: 'restauration de sauvegarde'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'migration MPDB'
|
||||
save_cause_legacy_migration: 'migration legacy'
|
||||
save_cause_converted_from_v2: 'converti de v2'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| Vous utilisez la dernière version de HuskSync(v%1%).](#00fb9a)'
|
||||
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)'
|
||||
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%)'
|
||||
error_no_permission: '[Erreur:](#ff3300) [Vous n''avez pas la permission d''exécuter cettecommande](#ff7e5e)'
|
||||
error_console_command_only: '[Erreur:](#ff3300) [Cette commande peut seulement être exécutée via la console](#ff7e5e)'
|
||||
error_in_game_command_only: 'Erreur: Cette commande peut uniquement être utilisée en jeu.'
|
||||
error_no_data_to_display: '[Erreur:](#ff3300) [Impossible de trouver des données utilisateur à afficher.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Erreur:](#ff3300) [Impossible de trouver des données utilisateur pour cet UUID de version.](#ff7e5e)'
|
||||
husksync_command_description: 'Gérer le plugin HuskSync'
|
||||
userdata_command_description: 'Voir, gérer & restaurer les données utilisateur des joueurs'
|
||||
inventory_command_description: 'Voir & modifier l''inventaire d''un joueur'
|
||||
enderchest_command_description: 'Voir & modifier le Coffre de l''Ender d''un joueur'
|
||||
@@ -22,6 +22,7 @@ locales:
|
||||
data_manager_advancements_preview_remaining: 'dan %1% lagi…'
|
||||
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_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%)'
|
||||
@@ -52,6 +53,7 @@ locales:
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| Laporan status sistem:](#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%)'
|
||||
error_no_permission: '[Kesalahan:](#ff3300) [Kamu tidak memiliki izin untuk menjalankan perintah ini](#ff7e5e)'
|
||||
error_console_command_only: '[Kesalahan:](#ff3300) [Perintah itu hanya dapat dijalankan melalui konsol](#ff7e5e)'
|
||||
error_in_game_command_only: 'Kesalahan: Perintah itu hanya dapat dijalankan dalam game.'
|
||||
|
||||
@@ -22,6 +22,7 @@ locales:
|
||||
data_manager_advancements_preview_remaining: 'e %1% altro…'
|
||||
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_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%)'
|
||||
@@ -52,6 +53,7 @@ locales:
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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%)'
|
||||
error_no_permission: '[Errore:](#ff3300) [Non hai il permesso di usare questo comando](#ff7e5e)'
|
||||
error_console_command_only: '[Errore:](#ff3300) [Questo comando può essere eseguito solo dalla](#ff7e5e)'
|
||||
error_in_game_command_only: 'Errore: Questo comando può essere utilizzato solo in gioco.'
|
||||
|
||||
@@ -22,6 +22,7 @@ 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_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)'
|
||||
@@ -52,6 +53,7 @@ locales:
|
||||
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: そのコマンドはゲーム内でしか使えません。'
|
||||
|
||||
@@ -22,6 +22,7 @@ locales:
|
||||
data_manager_advancements_preview_remaining: '외 %1%개...'
|
||||
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_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)'
|
||||
@@ -52,6 +53,7 @@ locales:
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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%)'
|
||||
error_no_permission: '[오류:](#ff3300) [해당 명령어를 사용할 권한이 없습니다.](#ff7e5e)'
|
||||
error_console_command_only: '[오류:](#ff3300) [해당 명령어는 콘솔을 통해서만 사용할 수 있습니다.](#ff7e5e)'
|
||||
error_in_game_command_only: '오류: 해당 명령어는 게임 내부에서만 사용할 수 있습니다.'
|
||||
|
||||
@@ -22,6 +22,7 @@ locales:
|
||||
data_manager_advancements_preview_remaining: 'en %1% meer…'
|
||||
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_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%)'
|
||||
@@ -52,6 +53,7 @@ locales:
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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%)'
|
||||
error_no_permission: '[Error:](#ff3300) [Je hebt geen toestemming om deze opdracht uit te voeren](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [Dat command kan alleen via de console worden uitgevoerd](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: Dat command kan alleen in-game worden gebruikt.'
|
||||
|
||||
@@ -21,7 +21,8 @@ locales:
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'e %1% mais…'
|
||||
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%&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: '[%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_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%)'
|
||||
@@ -52,6 +53,7 @@ locales:
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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%)'
|
||||
error_no_permission: '[Error:](#ff3300) [Você não tem permissão para executar este comando](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [Esse comando só pode ser executado através do console](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: Esse comando só pode ser usado dentro do jogo.'
|
||||
|
||||
@@ -22,6 +22,7 @@ locales:
|
||||
data_manager_advancements_preview_remaining: 'и еще %1%…'
|
||||
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_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)'
|
||||
@@ -52,6 +53,7 @@ locales:
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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%)'
|
||||
error_no_permission: '[Ошибка:](#ff3300) [У вас недостаточно прав для выполнения данной команды.](#ff7e5e)'
|
||||
error_console_command_only: '[Ошибка:](#ff3300) [Данная команда может быть выполнена только из консоли.](#ff7e5e)'
|
||||
error_in_game_command_only: 'Ошибка: Данная команда может быть выполнена только в игре.'
|
||||
|
||||
@@ -22,6 +22,7 @@ locales:
|
||||
data_manager_advancements_preview_remaining: 've %1% daha fazla…'
|
||||
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_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%)'
|
||||
@@ -52,6 +53,7 @@ locales:
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| Sistem durumu raporu:](#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%)'
|
||||
error_no_permission: '[Hata:](#ff3300) [Bu komutu gerçekleştirmek için izniniz yok](#ff7e5e)'
|
||||
error_console_command_only: '[Hata:](#ff3300) [Bu komut yalnızca konsoldan çalıştırılabilir](#ff7e5e)'
|
||||
error_in_game_command_only: 'Hata: Bu komut yalnızca oyun içinde kullanılabilir.'
|
||||
|
||||
@@ -21,7 +21,8 @@ locales:
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'and %1% more…'
|
||||
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%&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: '[%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_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%)'
|
||||
@@ -52,6 +53,7 @@ locales:
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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%)'
|
||||
error_no_permission: '[Помилка:](#ff3300) [Ввас немає дозволу на використання цієї команди](#ff7e5e)'
|
||||
error_console_command_only: '[Помилка:](#ff3300) [Ця команда може бути використана лише з допомогою %1% консолі](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: That command can only be used in-game.'
|
||||
|
||||
@@ -22,6 +22,7 @@ locales:
|
||||
data_manager_advancements_preview_remaining: '以及其他 %1%…'
|
||||
data_list_title: '[%1%的玩家数据备份:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7玩家数据备份 %2%&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=&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_deleted: '[❌ 成功删除玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&7%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本UUID:\n&7%2%)'
|
||||
data_restored: '[⏪ 成功恢复玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&7%2%)[的数据备份](#00fb9a) [%3%.](#00fb9a show_text=&7备份版本UUID:\n&7%4%)'
|
||||
data_pinned: '[※ 成功置顶玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本UUID:\n&8%2%)'
|
||||
@@ -52,6 +53,7 @@ locales:
|
||||
system_status_header: '[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) [该命令只能在控制台中运行](#ff7e5e)'
|
||||
error_in_game_command_only: '错误: 该命令只能在游戏中使用.'
|
||||
|
||||
@@ -7,13 +7,13 @@ locales:
|
||||
ender_chest_viewer_opened: '[查看](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的終界箱快照資料](#00fb9a)'
|
||||
data_update_complete: '[🔔 你的資料已更新!](#00fb9a)'
|
||||
data_update_failed: '[🔔 無法更新您的資料! 請聯繫管理員](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
user_registration_complete: '[⭐ 使用者註冊成功!](#00fb9a)'
|
||||
data_manager_title: '[查看](#00fb9a) [%3%](#00fb9a bold show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [:](#00fb9a)'
|
||||
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_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
|
||||
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=&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'
|
||||
data_manager_item_buttons: '[查看:](gray) [[🪣 背包…]](color=#a17b5f-#f5b98c show_text=&7點擊查看 run_command=/inventory %1% %2%) [[⌀ 終界箱…]](#b649c4-#d254ff show_text=&7點擊查看 run_command=/enderchest %1% %2%)'
|
||||
@@ -21,7 +21,8 @@ locales:
|
||||
data_manager_system_buttons: '[系統:](gray) [[⏷ 本地轉存…]](dark_gray show_text=&7點擊將此玩家資料快照轉存到本地文件中\n&8轉存的資料可以在以下路徑找到 ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ 雲端轉存…]](dark_gray show_text=&7點擊將此玩家資料快照轉存到 mc-logs 服務\n&8您將獲得一個包含資料的 URL. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '還有 %1% …'
|
||||
data_list_title: '[%1% 的玩家資料快照:](#00fb9a) [(%2%-%3% 共](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%&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: '[%1%](gray show_text=&7玩家資料快照 %2%\n&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版本時間戳:\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快照大小:\n&8快照的預估檔案大小(KiB) run_command=/userdata view %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_deleted: '[❌ 成功刪除:](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%)'
|
||||
data_restored: '[⏪ 成功將玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&8%2%)[的資料恢復為 快照:](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ 成功標記](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%)'
|
||||
@@ -35,29 +36,30 @@ 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_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) [| 發現可用的新版本: 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)'
|
||||
error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 配置和語言文件已重新加載。](#00fb9a)\n[⚠ 確保所有伺服器上的配置文件都是最新的!](#00fb9a)\n[重啟後配置變更才會生效。](#00fb9a italic)'
|
||||
system_status_header: '[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) [該指令只能透過 控制台 執行](#ff7e5e)'
|
||||
error_in_game_command_only: '[錯誤:](#ff3300) [該指令只能在遊戲內執行](#ff7e5e)'
|
||||
error_no_data_to_display: '[錯誤:](#ff3300) [找不到任何可顯示的用戶資訊.](#ff7e5e)'
|
||||
error_no_data_to_display: '[錯誤:](#ff3300) [找不到任何可顯示的用戶資料.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[錯誤:](#ff3300) [找不到正確的 Version UUID.](#ff7e5e)'
|
||||
husksync_command_description: 'Manage the HuskSync plugin'
|
||||
userdata_command_description: 'View, manage & restore player userdata'
|
||||
inventory_command_description: 'View & edit a player''s inventory'
|
||||
enderchest_command_description: 'View & edit a player''s Ender Chest'
|
||||
husksync_command_description: '管理 HuskSync 插件'
|
||||
userdata_command_description: '查看、管理和還原玩家資料'
|
||||
inventory_command_description: '查看和編輯玩家的物品欄'
|
||||
enderchest_command_description: '查看和編輯玩家的終界箱'
|
||||
|
||||
@@ -4,11 +4,20 @@ Consult the Javadocs for more information. Please note that carrying out expensi
|
||||
|
||||
## Bukkit Platform Events
|
||||
> **Tip:** Don't forget to register your listener when listening for these event calls.
|
||||
>
|
||||
|
||||
| Bukkit Event class | Cancellable | Description |
|
||||
|---------------------------|:-----------:|---------------------------------------------------------------------------------------------|
|
||||
| `BukkitDataSaveEvent` | ✅ | Called when player data snapshot is created, saved and cached due to a DataSaveCause |
|
||||
| `BukkitPreSyncEvent` | ✅ | Called before a player has their data updated from the cache or database, just after login |
|
||||
| `BukkitSyncCompleteEvent` | ❌ | Called once a player has completed their data synchronization on login successfully† |
|
||||
|
||||
## Fabric Platform Callbacks
|
||||
> Access the callback via the static EVENT field in each interface class.
|
||||
|
||||
| Fabric Callback | Cancellable | Description |
|
||||
|------------------------------|:-----------:|---------------------------------------------------------------------------------------------|
|
||||
| `FabricDataSaveCallback` | ✅ | Called when player data snapshot is created, saved and cached due to a DataSaveCause |
|
||||
| `FabricPreSyncCallback` | ✅ | Called before a player has their data updated from the cache or database, just after login |
|
||||
| `FabricSyncCompleteCallback` | ❌ | Called once a player has completed their data synchronization on login successfully† |
|
||||
|
||||
†This can also fire when a user's data is updated while the player is logged in; i.e., when an admin rolls back the user, updates their inventory or Ender Chest through the respective commands, or when an API call is made forcing the user to have their data updated.
|
||||
|
||||
24
docs/API.md
24
docs/API.md
@@ -17,6 +17,7 @@ The HuskSync API shares version numbering with the plugin itself for consistency
|
||||
The HuskSync API is available for the following platforms:
|
||||
|
||||
* `bukkit` - Bukkit, Spigot, Paper, etc. Provides Bukkit API event listeners and adapters to `org.bukkit` objects.
|
||||
* `fabric` - Fabric API for Minecraft. Provides Fabric API event listeners and adapters to `net.minecraft` objects.
|
||||
* `common` - Common API for all platforms.
|
||||
|
||||
|
||||
@@ -31,11 +32,12 @@ The HuskSync API is available for the following platforms:
|
||||
1. [API Introduction](#api-introduction)
|
||||
1. [Setup with Maven](#11-setup-with-maven)
|
||||
2. [Setup with Gradle](#12-setup-with-gradle)
|
||||
2. [Creating a class to interface with the API](#3-creating-a-class-to-interface-with-the-api)
|
||||
3. [Checking if HuskSync is present and creating the hook](#4-checking-if-husksync-is-present-and-creating-the-hook)
|
||||
4. [Getting an instance of the API](#5-getting-an-instance-of-the-api)
|
||||
5. [CompletableFuture and Optional basics](#6-completablefuture-and-optional-basics)
|
||||
6. [Next steps](#7-next-steps)
|
||||
2. [Adding HuskSync as a dependency](#2-adding-husksync-as-a-dependency)
|
||||
3. [Creating a class to interface with the API](#3-creating-a-class-to-interface-with-the-api)
|
||||
4. [Checking if HuskSync is present and creating the hook](#4-checking-if-husksync-is-present-and-creating-the-hook)
|
||||
5. [Getting an instance of the API](#5-getting-an-instance-of-the-api)
|
||||
6. [CompletableFuture and Optional basics](#6-completablefuture-and-optional-basics)
|
||||
7. [Next steps](#7-next-steps)
|
||||
|
||||
## API Introduction
|
||||
### 1.1 Setup with Maven
|
||||
@@ -51,12 +53,12 @@ Add the repository to your `pom.xml` as per below. You can alternatively specify
|
||||
</repository>
|
||||
</repositories>
|
||||
```
|
||||
Add the dependency to your `pom.xml` as per below. Replace `VERSION` with the latest version of HuskSync (without the v): 
|
||||
Add the dependency to your `pom.xml` as per below. Replace `HUSKSYNC_VERSION` with the latest version of HuskSync (without the v): . and `MINECRAFT_VERSION` with the version of Minecraft you want to target (e.g. `1.20.1`). A correctly formed version target should look like: `3.7+1.20.1`. Omit the plus symbol and Minecraft version if you are targeting the `common` platform.
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>net.william278.husksync</groupId>
|
||||
<artifactId>husksync-PLATFORM</artifactId>
|
||||
<version>VERSION</version>
|
||||
<version>HUSKSYNC_VERSION+MINECRAFT_VERSION</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
```
|
||||
@@ -74,16 +76,16 @@ allprojects {
|
||||
}
|
||||
}
|
||||
```
|
||||
Add the dependency as per below. Replace `VERSION` with the latest version of HuskSync (without the v): 
|
||||
Add the dependency as per below. Replace `HUSKSYNC_VERSION` with the latest version of HuskSync (without the v): . and `MINECRAFT_VERSION` with the version of Minecraft you want to target (e.g. `1.20.1`). A correctly formed version target should look like: `3.7+1.20.1`. Omit the plus symbol and Minecraft version if you are targeting the `common` platform.
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
compileOnly 'net.william278.husksync:husksync-PLATFORM:VERSION'
|
||||
compileOnly 'net.william278.husksync:husksync-PLATFORM:HUSKSYNC_VERSION+MINECRAFT_VERSION'
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
### 2. Adding HuskSync as a dependency
|
||||
## 2. Adding HuskSync as a dependency
|
||||
- Add HuskSync to your `softdepend` (if you want to optionally use HuskSync) or `depend` (if your plugin relies on HuskSync) section in `plugin.yml` of your project.
|
||||
|
||||
```yaml
|
||||
@@ -146,7 +148,7 @@ public class HuskSyncAPIHook {
|
||||
## 6. CompletableFuture and Optional basics
|
||||
- HuskSync's API methods often deal with `CompletableFuture`s and `Optional`s.
|
||||
- A `CompletableFuture` is an asynchronous callback mechanism. The method will be processed asynchronously and the data returned when it has been retrieved. Then, use `CompletableFuture#thenAccept(data -> {})` to do what you want to do with the `data` you requested after it has asynchronously been retrieved, to prevent lag.
|
||||
- An `Optional` is a null-safe representation of data, or no data. You can check if the Optional is empty via `Optional#isEmpty()` (which will be returned by the API if no data could be found for the call you made). If the optional does contain data, you can get it via `Optional#get().
|
||||
- An `Optional` is a null-safe representation of data, or no data. You can check if the Optional is empty via `Optional#isEmpty()` (which will be returned by the API if no data could be found for the call you made). If the optional does contain data, you can get it via `Optional#get()`.
|
||||
|
||||
> **Warning:** You should never call `#join()` on futures returned from the HuskSyncAPI as futures are processed on server asynchronous tasks, which could lead to thread deadlock and crash your server if you attempt to lock the main thread to process them.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user