mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-24 00:59:18 +00:00
Compare commits
219 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a15739fbb9 | ||
|
|
f4b9124636 | ||
|
|
fecda83fcb | ||
|
|
07228c3661 | ||
|
|
a0fb2e90b3 | ||
|
|
ae69c1c060 | ||
|
|
4992f4492c | ||
|
|
58bd3acdc3 | ||
|
|
af51c035a3 | ||
|
|
85ae2b5fb2 | ||
|
|
7ff10b33a0 | ||
|
|
431c9e13c9 | ||
|
|
c8579fb987 | ||
|
|
2f4eb46456 | ||
|
|
2d547507d5 | ||
|
|
8e4678468e | ||
|
|
c2a32cabc5 | ||
|
|
07f06aac68 | ||
|
|
7ae1001b1b | ||
|
|
e04c19acf5 | ||
|
|
1820a810f4 | ||
|
|
cedd12a048 | ||
|
|
7967d00208 | ||
|
|
00a68be2ad | ||
|
|
da5d991d2a | ||
|
|
c2f6d240ad | ||
|
|
4cde24c536 | ||
|
|
029617bc45 | ||
|
|
0627fb20e4 | ||
|
|
bc1f983684 | ||
|
|
31eb747c55 | ||
|
|
e8facf52ce | ||
|
|
5ee4bdd644 | ||
|
|
92c371e201 | ||
|
|
d27278454a | ||
|
|
16780c149c | ||
|
|
0445ba63bc | ||
|
|
b6aefd6f57 | ||
|
|
f803af0225 | ||
|
|
2675f4a377 | ||
|
|
03341c981f | ||
|
|
38cc654167 | ||
|
|
b347a8d060 | ||
|
|
8733b86b45 | ||
|
|
eda8e72633 | ||
|
|
c942a015d1 | ||
|
|
c00265f1f9 | ||
|
|
e303984dcf | ||
|
|
b449b5dee6 | ||
|
|
48f8c0c967 | ||
|
|
f88c4c3e2c | ||
|
|
e6273fa9a0 | ||
|
|
1ba5585d0d | ||
|
|
73547371ae | ||
|
|
fca6825394 | ||
|
|
53af114f44 | ||
|
|
311cc85c92 | ||
|
|
099a258cf8 | ||
|
|
480f59a166 | ||
|
|
45c2f5350f | ||
|
|
ed88d77852 | ||
|
|
e7fc9f015e | ||
|
|
cabde9e8d8 | ||
|
|
4df7d2def4 | ||
|
|
59ed77c169 | ||
|
|
53da3bd40c | ||
|
|
abdf8223fc | ||
|
|
a5efeecad3 | ||
|
|
4d26b24d13 | ||
|
|
29b3a60c64 | ||
|
|
da894f57c4 | ||
|
|
1bd703641b | ||
|
|
1b1d4c8e8d | ||
|
|
842ec0e28d | ||
|
|
2d5648408e | ||
|
|
41b3240741 | ||
|
|
bc03e8f3e3 | ||
|
|
86799f4c08 | ||
|
|
a3e004cf71 | ||
|
|
a7aeb1de21 | ||
|
|
1a703102c3 | ||
|
|
368c68f42b | ||
|
|
e191713bdc | ||
|
|
1604338498 | ||
|
|
c223797bf4 | ||
|
|
9b10adc8e4 | ||
|
|
5935f1ab5f | ||
|
|
3455b10a20 | ||
|
|
34e08b712d | ||
|
|
605d314a58 | ||
|
|
daaf5147a7 | ||
|
|
50eb9a7543 | ||
|
|
7d8ef7b6b3 | ||
|
|
347d2d0a8f | ||
|
|
bd560fcc99 | ||
|
|
b68aedc99a | ||
|
|
47373d8974 | ||
|
|
a57b8df994 | ||
|
|
17235637a5 | ||
|
|
cd5abd5a65 | ||
|
|
5c6631cdcf | ||
|
|
621afcd5c6 | ||
|
|
112a974a6c | ||
|
|
f9d46b4aff | ||
|
|
dfd828bca1 | ||
|
|
2df9fd897a | ||
|
|
ff2531539e | ||
|
|
52ec138273 | ||
|
|
0f7a866652 | ||
|
|
eeb52ac41e | ||
|
|
4c7ec9ec21 | ||
|
|
2f9064c4c6 | ||
|
|
5c234cdb1d | ||
|
|
7d8a74381b | ||
|
|
04a7793585 | ||
|
|
ea068529f6 | ||
|
|
fead3df0d8 | ||
|
|
0c5a42a344 | ||
|
|
75a2378ea8 | ||
|
|
662fc96ad5 | ||
|
|
f456443da0 | ||
|
|
07da1c04ce | ||
|
|
845abf370a | ||
|
|
83b5209a75 | ||
|
|
8e9850dd19 | ||
|
|
1d24209b68 | ||
|
|
da70a54d78 | ||
|
|
fc7330213a | ||
|
|
d8272ba52d | ||
|
|
315f0eeb2f | ||
|
|
8e83617ac4 | ||
|
|
212bb0beb8 | ||
|
|
c16231b12b | ||
|
|
93f7294859 | ||
|
|
32ac57e2a4 | ||
|
|
c949c976d6 | ||
|
|
ab736829f2 | ||
|
|
4433926ce7 | ||
|
|
f819fd4d5e | ||
|
|
e7659255fe | ||
|
|
0dee2e8319 | ||
|
|
7b35c47315 | ||
|
|
5056a794d8 | ||
|
|
5e6068431a | ||
|
|
8d69508689 | ||
|
|
efb6d8a7de | ||
|
|
79d9778378 | ||
|
|
6a6695e447 | ||
|
|
8862e6cd70 | ||
|
|
0b29de9efc | ||
|
|
962cdfce0b | ||
|
|
0c527202e5 | ||
|
|
d4e33aa9d2 | ||
|
|
2fcd58fc18 | ||
|
|
3d10b2324f | ||
|
|
31419f3b97 | ||
|
|
8105ac27fc | ||
|
|
44f251a948 | ||
|
|
463e707d27 | ||
|
|
2d85910744 | ||
|
|
268b279fdf | ||
|
|
a8ca3314d8 | ||
|
|
2bdd3dae37 | ||
|
|
e29564c4ad | ||
|
|
6b8bb23af9 | ||
|
|
91bbe05851 | ||
|
|
8ed6869aad | ||
|
|
7efdf0d329 | ||
|
|
49c32e3f98 | ||
|
|
f0574527b9 | ||
|
|
ad510a8fca | ||
|
|
303b287705 | ||
|
|
549508b9c1 | ||
|
|
6c8a577701 | ||
|
|
862177bec7 | ||
|
|
dbed4d83a2 | ||
|
|
aa2090d97a | ||
|
|
b168ede7c5 | ||
|
|
0e706d36c4 | ||
|
|
69d68de5c0 | ||
|
|
3d5395e5ae | ||
|
|
332c71f041 | ||
|
|
b9fbcd72dd | ||
|
|
68897e6265 | ||
|
|
04606a7c9a | ||
|
|
6286bbe2ad | ||
|
|
24ba209f8f | ||
|
|
05d588f681 | ||
|
|
9aa3606f54 | ||
|
|
fc05e4b17a | ||
|
|
7b2b47de83 | ||
|
|
be0b4e3397 | ||
|
|
dd1ba594de | ||
|
|
89368778f3 | ||
|
|
e3fb1762a1 | ||
|
|
516c243df8 | ||
|
|
b7aa75fcd5 | ||
|
|
549f013e0f | ||
|
|
14c56af465 | ||
|
|
bee01dd15a | ||
|
|
e97551e67e | ||
|
|
97023e8425 | ||
|
|
4fa7106a46 | ||
|
|
e0b81e4c76 | ||
|
|
c4adec3082 | ||
|
|
107238360c | ||
|
|
6141adbdb9 | ||
|
|
eaa2ed74a6 | ||
|
|
44c652c452 | ||
|
|
78cf6bff63 | ||
|
|
8ad4158ec0 | ||
|
|
405e6d7162 | ||
|
|
cff1c8f982 | ||
|
|
f43ca2f043 | ||
|
|
3114ab1a62 | ||
|
|
2da9749b0c | ||
|
|
d4d510e100 | ||
|
|
550ea26097 | ||
|
|
2b1e72a42e |
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'
|
||||
- name: '[Current - 1.20.1] Build 🛎️'
|
||||
run: |
|
||||
./gradlew clean build publish
|
||||
env:
|
||||
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v5
|
||||
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
|
||||
70
.github/workflows/ci_1.21.1.yml
vendored
Normal file
70
.github/workflows/ci_1.21.1.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
name: CI Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'minecraft/1.21.1' ]
|
||||
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
|
||||
with:
|
||||
ref: 'minecraft/1.21.1'
|
||||
- name: '[Current - 1.21.1] Build 🛎️'
|
||||
run: |
|
||||
./gradlew clean build publish
|
||||
env:
|
||||
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v5
|
||||
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
|
||||
68
.github/workflows/ci_master.yml
vendored
Normal file
68
.github/workflows/ci_master.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.4'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Setup JDK 21 📦'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
- name: 'Setup Gradle 8.12 🏗️'
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
gradle-version: '8.12'
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
- name: '[Current - 1.21.4] Build 🛎️'
|
||||
run: |
|
||||
./gradlew clean build publish
|
||||
env:
|
||||
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v5
|
||||
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.4
|
||||
fabric-1.21.4
|
||||
distro-groups: |
|
||||
paper
|
||||
fabric
|
||||
distro-descriptions: |
|
||||
Paper 1.21.4
|
||||
Fabric 1.21.4
|
||||
files: |
|
||||
target/HuskSync-Paper-${{ env.version_name }}+mc.1.21.4.jar
|
||||
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.4.jar
|
||||
6
.github/workflows/pr_tests.yml
vendored
6
.github/workflows/pr_tests.yml
vendored
@@ -14,17 +14,17 @@ 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
|
||||
with:
|
||||
arguments: test
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
uses: mikepenz/action-junit-report@v5
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
93
.github/workflows/release.yml
vendored
93
.github/workflows/release.yml
vendored
@@ -8,26 +8,101 @@ 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.12 🏗️'
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
arguments: build test publish
|
||||
gradle-version: '8.12'
|
||||
- name: '[Current - 1.21.4] Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: '1_21_4'
|
||||
- name: '[Non-LTS - 1.21.1] Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: 'minecraft/1.21.1'
|
||||
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'
|
||||
- name: '[Current - 1.21.4] Build 🛎️'
|
||||
run: |
|
||||
mkdir target
|
||||
cd 1_21_4
|
||||
./gradlew clean build publish -Dforce-hide-version-meta=1
|
||||
cp -rf target/* ../target/
|
||||
cd ..
|
||||
env:
|
||||
RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
RELEASES_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: '[Non-LTS - 1.21.1] Build 🛎️'
|
||||
run: |
|
||||
cd 1_21_1
|
||||
./gradlew clean build publish -Dforce-hide-version-meta=1
|
||||
cp -rf target/* ../target/
|
||||
cd ..
|
||||
env:
|
||||
RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
RELEASES_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- 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 ..
|
||||
env:
|
||||
RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
RELEASES_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
uses: mikepenz/action-junit-report@v5
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
- name: 'Publish to William278.net 🚀'
|
||||
uses: WiIIiam278/bones-publish-action@v1
|
||||
with:
|
||||
api-key: ${{ secrets.BONES_API_KEY }}
|
||||
project: 'husksync'
|
||||
channel: 'release'
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
changelog: ${{ github.event.release.body }}
|
||||
distro-names: |
|
||||
paper-1.21.4
|
||||
fabric-1.21.4
|
||||
paper-1.21.1
|
||||
fabric-1.21.1
|
||||
paper-1.20.1
|
||||
fabric-1.20.1
|
||||
distro-groups: |
|
||||
paper
|
||||
fabric
|
||||
paper
|
||||
fabric
|
||||
paper
|
||||
fabric
|
||||
distro-descriptions: |
|
||||
Paper 1.21.4
|
||||
Fabric 1.21.4
|
||||
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.4.jar
|
||||
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.4.jar
|
||||
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 🛎️'
|
||||
|
||||
41
README.md
41
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_master.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/ci_master.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" />
|
||||
@@ -43,16 +43,37 @@
|
||||
|
||||
**Ready?** [It's syncing time!](https://william278.net/docs/husksync/setup)
|
||||
|
||||
## Setup
|
||||
Requires a MySQL/Mongo/PostgreSQL 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 Status |
|
||||
|:---------------:|:---------------:|:------------:|:--------------|:-----------------------------|
|
||||
| 1.21.4 | _latest_ | 21 | Paper, Fabric | ✅ **Active Release** |
|
||||
| 1.21.3 | 3.7.1 | 21 | Paper, Fabric | 🗃️ Archived (December 2024) |
|
||||
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **November 2025** (LTS) |
|
||||
| 1.20.6 | 3.6.8 | 17 | Paper | 🗃️ Archived (October 2024) |
|
||||
| 1.20.4 | 3.6.8 | 17 | Paper | 🗃️ Archived (July 2024) |
|
||||
| 1.20.1 | _latest_ | 17 | Paper, Fabric | ✅ **November 2025** (LTS) |
|
||||
| 1.17.1 - 1.19.4 | 3.6.8 | 17 | Paper | 🗃️ Archived |
|
||||
| 1.16.5 | 3.2.1 | 16 | Paper | 🗃️ Archived |
|
||||
|
||||
HuskSync is primarily developed against the latest release. Old Minecraft versions are allocated a support channel based on popularity, mod support, etc:
|
||||
|
||||
* Long Term Support (LTS) – Supported for up to 12-18 months
|
||||
* Non-Long Term Support (Non-LTS) – Supported for 3-6 months
|
||||
|
||||
Verify your purchase on Discord and [Download HuskSync](https://william278.net/project/husksync/download) for your server.
|
||||
|
||||
## Setup
|
||||
Requires a [MySQL/MariaDB/Mongo/PostgreSQL database](https://william278.net/docs/husksync/database), a [Redis (v5.0+) server]((https://william278.net/docs/husksync/redis)) and a network of [compatible Spigot or Fabric Minecraft servers](https://william278.net/docs/husksync/compatibility).
|
||||
|
||||
1. Place the plugin jar file in the `/plugins` or `/mods` directory of each Spigot/Fabric server. You do not need to install HuskSync as a proxy plugin.
|
||||
2. Start, then stop every server to let HuskSync generate the config file.
|
||||
3. Navigate to the HuskSync config file on each server (~/plugins/HuskSync/config.yml) and fill in both your database and Redis server 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 +87,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.
|
||||
@@ -82,4 +103,4 @@ Translations of the plugin locales are welcome to help make the plugin more acce
|
||||
- [bStats](https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140) — View plugin metrics
|
||||
|
||||
---
|
||||
© [William278](https://william278.net/), 2023. Licensed under the Apache-2.0 License.
|
||||
© [William278](https://william278.net/), 2025. Licensed under the Apache-2.0 License.
|
||||
|
||||
58
build.gradle
58
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.5'
|
||||
id 'org.cadixdev.licenser' version '0.6.1' apply false
|
||||
id 'org.ajoberstar.grgit' version '5.2.2'
|
||||
id 'fabric-loom' version "$fabric_loom_version" apply false
|
||||
id 'org.ajoberstar.grgit' version '5.3.0'
|
||||
id 'maven-publish'
|
||||
id 'java'
|
||||
}
|
||||
@@ -17,6 +18,7 @@ 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()
|
||||
@@ -57,19 +59,23 @@ 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.compilerArgs += ['-Xlint:unchecked', '-Xlint:deprecation']
|
||||
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/' }
|
||||
@@ -77,13 +83,12 @@ allprojects {
|
||||
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.4'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.4'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.4'
|
||||
}
|
||||
|
||||
test {
|
||||
@@ -97,14 +102,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()}"
|
||||
|
||||
@@ -117,8 +128,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()
|
||||
@@ -150,13 +166,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,10 +193,15 @@ 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() {
|
||||
// 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'
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
dependencies {
|
||||
implementation project(path: ':common')
|
||||
|
||||
implementation 'org.bstats:bstats-bukkit:3.0.2'
|
||||
implementation 'net.william278.uniform:uniform-bukkit:1.3'
|
||||
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 'net.william278:mapdataapi:2.0'
|
||||
implementation 'org.bstats:bstats-bukkit:3.1.0'
|
||||
implementation 'net.kyori:adventure-platform-bukkit:4.3.4'
|
||||
implementation 'dev.triumphteam:triumph-gui:3.1.11'
|
||||
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
|
||||
implementation 'de.tr7zw:item-nbt-api:2.12.4'
|
||||
implementation 'de.tr7zw:item-nbt-api:2.14.2-SNAPSHOT'
|
||||
|
||||
compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT'
|
||||
compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||
compileOnly 'commons-io:commons-io:2.16.1'
|
||||
compileOnly 'org.json:json:20240303'
|
||||
compileOnly "org.spigotmc:spigot-api:${bukkit_spigot_api}"
|
||||
compileOnly 'com.github.retrooper:packetevents-spigot:2.7.0'
|
||||
compileOnly 'com.comphenix.protocol:ProtocolLib:5.3.0'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.36'
|
||||
compileOnly 'commons-io:commons-io:2.18.0'
|
||||
compileOnly 'org.json:json:20250107'
|
||||
compileOnly 'net.william278:minedown:1.8.2'
|
||||
compileOnly 'de.exlll:configlib-yaml:4.5.0'
|
||||
compileOnly 'com.zaxxer:HikariCP:5.1.0'
|
||||
compileOnly 'com.zaxxer:HikariCP:6.2.1'
|
||||
compileOnly 'net.william278:DesertWell:2.0.4'
|
||||
compileOnly 'net.william278:AdvancementAPI:97a9583413'
|
||||
compileOnly "redis.clients:jedis:$jedis_version"
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.36'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
@@ -41,16 +41,15 @@ shadowJar {
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'de.exlll', 'net.william278.husksync.libraries'
|
||||
relocate 'net.william278.uniform', 'net.william278.husksync.libraries.uniform'
|
||||
relocate 'net.william278.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'
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.gson.Gson;
|
||||
import de.tr7zw.changeme.nbtapi.utils.DataFixerUtil;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -34,7 +35,7 @@ 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;
|
||||
@@ -46,7 +47,6 @@ 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;
|
||||
@@ -58,13 +58,15 @@ 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.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;
|
||||
@@ -77,6 +79,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("unchecked")
|
||||
public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier,
|
||||
BukkitEventDispatcher, BukkitMapPersister {
|
||||
|
||||
@@ -86,7 +89,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();
|
||||
@@ -98,7 +103,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;
|
||||
@@ -113,11 +118,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
|
||||
@@ -125,8 +129,23 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
loadSettings();
|
||||
loadLocales();
|
||||
loadServer();
|
||||
validateConfigFiles();
|
||||
});
|
||||
|
||||
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()) {
|
||||
@@ -138,19 +157,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.Json<>(this, BukkitData.Location.class));
|
||||
registerSerializer(Identifier.HEALTH, new BukkitSerializer.Json<>(this, BukkitData.Health.class));
|
||||
registerSerializer(Identifier.HUNGER, new BukkitSerializer.Json<>(this, BukkitData.Hunger.class));
|
||||
registerSerializer(Identifier.ATTRIBUTES, new BukkitSerializer.Json<>(this, BukkitData.Attributes.class));
|
||||
registerSerializer(Identifier.GAME_MODE, new BukkitSerializer.Json<>(this, BukkitData.GameMode.class));
|
||||
registerSerializer(Identifier.FLIGHT_STATUS, new BukkitSerializer.Json<>(this, BukkitData.FlightStatus.class));
|
||||
registerSerializer(Identifier.STATISTICS, new Serializer.Json<>(this, BukkitData.Statistics.class));
|
||||
registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this));
|
||||
registerSerializer(Identifier.STATISTICS, new BukkitSerializer.Json<>(this, BukkitData.Statistics.class));
|
||||
registerSerializer(Identifier.EXPERIENCE, new BukkitSerializer.Json<>(this, BukkitData.Experience.class));
|
||||
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
|
||||
@@ -185,10 +205,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) -> {
|
||||
@@ -255,6 +272,12 @@ 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) {
|
||||
@@ -272,7 +295,8 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
|
||||
@Override
|
||||
public boolean isDependencyLoaded(@NotNull String name) {
|
||||
return getServer().getPluginManager().getPlugin(name) != null;
|
||||
final Plugin plugin = getServer().getPluginManager().getPlugin(name);
|
||||
return plugin != null;
|
||||
}
|
||||
|
||||
// Register bStats metrics
|
||||
@@ -284,7 +308,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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,12 +333,34 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
return Version.fromString(getServer().getBukkitVersion());
|
||||
}
|
||||
|
||||
public 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", "1.21.1" -> DataFixerUtil.VERSION1_21;
|
||||
case "1.21.2", "1.21.3" -> DataFixerUtil.VERSION1_21_2;
|
||||
case "1.21.4" -> 4189/*DataFixerUtil.VERSION1_21_4*/;
|
||||
default -> DataFixerUtil.getCurrentVersion();
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getPlatformType() {
|
||||
return PLATFORM_TYPE_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getServerVersion() {
|
||||
return String.format("%s/%s", getServer().getName(), getServer().getVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<LegacyConverter> getLegacyConverter() {
|
||||
return Optional.of(legacyConverter);
|
||||
@@ -342,11 +388,6 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
return getScheduler().entitySpecificScheduler(((BukkitUser) user).getPlayer());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public CommandRegistration getCommandRegistrar() {
|
||||
return paperLib.commandRegistration();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Path getConfigDirectory() {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -29,17 +29,16 @@ import net.william278.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.config.Settings.SynchronizationSettings.AttributeSettings;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.advancement.AdvancementProgress;
|
||||
import org.bukkit.attribute.AttributeInstance;
|
||||
import org.bukkit.attribute.AttributeModifier;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.EquipmentSlotGroup;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
@@ -47,6 +46,7 @@ import org.bukkit.potion.PotionEffectType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Range;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
@@ -69,7 +69,6 @@ public abstract class BukkitData implements Data {
|
||||
private final @Nullable ItemStack @NotNull [] contents;
|
||||
|
||||
private Items(@Nullable ItemStack @NotNull [] contents) {
|
||||
|
||||
this.contents = Arrays.stream(contents.clone())
|
||||
.map(i -> i == null || i.getType() == Material.AIR ? null : i)
|
||||
.toArray(ItemStack[]::new);
|
||||
@@ -127,8 +126,6 @@ public abstract class BukkitData implements Data {
|
||||
@Getter
|
||||
public static class Inventory extends BukkitData.Items implements Data.Items.Inventory {
|
||||
|
||||
public static final int INVENTORY_SLOT_COUNT = 41;
|
||||
|
||||
@Range(from = 0, to = 8)
|
||||
private int heldItemSlot;
|
||||
|
||||
@@ -158,8 +155,9 @@ public abstract class BukkitData implements Data {
|
||||
this.clearInventoryCraftingSlots(player);
|
||||
player.setItemOnCursor(null);
|
||||
player.getInventory().setContents(plugin.setMapViews(getContents()));
|
||||
player.updateInventory();
|
||||
player.getInventory().setHeldItemSlot(heldItemSlot);
|
||||
//noinspection UnstableApiUsage
|
||||
player.updateInventory();
|
||||
}
|
||||
|
||||
private void clearInventoryCraftingSlots(@NotNull Player player) {
|
||||
@@ -175,15 +173,18 @@ public abstract class BukkitData implements Data {
|
||||
|
||||
public static class EnderChest extends BukkitData.Items implements Data.Items.EnderChest {
|
||||
|
||||
public static final int ENDER_CHEST_SLOT_COUNT = 27;
|
||||
|
||||
private EnderChest(@NotNull ItemStack[] contents) {
|
||||
private EnderChest(@Nullable ItemStack @NotNull [] contents) {
|
||||
super(contents);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Items.EnderChest adapt(@NotNull ItemStack[] items) {
|
||||
return new BukkitData.Items.EnderChest(items);
|
||||
public static BukkitData.Items.EnderChest adapt(@Nullable ItemStack @NotNull [] contents) {
|
||||
return new BukkitData.Items.EnderChest(contents);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Items.EnderChest adapt(@NotNull Collection<ItemStack> items) {
|
||||
return adapt(items.toArray(ItemStack[]::new));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -200,7 +201,7 @@ public abstract class BukkitData implements Data {
|
||||
|
||||
public static class ItemArray extends BukkitData.Items implements Data.Items {
|
||||
|
||||
private ItemArray(@NotNull ItemStack[] contents) {
|
||||
private ItemArray(@Nullable ItemStack @NotNull [] contents) {
|
||||
super(contents);
|
||||
}
|
||||
|
||||
@@ -210,7 +211,7 @@ public abstract class BukkitData implements Data {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static ItemArray adapt(@NotNull ItemStack[] drops) {
|
||||
public static ItemArray adapt(@Nullable ItemStack @NotNull [] drops) {
|
||||
return new ItemArray(drops);
|
||||
}
|
||||
|
||||
@@ -231,33 +232,33 @@ public abstract class BukkitData implements Data {
|
||||
private final Collection<PotionEffect> effects;
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> effects) {
|
||||
return new BukkitData.PotionEffects(effects);
|
||||
public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> sei) {
|
||||
return new BukkitData.PotionEffects(Lists.newArrayList(sei.stream().filter(e -> !e.isAmbient()).toList()));
|
||||
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.PotionEffects adapt(@NotNull Collection<Effect> effects) {
|
||||
return from(
|
||||
effects.stream()
|
||||
.map(effect -> new PotionEffect(
|
||||
Objects.requireNonNull(
|
||||
PotionEffectType.getByName(effect.type()),
|
||||
"Invalid potion effect type"
|
||||
),
|
||||
effect.duration(),
|
||||
effect.amplifier(),
|
||||
effect.isAmbient(),
|
||||
effect.showParticles(),
|
||||
effect.hasIcon()
|
||||
))
|
||||
.toList()
|
||||
);
|
||||
return from(effects.stream()
|
||||
.map(effect -> {
|
||||
final PotionEffectType type = matchEffectType(effect.type());
|
||||
return type != null ? new PotionEffect(
|
||||
type,
|
||||
effect.duration(),
|
||||
effect.amplifier(),
|
||||
effect.isAmbient(),
|
||||
effect.showParticles(),
|
||||
effect.hasIcon()
|
||||
) : null;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@SuppressWarnings("unused")
|
||||
public static BukkitData.PotionEffects empty() {
|
||||
return new BukkitData.PotionEffects(List.of());
|
||||
return new BukkitData.PotionEffects(Lists.newArrayList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -273,10 +274,11 @@ public abstract class BukkitData implements Data {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
@Unmodifiable
|
||||
public List<Effect> getActiveEffects() {
|
||||
return effects.stream()
|
||||
.map(potionEffect -> new Effect(
|
||||
potionEffect.getType().getName().toLowerCase(Locale.ENGLISH),
|
||||
potionEffect.getType().getKey().toString(),
|
||||
potionEffect.getAmplifier(),
|
||||
potionEffect.getDuration(),
|
||||
potionEffect.isAmbient(),
|
||||
@@ -342,9 +344,12 @@ public abstract class BukkitData implements Data {
|
||||
}));
|
||||
}
|
||||
|
||||
private void setAdvancement(@NotNull HuskSync plugin, @NotNull org.bukkit.advancement.Advancement advancement,
|
||||
@NotNull Player player, @NotNull BukkitUser user,
|
||||
@NotNull Collection<String> toAward, @NotNull Collection<String> toRevoke) {
|
||||
private void setAdvancement(@NotNull HuskSync plugin,
|
||||
@NotNull org.bukkit.advancement.Advancement advancement,
|
||||
@NotNull Player player,
|
||||
@NotNull BukkitUser user,
|
||||
@NotNull Collection<String> toAward,
|
||||
@NotNull Collection<String> toRevoke) {
|
||||
plugin.runSync(() -> {
|
||||
// Track player exp level & progress
|
||||
final int expLevel = player.getLevel();
|
||||
@@ -356,7 +361,8 @@ public abstract class BukkitData implements Data {
|
||||
toRevoke.forEach(progress::revokeCriteria);
|
||||
|
||||
// Set player experience and level (prevent advancement awards applying twice), reset game rule
|
||||
if (!toAward.isEmpty() && player.getLevel() != expLevel || player.getExp() != expProgress) {
|
||||
if (!toAward.isEmpty()
|
||||
&& (player.getLevel() != expLevel || player.getExp() != expProgress)) {
|
||||
player.setLevel(expLevel);
|
||||
player.setExp(expProgress);
|
||||
}
|
||||
@@ -447,9 +453,10 @@ public abstract class BukkitData implements Data {
|
||||
Registry.STATISTIC.forEach(id -> {
|
||||
switch (id.getType()) {
|
||||
case UNTYPED -> addStatistic(player, id, generic);
|
||||
case BLOCK -> addMaterialStatistic(player, id, blocks, true);
|
||||
case ITEM -> addMaterialStatistic(player, id, items, false);
|
||||
case ENTITY -> addEntityStatistic(player, id, entities);
|
||||
// Todo - Future - Use BLOCK and ITEM registries when API stabilizes
|
||||
case BLOCK -> addStatistic(player, id, Registry.MATERIAL, blocks);
|
||||
case ITEM -> addStatistic(player, id, Registry.MATERIAL, items);
|
||||
case ENTITY -> addStatistic(player, id, Registry.ENTITY_TYPE, entities);
|
||||
}
|
||||
});
|
||||
return new BukkitData.Statistics(generic, blocks, items, entities);
|
||||
@@ -470,43 +477,31 @@ public abstract class BukkitData implements Data {
|
||||
}
|
||||
}
|
||||
|
||||
private static void addMaterialStatistic(@NotNull Player p, @NotNull Statistic id,
|
||||
@NotNull Map<String, Map<String, Integer>> map, boolean isBlock) {
|
||||
Registry.MATERIAL.forEach(material -> {
|
||||
if ((material.isBlock() && !isBlock) || (material.isItem() && isBlock)) {
|
||||
return;
|
||||
}
|
||||
final int stat = p.getStatistic(id, material);
|
||||
if (stat != 0) {
|
||||
map.computeIfAbsent(id.getKey().getKey(), k -> Maps.newHashMap())
|
||||
.put(material.getKey().getKey(), stat);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void addEntityStatistic(@NotNull Player p, @NotNull Statistic id,
|
||||
@NotNull Map<String, Map<String, Integer>> map) {
|
||||
Registry.ENTITY_TYPE.forEach(entity -> {
|
||||
if (!entity.isAlive()) {
|
||||
return;
|
||||
}
|
||||
final int stat = p.getStatistic(id, entity);
|
||||
if (stat != 0) {
|
||||
map.computeIfAbsent(id.getKey().getKey(), k -> Maps.newHashMap())
|
||||
.put(entity.getKey().getKey(), stat);
|
||||
private static <R extends Keyed> void addStatistic(@NotNull Player p, @NotNull Statistic id,
|
||||
@NotNull Registry<R> registry,
|
||||
@NotNull Map<String, Map<String, Integer>> map) {
|
||||
registry.forEach(i -> {
|
||||
try {
|
||||
final int stat = i instanceof Material m ? p.getStatistic(id, m) :
|
||||
(i instanceof EntityType e ? p.getStatistic(id, e) : -1);
|
||||
if (stat != 0) {
|
||||
map.compute(id.getKey().getKey(), (k, v) -> v == null ? Maps.newHashMap() : v)
|
||||
.put(i.getKey().getKey(), stat);
|
||||
}
|
||||
} catch (IllegalStateException ignored) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) {
|
||||
genericStatistics.forEach((id, v) -> applyStat(user, id, Statistic.Type.UNTYPED, v));
|
||||
blockStatistics.forEach((id, m) -> m.forEach((b, v) -> applyStat(user, id, Statistic.Type.BLOCK, v, b)));
|
||||
itemStatistics.forEach((id, m) -> m.forEach((i, v) -> applyStat(user, id, Statistic.Type.ITEM, v, i)));
|
||||
entityStatistics.forEach((id, m) -> m.forEach((e, v) -> applyStat(user, id, Statistic.Type.ENTITY, v, e)));
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync p) {
|
||||
genericStatistics.forEach((k, v) -> applyStat(p, user, k, Statistic.Type.UNTYPED, v));
|
||||
blockStatistics.forEach((k, m) -> m.forEach((b, v) -> applyStat(p, user, k, Statistic.Type.BLOCK, v, b)));
|
||||
itemStatistics.forEach((k, m) -> m.forEach((i, v) -> applyStat(p, user, k, Statistic.Type.ITEM, v, i)));
|
||||
entityStatistics.forEach((k, m) -> m.forEach((e, v) -> applyStat(p, user, k, Statistic.Type.ENTITY, v, e)));
|
||||
}
|
||||
|
||||
private void applyStat(@NotNull UserDataHolder user, @NotNull String id,
|
||||
private void applyStat(@NotNull HuskSync plugin, @NotNull UserDataHolder user, @NotNull String id,
|
||||
@NotNull Statistic.Type type, int value, @NotNull String... key) {
|
||||
final Player player = ((BukkitUser) user).getPlayer();
|
||||
final Statistic stat = matchStatistic(id);
|
||||
@@ -520,7 +515,8 @@ public abstract class BukkitData implements Data {
|
||||
case BLOCK, ITEM -> player.setStatistic(stat, Objects.requireNonNull(matchMaterial(key[0])), value);
|
||||
case ENTITY -> player.setStatistic(stat, Objects.requireNonNull(matchEntityType(key[0])), value);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
} catch (Throwable a) {
|
||||
plugin.log(Level.WARNING, "Failed to apply statistic " + id, a);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,6 +551,7 @@ public abstract class BukkitData implements Data {
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public static class Attributes extends BukkitData implements Data.Attributes, Adaptable {
|
||||
|
||||
private List<Attribute> attributes;
|
||||
@@ -562,14 +559,13 @@ public abstract class BukkitData implements Data {
|
||||
@NotNull
|
||||
public static BukkitData.Attributes adapt(@NotNull Player player, @NotNull HuskSync plugin) {
|
||||
final List<Attribute> attributes = Lists.newArrayList();
|
||||
final AttributeSettings settings = plugin.getSettings().getSynchronization().getAttributes();
|
||||
Registry.ATTRIBUTE.forEach(id -> {
|
||||
final AttributeInstance instance = player.getAttribute(id);
|
||||
if (instance == null || instance.getValue() == instance.getDefaultValue() || plugin
|
||||
.getSettings().getSynchronization().isIgnoredAttribute(id.getKey().toString())) {
|
||||
// We don't sync unmodified or disabled attributes
|
||||
return;
|
||||
if (settings.isIgnoredAttribute(id.getKey().toString()) || instance == null) {
|
||||
return; // We don't sync attributes not marked as to be synced
|
||||
}
|
||||
attributes.add(adapt(instance));
|
||||
attributes.add(adapt(instance, settings));
|
||||
});
|
||||
return new BukkitData.Attributes(attributes);
|
||||
}
|
||||
@@ -588,47 +584,63 @@ public abstract class BukkitData implements Data {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Attribute adapt(@NotNull AttributeInstance instance) {
|
||||
private static Attribute adapt(@NotNull AttributeInstance instance, @NotNull AttributeSettings settings) {
|
||||
return new Attribute(
|
||||
instance.getAttribute().getKey().toString(),
|
||||
instance.getBaseValue(),
|
||||
instance.getModifiers().stream().map(BukkitData.Attributes::adapt).collect(Collectors.toSet())
|
||||
instance.getModifiers().stream()
|
||||
.filter(modifier -> !settings.isIgnoredModifier(modifier.getName()))
|
||||
.filter(modifier -> modifier.getSlotGroup() != EquipmentSlotGroup.ANY)
|
||||
.map(BukkitData.Attributes::adapt).collect(Collectors.toSet())
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Modifier adapt(@NotNull AttributeModifier modifier) {
|
||||
return new Modifier(
|
||||
modifier.getUniqueId(),
|
||||
modifier.getName(),
|
||||
modifier.getKey().toString(),
|
||||
modifier.getAmount(),
|
||||
modifier.getOperation().ordinal(),
|
||||
modifier.getSlot() != null ? modifier.getSlot().ordinal() : -1
|
||||
modifier.getSlotGroup().toString()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
Registry.ATTRIBUTE.forEach(id -> applyAttribute(user.getPlayer().getAttribute(id), getAttribute(id).orElse(null)));
|
||||
}
|
||||
|
||||
private static void applyAttribute(@Nullable AttributeInstance instance, @Nullable Attribute attribute) {
|
||||
if (instance == null) {
|
||||
return;
|
||||
}
|
||||
instance.setBaseValue(attribute == null ? instance.getDefaultValue() : instance.getBaseValue());
|
||||
instance.getModifiers().forEach(instance::removeModifier);
|
||||
instance.setBaseValue(attribute == null ? instance.getValue() : attribute.baseValue());
|
||||
if (attribute != null) {
|
||||
attribute.modifiers().forEach(modifier -> instance.addModifier(new AttributeModifier(
|
||||
modifier.uuid(),
|
||||
modifier.name(),
|
||||
modifier.amount(),
|
||||
AttributeModifier.Operation.values()[modifier.operationType()],
|
||||
modifier.equipmentSlot() != -1 ? EquipmentSlot.values()[modifier.equipmentSlot()] : null
|
||||
)));
|
||||
attribute.modifiers().stream()
|
||||
.filter(mod -> instance.getModifiers().stream().map(AttributeModifier::getName)
|
||||
.noneMatch(n -> n.equals(mod.name())))
|
||||
.distinct().filter(mod -> !mod.hasUuid())
|
||||
.forEach(mod -> instance.addModifier(adapt(mod)));
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static AttributeModifier adapt(@NotNull Modifier modifier) {
|
||||
return new AttributeModifier(
|
||||
Objects.requireNonNull(NamespacedKey.fromString(modifier.name())),
|
||||
modifier.amount(),
|
||||
AttributeModifier.Operation.values()[modifier.operation()],
|
||||
Optional.ofNullable(EquipmentSlotGroup.getByName(modifier.slotGroup())).orElse(EquipmentSlotGroup.ANY)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
final AttributeSettings settings = plugin.getSettings().getSynchronization().getAttributes();
|
||||
Registry.ATTRIBUTE.forEach(id -> {
|
||||
if (settings.isIgnoredAttribute(id.getKey().toString())) {
|
||||
return;
|
||||
}
|
||||
applyAttribute(user.getPlayer().getAttribute(id), getAttribute(id).orElse(null));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -640,24 +652,38 @@ public abstract class BukkitData implements Data {
|
||||
private double health;
|
||||
@SerializedName("health_scale")
|
||||
private double healthScale;
|
||||
@SerializedName("is_health_scaled")
|
||||
private boolean isHealthScaled;
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Health from(double health, double healthScale) {
|
||||
return new BukkitData.Health(health, healthScale);
|
||||
public static BukkitData.Health from(double health, double scale, boolean isScaled) {
|
||||
return new BukkitData.Health(health, scale, isScaled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #from(double, double, boolean)} instead
|
||||
*/
|
||||
@NotNull
|
||||
@Deprecated(since = "3.5.4")
|
||||
public static BukkitData.Health from(double health, double scale) {
|
||||
return from(health, scale, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #from(double, double, boolean)} instead
|
||||
*/
|
||||
@NotNull
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
@SuppressWarnings("unused")
|
||||
public static BukkitData.Health from(double health, double maxHealth, double healthScale) {
|
||||
return from(health, healthScale);
|
||||
public static BukkitData.Health from(double health, @SuppressWarnings("unused") double max, double scale) {
|
||||
return from(health, scale, false);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Health adapt(@NotNull Player player) {
|
||||
return from(
|
||||
player.getHealth(),
|
||||
player.isHealthScaled() ? player.getHealthScale() : 0d
|
||||
player.getHealthScale(),
|
||||
player.isHealthScaled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -674,16 +700,12 @@ public abstract class BukkitData implements Data {
|
||||
}
|
||||
|
||||
// Set health scale
|
||||
double scale = healthScale <= 0 ? player.getMaxHealth() : healthScale;
|
||||
try {
|
||||
if (healthScale != 0d) {
|
||||
player.setHealthScaled(true);
|
||||
player.setHealthScale(healthScale);
|
||||
} else {
|
||||
player.setHealthScaled(false);
|
||||
player.setHealthScale(player.getMaxHealth());
|
||||
}
|
||||
player.setHealthScale(scale);
|
||||
player.setHealthScaled(isHealthScaled);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.WARNING, "Error setting %s's health scale to %s".formatted(player.getName(), healthScale), e);
|
||||
plugin.log(Level.WARNING, "Error setting %s's health scale to %s".formatted(player.getName(), scale), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ 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;
|
||||
@@ -41,6 +42,8 @@ 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 {
|
||||
@@ -60,8 +63,6 @@ public class BukkitSerializer {
|
||||
|
||||
public static class Inventory extends BukkitSerializer implements Serializer<BukkitData.Items.Inventory>,
|
||||
ItemDeserializer {
|
||||
private static final String ITEMS_TAG = "items";
|
||||
private static final String HELD_ITEM_SLOT_TAG = "held_item_slot";
|
||||
|
||||
public Inventory(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
@@ -74,7 +75,7 @@ public class BukkitSerializer {
|
||||
final ReadWriteNBT items = root.hasTag(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null;
|
||||
return BukkitData.Items.Inventory.from(
|
||||
items != null ? getItems(items, dataMcVersion) : new ItemStack[INVENTORY_SLOT_COUNT],
|
||||
root.getInteger(HELD_ITEM_SLOT_TAG)
|
||||
root.hasTag(HELD_ITEM_SLOT_TAG) ? root.getInteger(HELD_ITEM_SLOT_TAG) : 0
|
||||
);
|
||||
}
|
||||
|
||||
@@ -126,15 +127,15 @@ public class BukkitSerializer {
|
||||
@Nullable
|
||||
default ItemStack[] getItems(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion) {
|
||||
if (mcVersion.compareTo(getPlugin().getMinecraftVersion()) < 0) {
|
||||
return upgradeItemStack((NBTCompound) tag, mcVersion);
|
||||
return upgradeItemStacks((NBTCompound) tag, mcVersion);
|
||||
}
|
||||
return NBT.itemStackArrayFromNBT(tag);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ItemStack @NotNull [] upgradeItemStack(@NotNull NBTCompound compound, @NotNull Version mcVersion) {
|
||||
final ReadWriteNBTCompoundList items = compound.getCompoundList("items");
|
||||
final ItemStack[] itemStacks = new ItemStack[compound.getInteger("size")];
|
||||
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);
|
||||
@@ -152,19 +153,11 @@ public class BukkitSerializer {
|
||||
@NotNull
|
||||
private ReadWriteNBT upgradeItemData(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion)
|
||||
throws NoSuchFieldException, IllegalAccessException {
|
||||
return DataFixerUtil.fixUpItemData(tag, getDataVersion(mcVersion), DataFixerUtil.getCurrentVersion());
|
||||
}
|
||||
|
||||
private int getDataVersion(@NotNull Version mcVersion) {
|
||||
return switch (mcVersion.toStringWithoutMetadata()) {
|
||||
case "1.16", "1.16.1", "1.16.2", "1.16.3", "1.16.4", "1.16.5" -> DataFixerUtil.VERSION1_16_5;
|
||||
case "1.17", "1.17.1" -> DataFixerUtil.VERSION1_17_1;
|
||||
case "1.18", "1.18.1", "1.18.2" -> DataFixerUtil.VERSION1_18_2;
|
||||
case "1.19", "1.19.1", "1.19.2" -> DataFixerUtil.VERSION1_19_2;
|
||||
case "1.20", "1.20.1", "1.20.2" -> DataFixerUtil.VERSION1_20_2;
|
||||
case "1.20.3", "1.20.4" -> DataFixerUtil.VERSION1_20_4;
|
||||
default -> DataFixerUtil.getCurrentVersion();
|
||||
};
|
||||
return DataFixerUtil.fixUpItemData(
|
||||
tag,
|
||||
getPlugin().getDataVersion(mcVersion),
|
||||
DataFixerUtil.getCurrentVersion()
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -226,7 +219,7 @@ public class BukkitSerializer {
|
||||
|
||||
@Override
|
||||
public BukkitData.PersistentData deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
return BukkitData.PersistentData.from(new NBTContainer(serialized));
|
||||
return BukkitData.PersistentData.from((NBTContainer) NBT.parseNBT(serialized));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -237,24 +230,19 @@ public class BukkitSerializer {
|
||||
|
||||
}
|
||||
|
||||
public static class Json<T extends Data & Adaptable> extends BukkitSerializer implements Serializer<T> {
|
||||
/**
|
||||
* @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> {
|
||||
|
||||
private final Class<T> type;
|
||||
|
||||
public 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -67,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()
|
||||
));
|
||||
}
|
||||
@@ -78,80 +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(getBukkitPlayer(), getPlugin()));
|
||||
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(getBukkitPlayer()));
|
||||
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
|
||||
default Player getBukkitPlayer() {
|
||||
return getPlayer();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default BukkitMapPersister getMapPersister() {
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
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;
|
||||
@@ -40,21 +39,38 @@ import java.util.stream.Collectors;
|
||||
public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
|
||||
BukkitDeathEventListener, Listener {
|
||||
|
||||
protected final LockedHandler lockedHandler;
|
||||
protected LockedHandler lockedHandler;
|
||||
|
||||
public BukkitEventListener(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
this.lockedHandler = createLockedHandler(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().isDependencyLoaded("ProtocolLib") && getPlugin().getSettings().isCancelPackets()) {
|
||||
return new BukkitLockedPacketListener(plugin);
|
||||
} else {
|
||||
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
|
||||
@@ -134,8 +150,8 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
public BukkitHuskSync getPlugin() {
|
||||
return (BukkitHuskSync) plugin;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -50,6 +50,10 @@ public class BukkitLockedEventListener implements LockedHandler, Listener {
|
||||
|
||||
protected BukkitLockedEventListener(@NotNull BukkitHuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import com.github.retrooper.packetevents.PacketEvents;
|
||||
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
|
||||
import com.github.retrooper.packetevents.event.PacketListenerPriority;
|
||||
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
|
||||
import com.github.retrooper.packetevents.event.PacketSendEvent;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.google.common.collect.Sets;
|
||||
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
|
||||
|
||||
protected BukkitPacketEventsLockedPacketListener(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public void onLoad() {
|
||||
super.onLoad();
|
||||
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(getPlugin()));
|
||||
PacketEvents.getAPI().getSettings().reEncodeByDefault(false).checkForUpdates(false);
|
||||
PacketEvents.getAPI().load();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
super.onEnable();
|
||||
PacketEvents.getAPI().getEventManager().registerListener(new PlayerPacketAdapter(this));
|
||||
PacketEvents.getAPI().init();
|
||||
plugin.log(Level.INFO, "Using PacketEvents to cancel packets for locked players");
|
||||
}
|
||||
|
||||
private static class PlayerPacketAdapter extends PacketListenerAbstract {
|
||||
|
||||
private static final Set<PacketType.Play.Client> ALLOWED_PACKETS = Set.of(
|
||||
PacketType.Play.Client.KEEP_ALIVE, PacketType.Play.Client.PONG, PacketType.Play.Client.PLUGIN_MESSAGE, // Connection packets
|
||||
PacketType.Play.Client.PLAYER_LOADED, PacketType.Play.Client.CLIENT_TICK_END, // Connection packets
|
||||
PacketType.Play.Client.CHAT_MESSAGE, PacketType.Play.Client.CHAT_COMMAND, PacketType.Play.Client.CHAT_SESSION_UPDATE, // Chat / command packets
|
||||
PacketType.Play.Client.PLAYER_POSITION, PacketType.Play.Client.PLAYER_POSITION_AND_ROTATION, PacketType.Play.Client.PLAYER_ROTATION, // Movement packets
|
||||
PacketType.Play.Client.HELD_ITEM_CHANGE, PacketType.Play.Client.ANIMATION, PacketType.Play.Client.TELEPORT_CONFIRM, // Animation packets
|
||||
PacketType.Play.Client.CLIENT_SETTINGS // Video setting packets
|
||||
);
|
||||
|
||||
private static final Set<PacketType.Play.Client> CANCEL_PACKETS = getPacketsToListenFor();
|
||||
|
||||
|
||||
private final BukkitPacketEventsLockedPacketListener listener;
|
||||
|
||||
public PlayerPacketAdapter(@NotNull BukkitPacketEventsLockedPacketListener listener) {
|
||||
super(PacketListenerPriority.HIGH);
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
|
||||
return;
|
||||
}
|
||||
if (!CANCEL_PACKETS.contains(client)) {
|
||||
return;
|
||||
}
|
||||
if (listener.cancelPlayerEvent(event.getUser().getUUID())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketSend(PacketSendEvent event) {
|
||||
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
|
||||
return;
|
||||
}
|
||||
if (!CANCEL_PACKETS.contains(client)) {
|
||||
return;
|
||||
}
|
||||
if (listener.cancelPlayerEvent(event.getUser().getUUID())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the set of ALL Server packets, excluding the set of allowed packets
|
||||
@NotNull
|
||||
private static Set<PacketType.Play.Client> getPacketsToListenFor() {
|
||||
return Sets.difference(
|
||||
Sets.newHashSet(PacketType.Play.Client.values()),
|
||||
ALLOWED_PACKETS
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -34,17 +34,22 @@ import java.util.stream.Collectors;
|
||||
|
||||
import static com.comphenix.protocol.PacketType.Play.Client;
|
||||
|
||||
public class BukkitLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
|
||||
public class BukkitProtocolLibLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
|
||||
|
||||
protected BukkitLockedPacketListener(@NotNull BukkitHuskSync plugin) {
|
||||
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
|
||||
// Packets we want the player to still be able to send/receiver to/from the server - //todo update 1.21.4
|
||||
private static final Set<PacketType> ALLOWED_PACKETS = Set.of(
|
||||
Client.KEEP_ALIVE, Client.PONG, Client.CUSTOM_PAYLOAD, // Connection packets
|
||||
Client.CHAT_COMMAND, Client.CLIENT_COMMAND, Client.CHAT, Client.CHAT_SESSION_UPDATE, // Chat / command packets
|
||||
@@ -53,9 +58,9 @@ public class BukkitLockedPacketListener extends BukkitLockedEventListener implem
|
||||
Client.SETTINGS // Video setting packets
|
||||
);
|
||||
|
||||
private final BukkitLockedPacketListener listener;
|
||||
private final BukkitProtocolLibLockedPacketListener listener;
|
||||
|
||||
public PlayerPacketAdapter(@NotNull BukkitLockedPacketListener listener) {
|
||||
public PlayerPacketAdapter(@NotNull BukkitProtocolLibLockedPacketListener listener) {
|
||||
super(listener.getPlugin(), ListenerPriority.HIGHEST, getPacketsToListenFor());
|
||||
this.listener = listener;
|
||||
}
|
||||
@@ -204,10 +204,10 @@ public class LegacyMigrator extends Migrator {
|
||||
}) {
|
||||
plugin.log(Level.INFO, getHelpMenu());
|
||||
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]));
|
||||
obfuscateDataString(args[1]));
|
||||
} else {
|
||||
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||
}
|
||||
} else {
|
||||
plugin.log(Level.INFO, getHelpMenu());
|
||||
@@ -330,7 +330,7 @@ public class LegacyMigrator extends Migrator {
|
||||
))
|
||||
|
||||
// Health, hunger, experience & game mode
|
||||
.health(BukkitData.Health.from(health, 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))
|
||||
|
||||
@@ -201,10 +201,10 @@ public class MpdbMigrator extends Migrator {
|
||||
}) {
|
||||
plugin.log(Level.INFO, getHelpMenu());
|
||||
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]));
|
||||
obfuscateDataString(args[1]));
|
||||
} else {
|
||||
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||
}
|
||||
} else {
|
||||
plugin.log(Level.INFO, getHelpMenu());
|
||||
@@ -255,7 +255,7 @@ public class MpdbMigrator extends Migrator {
|
||||
If any of these are not correct, please correct them
|
||||
using the command:
|
||||
"husksync migrate mpdb set <parameter> <value>"
|
||||
(e.g.: "husksync migrate mpdb set host 1.2.3.4")
|
||||
(e.g.: "husksync migrate set mpdb host 1.2.3.4")
|
||||
|
||||
STEP 3] HuskSync will migrate data into the database
|
||||
tables configures in the config.yml file of this
|
||||
@@ -263,7 +263,7 @@ public class MpdbMigrator extends Migrator {
|
||||
before proceeding.
|
||||
|
||||
STEP 4] To start the migration, please run:
|
||||
"husksync migrate mpdb start"
|
||||
"husksync migrate start mpdb"
|
||||
|
||||
NOTE: This migrator currently WORKS WITH MPDB version
|
||||
v4.9.2 and below!
|
||||
|
||||
@@ -23,14 +23,10 @@ import de.themoep.minedown.adventure.MineDown;
|
||||
import dev.triumphteam.gui.builder.gui.StorageBuilder;
|
||||
import dev.triumphteam.gui.guis.Gui;
|
||||
import dev.triumphteam.gui.guis.StorageGui;
|
||||
import net.roxeez.advancement.display.FrameType;
|
||||
import net.william278.andjam.Toast;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.data.BukkitUserDataHolder;
|
||||
import net.william278.husksync.data.Data;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -40,8 +36,6 @@ import java.util.Arrays;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static net.william278.husksync.util.BukkitKeyedAdapter.matchMaterial;
|
||||
|
||||
/**
|
||||
* Bukkit platform implementation of an {@link OnlineUser}
|
||||
*/
|
||||
@@ -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
|
||||
@@ -103,7 +78,7 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
||||
if (!editable) {
|
||||
builder.disableAllInteractions();
|
||||
}
|
||||
final StorageGui gui = builder.enableOtherActions()
|
||||
final StorageGui gui = builder
|
||||
.apply(a -> a.getInventory().setContents(contents))
|
||||
.title(title.toComponent()).create();
|
||||
gui.setCloseGuiAction((close) -> onClose.accept(BukkitData.Items.ItemArray.adapt(
|
||||
@@ -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,38 +19,44 @@
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
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.Objects;
|
||||
|
||||
// Utility class for adapting "Keyed" Bukkit objects
|
||||
public final class BukkitKeyedAdapter {
|
||||
|
||||
@Nullable
|
||||
public static Statistic matchStatistic(@NotNull String key) {
|
||||
return Registry.STATISTIC.get(Objects.requireNonNull(NamespacedKey.fromString(key), "Null key"));
|
||||
return getRegistryValue(Registry.STATISTIC, key);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static EntityType matchEntityType(@NotNull String key) {
|
||||
return Registry.ENTITY_TYPE.get(Objects.requireNonNull(NamespacedKey.fromString(key), "Null key"));
|
||||
return getRegistryValue(Registry.ENTITY_TYPE, key);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Material matchMaterial(@NotNull String key) {
|
||||
return Registry.MATERIAL.get(Objects.requireNonNull(NamespacedKey.fromString(key), "Null key"));
|
||||
return getRegistryValue(Registry.MATERIAL, key);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Attribute matchAttribute(@NotNull String key) {
|
||||
return Registry.ATTRIBUTE.get(Objects.requireNonNull(NamespacedKey.fromString(key), "Null 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -82,32 +82,33 @@ 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("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")
|
||||
));
|
||||
}
|
||||
if (shouldImport(Identifier.FLIGHT_STATUS)) {
|
||||
if (Identifier.FLIGHT_STATUS.isEnabled()) {
|
||||
containers.put(Identifier.FLIGHT_STATUS, BukkitData.FlightStatus.from(
|
||||
status.getBoolean("is_flying"),
|
||||
status.getBoolean("is_flying")
|
||||
@@ -118,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();
|
||||
}
|
||||
|
||||
@@ -130,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();
|
||||
}
|
||||
|
||||
@@ -142,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();
|
||||
}
|
||||
|
||||
@@ -163,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();
|
||||
}
|
||||
|
||||
@@ -186,7 +187,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Statistics> readStatistics(@NotNull JSONObject object) {
|
||||
if (!object.has("statistics") || !shouldImport(Identifier.STATISTICS)) {
|
||||
if (!object.has("statistics") || !Identifier.STATISTICS.isEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -280,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 {
|
||||
|
||||
@@ -31,8 +31,10 @@ 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;
|
||||
@@ -94,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;
|
||||
@@ -117,7 +122,8 @@ public interface BukkitMapPersister {
|
||||
}
|
||||
|
||||
// Render the map
|
||||
final PersistentMapCanvas canvas = new PersistentMapCanvas(view);
|
||||
final int dataVersion = getPlugin().getDataVersion(getPlugin().getMinecraftVersion());
|
||||
final PersistentMapCanvas canvas = new PersistentMapCanvas(view, dataVersion);
|
||||
for (MapRenderer renderer : view.getRenderers()) {
|
||||
renderer.render(view, canvas, delegateRenderer);
|
||||
getPlugin().debug(String.format("Rendered locked map canvas to view (#%s)", view.getId()));
|
||||
@@ -135,6 +141,7 @@ public interface BukkitMapPersister {
|
||||
|
||||
@NotNull
|
||||
private ItemStack applyMapView(@NotNull ItemStack map) {
|
||||
final int dataVersion = getPlugin().getDataVersion(getPlugin().getMinecraftVersion());
|
||||
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
|
||||
NBT.get(map, nbt -> {
|
||||
if (!nbt.hasTag(MAP_DATA_KEY)) {
|
||||
@@ -173,8 +180,9 @@ public interface BukkitMapPersister {
|
||||
final MapData canvasData;
|
||||
try {
|
||||
getPlugin().debug("Deserializing map data from NBT and generating view...");
|
||||
canvasData = MapData.fromByteArray(Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY),
|
||||
"Map pixel data is null"));
|
||||
canvasData = MapData.fromByteArray(
|
||||
dataVersion,
|
||||
Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY), "Pixel data null!"));
|
||||
} catch (Throwable e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to deserialize map data from NBT", e);
|
||||
return;
|
||||
@@ -271,7 +279,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!");
|
||||
}
|
||||
@@ -289,6 +297,7 @@ public interface BukkitMapPersister {
|
||||
/**
|
||||
* A {@link MapRenderer} that can be used to render persistently serialized {@link MapData} to a {@link MapView}
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
class PersistentMapRenderer extends MapRenderer {
|
||||
|
||||
private final MapData canvasData;
|
||||
@@ -350,13 +359,16 @@ public interface BukkitMapPersister {
|
||||
/**
|
||||
* A {@link MapCanvas} implementation used for pre-rendering maps to be converted into {@link MapData}
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
class PersistentMapCanvas implements MapCanvas {
|
||||
|
||||
private final int mapDataVersion;
|
||||
private final MapView mapView;
|
||||
private final int[][] pixels = new int[128][128];
|
||||
private MapCursorCollection cursors;
|
||||
|
||||
private PersistentMapCanvas(@NotNull MapView mapView) {
|
||||
private PersistentMapCanvas(@NotNull MapView mapView, int mapDataVersion) {
|
||||
this.mapDataVersion = mapDataVersion;
|
||||
this.mapView = mapView;
|
||||
}
|
||||
|
||||
@@ -378,18 +390,38 @@ 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);
|
||||
return (byte) pixels[x][y];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPixelColor(int x, int y, @Nullable Color color) {
|
||||
pixels[x][y] = color == null ? -1 : MapPalette.matchColor(color);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Color getPixelColor(int x, int y) {
|
||||
return MapPalette.getColor((byte) pixels[x][y]);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Color getBasePixelColor(int x, int y) {
|
||||
return MapPalette.getColor((byte) pixels[x][y]);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -422,7 +454,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, ""),
|
||||
@@ -432,8 +464,9 @@ public interface BukkitMapPersister {
|
||||
cursor.getY()
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
return MapData.fromPixels(pixels, getDimension(), (byte) 2, banners, List.of());
|
||||
return MapData.fromPixels(mapDataVersion, pixels, getDimension(), (byte) 2, banners, List.of());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ description: '${description}'
|
||||
website: 'https://william278.net'
|
||||
folia-supported: true
|
||||
softdepend:
|
||||
- 'packetevents'
|
||||
- 'ProtocolLib'
|
||||
- 'MysqlPlayerDataBridge'
|
||||
- 'Plan'
|
||||
|
||||
@@ -3,24 +3,26 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'commons-io:commons-io:2.16.1'
|
||||
api 'org.apache.commons:commons-text:1.12.0'
|
||||
api 'commons-io:commons-io:2.18.0'
|
||||
api 'org.apache.commons:commons-text:1.13.0'
|
||||
api 'net.william278:minedown:1.8.2'
|
||||
api 'org.json:json:20240303'
|
||||
api 'com.google.code.gson:gson:2.10.1'
|
||||
api 'org.json:json:20250107'
|
||||
api 'com.google.code.gson:gson:2.11.0'
|
||||
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
|
||||
api 'de.exlll:configlib-yaml:4.5.0'
|
||||
api 'net.william278:paginedown:1.1.2'
|
||||
api 'net.william278:DesertWell:2.0.4'
|
||||
api('com.zaxxer:HikariCP:5.1.0') {
|
||||
api('com.zaxxer:HikariCP:6.2.1') {
|
||||
exclude module: 'slf4j-api'
|
||||
}
|
||||
|
||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||
compileOnly 'org.jetbrains:annotations:24.1.0'
|
||||
compileOnly 'net.kyori:adventure-api:4.16.0'
|
||||
compileOnly 'net.kyori:adventure-platform-api:4.3.2'
|
||||
compileOnly 'com.google.guava:guava:33.2.0-jre'
|
||||
compileOnly 'net.william278.uniform:uniform-common:1.3'
|
||||
compileOnly 'com.mojang:brigadier:1.1.8'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.36'
|
||||
compileOnly 'org.jetbrains:annotations:26.0.1'
|
||||
compileOnly 'net.kyori:adventure-api:4.18.0'
|
||||
compileOnly 'net.kyori:adventure-platform-api:4.3.4'
|
||||
compileOnly 'com.google.guava:guava:33.4.0-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"
|
||||
@@ -31,10 +33,10 @@ dependencies {
|
||||
|
||||
testImplementation "redis.clients:jedis:$jedis_version"
|
||||
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
|
||||
testImplementation 'com.google.guava:guava:33.2.0-jre'
|
||||
testImplementation 'com.google.guava:guava:33.4.0-jre'
|
||||
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
testCompileOnly 'de.exlll:configlib-yaml:4.5.0'
|
||||
testCompileOnly 'org.jetbrains:annotations:24.1.0'
|
||||
testCompileOnly 'org.jetbrains:annotations:26.0.1'
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.36'
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.config.ConfigProvider;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.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;
|
||||
@@ -39,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.*;
|
||||
@@ -52,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;
|
||||
|
||||
@@ -86,7 +88,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
*
|
||||
* @return the {@link RedisManager} implementation
|
||||
*/
|
||||
|
||||
@NotNull
|
||||
RedisManager getRedisManager();
|
||||
|
||||
@@ -98,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
|
||||
*
|
||||
@@ -150,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
|
||||
*
|
||||
@@ -159,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.
|
||||
@@ -193,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
|
||||
*
|
||||
@@ -275,6 +249,14 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
@NotNull
|
||||
Version getMinecraftVersion();
|
||||
|
||||
/**
|
||||
* Returns the data version for a Minecraft version
|
||||
*
|
||||
* @param minecraftVersion the Minecraft version
|
||||
* @return the data version int
|
||||
*/
|
||||
int getDataVersion(@NotNull Version minecraftVersion);
|
||||
|
||||
/**
|
||||
* Returns the platform type
|
||||
*
|
||||
@@ -283,6 +265,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
|
||||
*
|
||||
@@ -350,15 +340,19 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
private static final String FORMAT = """
|
||||
HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized.
|
||||
Please make sure the plugin has been setup correctly (https://william278.net/docs/husksync/setup):
|
||||
|
||||
|
||||
1) Make sure you've entered your MySQL, MariaDB or MongoDB database details correctly in config.yml
|
||||
2) Make sure your Redis server details are also correct in config.yml
|
||||
3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-file)
|
||||
4) Check the error below for more details
|
||||
|
||||
|
||||
Caused by: %s""";
|
||||
|
||||
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) {
|
||||
|
||||
@@ -99,6 +99,18 @@ public class HuskSyncAPI {
|
||||
return plugin.supplyAsync(() -> plugin.getDatabase().getUser(uuid));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an {@link OnlineUser} by their UUID
|
||||
*
|
||||
* @param uuid the UUID of the user to get
|
||||
* @return The {@link OnlineUser} wrapped in an optional, if they are online on <i>this</i> server.
|
||||
* @since 3.7.2
|
||||
*/
|
||||
@NotNull
|
||||
public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
|
||||
return plugin.getOnlineUser(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link User} by their username
|
||||
*
|
||||
@@ -378,6 +390,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}
|
||||
*
|
||||
@@ -500,17 +523,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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -37,7 +37,7 @@ 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
|
||||
|
||||
@@ -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,36 +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"))
|
||||
@@ -68,7 +67,9 @@ public class HuskSyncCommand extends Command implements TabProvider {
|
||||
.credits("Contributors",
|
||||
AboutMenu.Credit.of("HarvelsX").description("Code"),
|
||||
AboutMenu.Credit.of("HookWoods").description("Code"),
|
||||
AboutMenu.Credit.of("Preva1l").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)"),
|
||||
@@ -93,136 +94,167 @@ 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;
|
||||
}
|
||||
|
||||
switch (subCommand) {
|
||||
case "about" -> executor.sendMessage(aboutMenu.toComponent());
|
||||
case "status" -> {
|
||||
getPlugin().getLocales().getLocale("system_status_header").ifPresent(executor::sendMessage);
|
||||
executor.sendMessage(Component.join(
|
||||
JoinConfiguration.newlines(),
|
||||
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList()
|
||||
));
|
||||
}
|
||||
case "reload" -> {
|
||||
try {
|
||||
plugin.loadSettings();
|
||||
plugin.loadLocales();
|
||||
plugin.loadServer();
|
||||
plugin.getLocales().getLocale("reload_complete").ifPresent(executor::sendMessage);
|
||||
} catch (Throwable e) {
|
||||
executor.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 -> {
|
||||
if (checked.isUpToDate()) {
|
||||
plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString())
|
||||
.ifPresent(executor::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);
|
||||
}
|
||||
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());
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
private void about(@NotNull BaseCommand<?> c, @NotNull CommandContext<?> ctx) {
|
||||
user(c, ctx).getAudience().sendMessage(aboutMenu.toComponent());
|
||||
}
|
||||
|
||||
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());
|
||||
@NotNull
|
||||
private CommandProvider status() {
|
||||
return (sub) -> sub.setDefaultExecutor((ctx) -> {
|
||||
final CommandUser user = user(sub, ctx);
|
||||
plugin.getLocales().getLocale("system_status_header").ifPresent(user::sendMessage);
|
||||
user.sendMessage(Component.join(
|
||||
JoinConfiguration.newlines(),
|
||||
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList()
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider reload() {
|
||||
return (sub) -> sub.setDefaultExecutor((ctx) -> {
|
||||
final CommandUser user = user(sub, ctx);
|
||||
try {
|
||||
plugin.loadSettings();
|
||||
plugin.loadLocales();
|
||||
plugin.loadServer();
|
||||
plugin.getLocales().getLocale("reload_complete").ifPresent(user::sendMessage);
|
||||
} catch (Throwable e) {
|
||||
user.sendMessage(new MineDown(
|
||||
"[Error:](#ff3300) [Failed to reload the plugin. Check console for errors.](#ff7e5e)"
|
||||
));
|
||||
plugin.log(Level.SEVERE, "Failed to reload the plugin", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider update() {
|
||||
return (sub) -> sub.setDefaultExecutor((ctx) -> updateChecker.check().thenAccept(checked -> {
|
||||
final CommandUser user = user(sub, ctx);
|
||||
if (checked.isUpToDate()) {
|
||||
plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
return;
|
||||
}
|
||||
switch (args[2]) {
|
||||
case "start" -> migrator.start().thenAccept(succeeded -> {
|
||||
plugin.getLocales().getLocale("update_available", checked.getLatestVersion().toString(),
|
||||
plugin.getPluginVersion().toString()).ifPresent(user::sendMessage);
|
||||
}));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider migrate() {
|
||||
return (sub) -> {
|
||||
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
|
||||
sub.setDefaultExecutor((ctx) -> {
|
||||
plugin.log(Level.INFO, "Please choose a migrator, then run \"husksync migrate start <migrator>\"");
|
||||
plugin.log(Level.INFO, String.format(
|
||||
"List of available migrators:\nMigrator ID / Migrator Name:\n%s",
|
||||
plugin.getAvailableMigrators().stream()
|
||||
.map(migrator -> String.format("%s - %s", migrator.getIdentifier(), migrator.getName()))
|
||||
.collect(Collectors.joining("\n"))
|
||||
));
|
||||
});
|
||||
sub.addSubCommand("help", (help) -> help.addSyntax((cmd) -> {
|
||||
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
|
||||
plugin.log(Level.INFO, migrator.getHelpMenu());
|
||||
}, migrator()));
|
||||
sub.addSubCommand("start", (start) -> start.addSyntax((cmd) -> {
|
||||
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
|
||||
migrator.start().thenAccept(succeeded -> {
|
||||
if (succeeded) {
|
||||
plugin.log(Level.INFO, "Migration completed successfully!");
|
||||
} else {
|
||||
plugin.log(Level.WARNING, "Migration failed!");
|
||||
}
|
||||
});
|
||||
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() {
|
||||
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"))
|
||||
));
|
||||
}
|
||||
|
||||
@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;
|
||||
}, migrator()));
|
||||
sub.addSubCommand("set", (set) -> set.addSyntax((cmd) -> {
|
||||
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
|
||||
final String[] args = cmd.getArgument("args", String.class).split(" ");
|
||||
migrator.handleConfigurationCommand(args);
|
||||
}, migrator(), BaseCommand.greedyString("args")));
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CommandProvider forceUpgrade() {
|
||||
return (sub) -> {
|
||||
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
|
||||
sub.setDefaultExecutor((ctx) -> {
|
||||
final LegacyConverter converter = plugin.getLegacyConverter().orElse(null);
|
||||
if (converter == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.runAsync(() -> {
|
||||
final Database database = plugin.getDatabase();
|
||||
plugin.log(Level.INFO, "Beginning forced legacy data upgrade for all users...");
|
||||
database.getAllUsers().forEach(user -> database.getLatestSnapshot(user).ifPresent(snapshot -> {
|
||||
final DataSnapshot.Packed upgraded = converter.convert(
|
||||
snapshot.asBytes(plugin),
|
||||
UUID.randomUUID(),
|
||||
OffsetDateTime.now()
|
||||
);
|
||||
upgraded.setSaveCause(DataSnapshot.SaveCause.CONVERTED_FROM_V2);
|
||||
plugin.getDatabase().addSnapshot(user, upgraded);
|
||||
plugin.getRedisManager().clearUserData(user);
|
||||
}));
|
||||
plugin.log(Level.INFO, "Legacy data upgrade complete!");
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private <S> ArgumentElement<S, Migrator> migrator() {
|
||||
return new ArgumentElement<>("migrator", reader -> {
|
||||
final String id = reader.readString();
|
||||
final Migrator migrator = plugin.getAvailableMigrators().stream()
|
||||
.filter(m -> m.getIdentifier().equalsIgnoreCase(id)).findFirst().orElse(null);
|
||||
if (migrator == null) {
|
||||
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
|
||||
}
|
||||
return migrator;
|
||||
}, (context, builder) -> {
|
||||
for (Migrator material : plugin.getAvailableMigrators()) {
|
||||
builder.suggest(material.getIdentifier());
|
||||
}
|
||||
return builder.buildFuture();
|
||||
});
|
||||
}
|
||||
|
||||
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())),
|
||||
CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())),
|
||||
DATABASE_TYPE(plugin ->
|
||||
Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() +
|
||||
(plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ?
|
||||
(plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : ""))
|
||||
(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(
|
||||
@@ -239,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;
|
||||
@@ -274,7 +312,7 @@ public class HuskSyncCommand extends Command implements TabProvider {
|
||||
@NotNull
|
||||
private static Component getLocalhostBoolean(@NotNull String value) {
|
||||
return getBoolean(value.equals("127.0.0.1") || value.equals("0.0.0.0")
|
||||
|| value.equals("localhost") || value.equals("::1"));
|
||||
|| value.equals("localhost") || value.equals("::1"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ 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
|
||||
@@ -45,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;
|
||||
|
||||
@@ -24,47 +24,43 @@ 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)) {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the user data
|
||||
final User user = optionalUser.get();
|
||||
parseUUIDArg(args, 1).ifPresentOrElse(
|
||||
version -> this.showSnapshotItems(player, user, version),
|
||||
() -> this.showLatestItems(player, user)
|
||||
);
|
||||
public void provide(@NotNull BaseCommand<?> command) {
|
||||
command.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final UUID version = ctx.getArgument("version", UUID.class);
|
||||
final CommandUser executor = user(command, ctx);
|
||||
if (!(executor instanceof OnlineUser online)) {
|
||||
plugin.getLocales().getLocale("error_in_game_command_only")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
this.showSnapshotItems(online, user, version);
|
||||
}, user("username"), 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;
|
||||
}
|
||||
this.showLatestItems(online, user);
|
||||
}, user("username"));
|
||||
}
|
||||
|
||||
// View (and edit) the latest user data
|
||||
@@ -114,12 +110,4 @@ public abstract class ItemsCommand extends Command implements TabProvider {
|
||||
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,6 +19,7 @@
|
||||
|
||||
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;
|
||||
@@ -28,83 +29,32 @@ 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> uuid = parseUUIDArg(args, 2).or(() -> parseUUIDArg(args, 1));
|
||||
if (optionalUser.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
final User user = optionalUser.get();
|
||||
switch (subCommand) {
|
||||
case "view" -> uuid.ifPresentOrElse(
|
||||
version -> viewSnapshot(executor, user, version),
|
||||
() -> viewLatestSnapshot(executor, user)
|
||||
);
|
||||
case "list" -> listSnapshots(
|
||||
executor, user, parseIntArg(args, 2).or(() -> parseIntArg(args, 1)).orElse(1)
|
||||
);
|
||||
case "delete" -> uuid.ifPresentOrElse(
|
||||
version -> deleteSnapshot(executor, user, version),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata delete <username> <version_uuid>")
|
||||
.ifPresent(executor::sendMessage)
|
||||
);
|
||||
case "restore" -> uuid.ifPresentOrElse(
|
||||
version -> restoreSnapshot(executor, user, version),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata restore <username> <version_uuid>")
|
||||
.ifPresent(executor::sendMessage)
|
||||
);
|
||||
case "pin" -> uuid.ifPresentOrElse(
|
||||
version -> pinSnapshot(executor, user, version),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata pin <username> <version_uuid>")
|
||||
.ifPresent(executor::sendMessage)
|
||||
);
|
||||
case "dump" -> uuid.ifPresentOrElse(
|
||||
version -> dumpSnapshot(executor, user, version, parseStringArg(args, 3)
|
||||
.map(arg -> arg.equalsIgnoreCase("web")).orElse(false)),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata dump <web/file> <username> <version_uuid>")
|
||||
.ifPresent(executor::sendMessage)
|
||||
);
|
||||
default -> plugin.getLocales().getLocale("error_invalid_syntax", getUsage())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
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
|
||||
@@ -224,7 +174,8 @@ public class UserDataCommand extends Command implements TabProvider {
|
||||
}
|
||||
|
||||
// Dump a snapshot
|
||||
private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version, boolean webDump) {
|
||||
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")
|
||||
@@ -237,22 +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);
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -131,6 +131,15 @@ public interface ConfigProvider {
|
||||
));
|
||||
}
|
||||
|
||||
default void validateConfigFiles() {
|
||||
// Validate server name is default
|
||||
if (getServerName().equals("server")) {
|
||||
getPlugin().log(Level.WARNING, "The server name set in ~/plugins/HuskSync/server.yml appears to" +
|
||||
"be unchanged from the default (currently set to: \"server\"). Please check that this value has" +
|
||||
"been updated to match the case-sensitive ID of this server in your proxy config file!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a plugin resource
|
||||
*
|
||||
|
||||
@@ -193,14 +193,20 @@ public class Locales {
|
||||
* Displays the notification in the action bar
|
||||
*/
|
||||
ACTION_BAR,
|
||||
|
||||
/**
|
||||
* Displays the notification in the chat
|
||||
*/
|
||||
CHAT,
|
||||
|
||||
/**
|
||||
* Displays the notification in an Advancement Toast
|
||||
*
|
||||
* @deprecated No longer supported
|
||||
*/
|
||||
@Deprecated(since = "3.6.7")
|
||||
TOAST,
|
||||
|
||||
/**
|
||||
* Does not display the notification
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
@@ -63,21 +64,21 @@ public class Settings {
|
||||
private boolean checkForUpdates = true;
|
||||
|
||||
@Comment("Specify a common ID for grouping servers running HuskSync. "
|
||||
+ "Don't modify this unless you know what you're doing!")
|
||||
+ "Don't modify this unless you know what you're doing!")
|
||||
private String clusterId = "";
|
||||
|
||||
@Comment("Enable development debug logging")
|
||||
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 is installed")
|
||||
@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")
|
||||
@@ -140,6 +141,9 @@ public class Settings {
|
||||
@Getter(AccessLevel.NONE)
|
||||
private Map<String, String> tableNames = Database.TableName.getDefaults();
|
||||
|
||||
@Comment("Whether to run the creation SQL on the database when the server starts. Don't modify this unless you know what you're doing!")
|
||||
private boolean createTables = true;
|
||||
|
||||
@NotNull
|
||||
public String getTableName(@NotNull Database.TableName tableName) {
|
||||
return tableNames.getOrDefault(tableName.name().toLowerCase(Locale.ENGLISH), tableName.getDefaultName());
|
||||
@@ -155,7 +159,7 @@ public class Settings {
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class RedisSettings {
|
||||
|
||||
@Comment("Specify the credentials of your Redis database here. Set \"password\" to '' if you don't have one")
|
||||
@Comment("Specify the credentials of your Redis server here. Set \"password\" to '' if you don't have one")
|
||||
private RedisCredentials credentials = new RedisCredentials();
|
||||
|
||||
@Getter
|
||||
@@ -185,7 +189,7 @@ public class Settings {
|
||||
}
|
||||
|
||||
// Synchronization settings
|
||||
@Comment("Redis settings")
|
||||
@Comment("Data syncing settings")
|
||||
private SynchronizationSettings synchronization = new SynchronizationSettings();
|
||||
|
||||
@Getter
|
||||
@@ -228,7 +232,7 @@ public class Settings {
|
||||
private boolean enabled = false;
|
||||
|
||||
@Comment("What items to save in death snapshots? (DROPS or ITEMS_TO_KEEP). "
|
||||
+ "Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server.")
|
||||
+ "Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server.")
|
||||
private DeathItemsMode itemsToSave = DeathItemsMode.DROPS;
|
||||
|
||||
@Comment("Should a death snapshot still be created even if the items to save on the player's death are empty?")
|
||||
@@ -249,14 +253,14 @@ public class Settings {
|
||||
@Comment("Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing")
|
||||
private boolean compressData = true;
|
||||
|
||||
@Comment("Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)")
|
||||
@Comment("Where to display sync notifications (ACTION_BAR, CHAT or NONE)")
|
||||
private Locales.NotificationSlot notificationDisplaySlot = Locales.NotificationSlot.ACTION_BAR;
|
||||
|
||||
@Comment("Persist maps locked in a Cartography Table to let them be viewed on any server")
|
||||
private boolean persistLockedMaps = true;
|
||||
|
||||
@Comment("If using the DELAY sync method, how long should this server listen for Redis key data updates before "
|
||||
+ "pulling data from the database instead (i.e., if the user did not change servers).")
|
||||
+ "pulling data from the database instead (i.e., if the user did not change servers).")
|
||||
private int networkLatencyMilliseconds = 500;
|
||||
|
||||
@Comment({"Which data types to synchronize.", "Docs: https://william278.net/docs/husksync/sync-features"})
|
||||
@@ -266,10 +270,52 @@ public class Settings {
|
||||
@Comment("Commands which should be blocked before a player has finished syncing (Use * to block all commands)")
|
||||
private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*"));
|
||||
|
||||
@Comment({"For attribute syncing, which attributes should be ignored/skipped when syncing",
|
||||
"(e.g. ['minecraft:generic.max_health', 'minecraft:generic.attack_damage'])"})
|
||||
@Getter(AccessLevel.NONE)
|
||||
private List<String> ignoredAttributes = new ArrayList<>(List.of(""));
|
||||
@Comment("Configuration for how to sync attributes")
|
||||
private AttributeSettings attributes = new AttributeSettings();
|
||||
|
||||
@Getter
|
||||
@Configuration
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class AttributeSettings {
|
||||
|
||||
@Comment({"Which attribute types should be saved as part of attribute syncing. Supports wildcard matching.",
|
||||
"(e.g. ['minecraft:generic.max_health', 'minecraft:generic.*'])"})
|
||||
@Getter(AccessLevel.NONE)
|
||||
private List<String> syncedAttributes = new ArrayList<>(List.of(
|
||||
"minecraft:generic.max_health", "minecraft:max_health",
|
||||
"minecraft:generic.max_absorption", "minecraft:max_absorption",
|
||||
"minecraft:generic.luck", "minecraft:luck",
|
||||
"minecraft:generic.scale", "minecraft:scale",
|
||||
"minecraft:generic.step_height", "minecraft:step_height",
|
||||
"minecraft:generic.gravity", "minecraft:gravity"
|
||||
));
|
||||
|
||||
@Comment({"Which attribute modifiers should be saved. Supports wildcard matching.",
|
||||
"(e.g. ['minecraft:effect.speed', 'minecraft:effect.*'])"})
|
||||
@Getter(AccessLevel.NONE)
|
||||
private List<String> ignoredModifiers = new ArrayList<>(List.of(
|
||||
"minecraft:effect.*", "minecraft:creative_mode_*"
|
||||
));
|
||||
|
||||
private boolean matchesWildcard(@NotNull String pat, @NotNull String value) {
|
||||
if (!pat.contains(":")) {
|
||||
pat = "minecraft:%s".formatted(pat);
|
||||
}
|
||||
if (!value.contains(":")) {
|
||||
value = "minecraft:%s".formatted(value);
|
||||
}
|
||||
return pat.contains("*") ? value.matches(pat.replace("*", ".*")) : pat.equals(value);
|
||||
}
|
||||
|
||||
public boolean isIgnoredAttribute(@NotNull String attribute) {
|
||||
return syncedAttributes.stream().noneMatch(wildcard -> matchesWildcard(wildcard, attribute));
|
||||
}
|
||||
|
||||
public boolean isIgnoredModifier(@NotNull String modifier) {
|
||||
return ignoredModifiers.stream().anyMatch(wildcard -> matchesWildcard(wildcard, modifier));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Comment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts")
|
||||
@Getter(AccessLevel.NONE)
|
||||
@@ -283,10 +329,6 @@ public class Settings {
|
||||
return id.isCustom() || features.getOrDefault(id.getKeyValue(), id.isEnabledByDefault());
|
||||
}
|
||||
|
||||
public boolean isIgnoredAttribute(@NotNull String attribute) {
|
||||
return ignoredAttributes.contains(attribute);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) {
|
||||
try {
|
||||
@@ -297,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));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ 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;
|
||||
@@ -78,6 +82,7 @@ public interface Data {
|
||||
*/
|
||||
interface Inventory extends Items {
|
||||
|
||||
int INVENTORY_SLOT_COUNT = 41;
|
||||
String ITEMS_TAG = "items";
|
||||
String HELD_ITEM_SLOT_TAG = "held_item_slot";
|
||||
|
||||
@@ -110,7 +115,7 @@ public interface Data {
|
||||
* Data container holding data for ender chests
|
||||
*/
|
||||
interface EnderChest extends Items {
|
||||
|
||||
int ENDER_CHEST_SLOT_COUNT = 27;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -126,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
|
||||
@@ -149,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);
|
||||
@@ -333,27 +338,78 @@ public interface Data {
|
||||
|
||||
}
|
||||
|
||||
record Modifier(
|
||||
@NotNull UUID uuid,
|
||||
@NotNull String name,
|
||||
double amount,
|
||||
@SerializedName("operation") int operationType,
|
||||
@SerializedName("equipment_slot") int equipmentSlot
|
||||
) {
|
||||
@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) {
|
||||
return obj instanceof Modifier modifier && modifier.uuid.equals(uuid);
|
||||
if (obj instanceof Modifier other) {
|
||||
if (uuid != null && other.uuid != null) {
|
||||
return uuid.equals(other.uuid);
|
||||
}
|
||||
return name.equals(other.name);
|
||||
}
|
||||
return super.equals(obj);
|
||||
}
|
||||
|
||||
public double modify(double value) {
|
||||
return switch (operationType) {
|
||||
return switch (operation) {
|
||||
case 0 -> value + amount;
|
||||
case 1 -> value * amount;
|
||||
case 2 -> value * (1 + amount);
|
||||
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) {
|
||||
|
||||
@@ -45,13 +45,13 @@ public class DataException extends IllegalStateException {
|
||||
@AllArgsConstructor
|
||||
public enum Reason {
|
||||
INVALID_MINECRAFT_VERSION((plugin, snapshot) -> String.format("The Minecraft version of the snapshot (%s) is " +
|
||||
"newer than the server's version (%s). Ensure each server is on the same version of Minecraft.",
|
||||
"newer than the server's version (%s). Ensure each server is on the same version of Minecraft.",
|
||||
snapshot.getMinecraftVersion(), plugin.getMinecraftVersion())),
|
||||
INVALID_FORMAT_VERSION((plugin, snapshot) -> String.format("The format version of the snapshot (%s) is newer " +
|
||||
"than the server's version (%s). Ensure each server is running the same version of HuskSync.",
|
||||
"than the server's version (%s). Ensure each server is running the same version of HuskSync.",
|
||||
snapshot.getFormatVersion(), DataSnapshot.CURRENT_FORMAT_VERSION)),
|
||||
INVALID_PLATFORM_TYPE((plugin, snapshot) -> String.format("The platform type of the snapshot (%s) does " +
|
||||
"not match the server's platform type (%s). Ensure each server has the same platform type.",
|
||||
"not match the server's platform type (%s). Ensure each server has the same platform type.",
|
||||
snapshot.getPlatformType(), plugin.getPlatformType())),
|
||||
NO_LEGACY_CONVERTER((plugin, snapshot) -> String.format("No legacy converter to convert format version: %s",
|
||||
snapshot.getFormatVersion()));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -370,7 +370,7 @@ 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 String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@@ -381,7 +381,7 @@ public class DataSnapshot {
|
||||
}
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull 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;
|
||||
@@ -389,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(), getMinecraftVersion())
|
||||
)).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())
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -453,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();
|
||||
@@ -535,9 +535,9 @@ public class DataSnapshot {
|
||||
public Builder timestamp(@NotNull OffsetDateTime timestamp) {
|
||||
if (timestamp.isAfter(OffsetDateTime.now())) {
|
||||
throw new IllegalArgumentException("Data snapshots cannot have a timestamp set in the future! "
|
||||
+ "Make sure your database server time matches the server time.\n"
|
||||
+ "Current game server timestamp: " + OffsetDateTime.now() + " / "
|
||||
+ "Snapshot timestamp: " + timestamp);
|
||||
+ "Make sure your database server time matches the server time.\n"
|
||||
+ "Current game server timestamp: " + OffsetDateTime.now() + " / "
|
||||
+ "Snapshot timestamp: " + timestamp);
|
||||
}
|
||||
this.timestamp = timestamp;
|
||||
return this;
|
||||
@@ -913,6 +913,8 @@ public class DataSnapshot {
|
||||
|
||||
private final boolean fireDataSaveEvent;
|
||||
|
||||
private static Map<String, SaveCause> registry;
|
||||
|
||||
/**
|
||||
* Get or create a {@link SaveCause} from a name
|
||||
*
|
||||
@@ -921,7 +923,7 @@ public class DataSnapshot {
|
||||
*/
|
||||
@NotNull
|
||||
public static SaveCause of(@NotNull String name) {
|
||||
return new SaveCause(name.length() > 32 ? name.substring(0, 31) : name, true);
|
||||
return of(name,true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -933,7 +935,14 @@ public class DataSnapshot {
|
||||
*/
|
||||
@NotNull
|
||||
public static SaveCause of(@NotNull String name, boolean firesSaveEvent) {
|
||||
return new SaveCause(name.length() > 32 ? name.substring(0, 31) : name, firesSaveEvent);
|
||||
name = name.length() > 32 ? name.substring(0, 31) : name;
|
||||
|
||||
if (registry == null) registry = new HashMap<>();
|
||||
if (registry.containsKey(name)) return registry.get(name);
|
||||
|
||||
SaveCause cause = new SaveCause(name, firesSaveEvent);
|
||||
registry.put(cause.name(), cause);
|
||||
return cause;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -944,11 +953,10 @@ public class DataSnapshot {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Obsolete
|
||||
public static SaveCause[] values() {
|
||||
return new SaveCause[]{
|
||||
DISCONNECT, WORLD_SAVE, DEATH, SERVER_SHUTDOWN, INVENTORY_COMMAND, ENDERCHEST_COMMAND,
|
||||
BACKUP_RESTORE, API, MPDB_MIGRATION, LEGACY_MIGRATION, CONVERTED_FROM_V2
|
||||
};
|
||||
if (registry == null) registry = new HashMap<>();
|
||||
return registry.values().toArray(new SaveCause[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,40 +19,84 @@
|
||||
|
||||
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 org.jetbrains.annotations.Nullable;
|
||||
|
||||
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 ATTRIBUTES = huskSync("attributes", true);
|
||||
public static Identifier EXPERIENCE = huskSync("experience", true);
|
||||
public static Identifier GAME_MODE = huskSync("game_mode", true);
|
||||
public static Identifier FLIGHT_STATUS = huskSync("flight_status", 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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,10 +108,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());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,25 +124,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));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,6 +172,17 @@ public class Identifier {
|
||||
.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
|
||||
*
|
||||
@@ -169,11 +230,89 @@ public class Identifier {
|
||||
* @return {@code true} if the given object is an identifier with the same key as this identifier
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Identifier other) {
|
||||
return key.equals(other.key);
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
return obj instanceof Identifier other ? toString().equals(other.toString()) : super.equals(obj);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
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> {
|
||||
@@ -46,4 +48,26 @@ public interface Serializer<T extends Data> {
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
@@ -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.
|
||||
*
|
||||
@@ -106,12 +107,15 @@ 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.isCustom()) {
|
||||
getCustomDataStore().put(identifier, entry.getValue());
|
||||
}
|
||||
entry.getValue().apply(this, plugin);
|
||||
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);
|
||||
|
||||
@@ -107,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.
|
||||
|
||||
@@ -50,17 +50,13 @@ public class MongoDbDatabase extends Database {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the database and ensure tables are present; create tables if they do not exist.
|
||||
*
|
||||
* @throws IllegalStateException if the database could not be initialized
|
||||
*/
|
||||
@Override
|
||||
public void initialize() throws IllegalStateException {
|
||||
final Settings.DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
|
||||
@@ -68,6 +64,10 @@ public class MongoDbDatabase extends Database {
|
||||
ConnectionString URI = createConnectionURI(credentials);
|
||||
mongoConnectionHandler = new MongoConnectionHandler(URI, credentials.getDatabase());
|
||||
mongoCollectionHelper = new MongoCollectionHelper(mongoConnectionHandler);
|
||||
|
||||
// Check config for if tables should be created
|
||||
if (!plugin.getSettings().getDatabase().isCreateTables()) return;
|
||||
|
||||
if (mongoCollectionHelper.getCollection(usersTable) == null) {
|
||||
mongoCollectionHelper.createCollection(usersTable);
|
||||
}
|
||||
@@ -76,7 +76,7 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " +
|
||||
"Please check the supplied database credentials in the config file", e);
|
||||
"Please check the supplied database credentials in the config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,11 +93,6 @@ public class MongoDbDatabase extends Database {
|
||||
return new ConnectionString(baseURI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a {@link User} has an entry in the database and that their username is up-to-date
|
||||
*
|
||||
* @param user The {@link User} to ensure
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public void ensureUser(@NotNull User user) {
|
||||
@@ -107,13 +102,13 @@ public class MongoDbDatabase extends Database {
|
||||
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().toString());
|
||||
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("uuid", user.getUuid().toString());
|
||||
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);
|
||||
@@ -123,7 +118,7 @@ public class MongoDbDatabase extends Database {
|
||||
() -> {
|
||||
// Insert new player data into the database
|
||||
try {
|
||||
Document doc = new Document("uuid", user.getUuid().toString()).append("username", user.getUsername());
|
||||
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);
|
||||
@@ -135,12 +130,6 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a player by their Minecraft account {@link UUID}
|
||||
*
|
||||
* @param uuid Minecraft account {@link UUID} of the {@link User} to get
|
||||
* @return An optional with the {@link User} present if they exist
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<User> getUser(@NotNull UUID uuid) {
|
||||
@@ -148,8 +137,7 @@ public class MongoDbDatabase extends Database {
|
||||
Document filter = new Document("uuid", uuid);
|
||||
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
||||
if (doc != null) {
|
||||
return Optional.of(new User(UUID.fromString(doc.getString("uuid")),
|
||||
doc.getString("username")));
|
||||
return Optional.of(new User(uuid, doc.getString("username")));
|
||||
}
|
||||
return Optional.empty();
|
||||
} catch (MongoException e) {
|
||||
@@ -158,12 +146,6 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by their username (<i>case-insensitive</i>)
|
||||
*
|
||||
* @param username Username of the {@link User} to get (<i>case-insensitive</i>)
|
||||
* @return An optional with the {@link User} present if they exist
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<User> getUserByName(@NotNull String username) {
|
||||
@@ -171,7 +153,7 @@ public class MongoDbDatabase extends Database {
|
||||
Document filter = new Document("username", username);
|
||||
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
||||
if (doc != null) {
|
||||
return Optional.of(new User(UUID.fromString(doc.getString("uuid")),
|
||||
return Optional.of(new User(doc.get("uuid", UUID.class),
|
||||
doc.getString("username")));
|
||||
}
|
||||
return Optional.empty();
|
||||
@@ -181,22 +163,34 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest data snapshot for a user.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @return an optional containing the {@link DataSnapshot}, if it exists, or an empty optional if it does not
|
||||
*/
|
||||
@Override
|
||||
@NotNull
|
||||
public List<User> getAllUsers() {
|
||||
final List<User> users = Lists.newArrayList();
|
||||
try {
|
||||
final FindIterable<Document> doc = mongoCollectionHelper.getCollection(usersTable).find();
|
||||
for (Document document : doc) {
|
||||
users.add(new User(
|
||||
UUID.fromString(document.getString("uuid")),
|
||||
document.getString("username")
|
||||
));
|
||||
}
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get all users from the database", e);
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
||||
try {
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString());
|
||||
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 = UUID.fromString(doc.getString("version_uuid"));
|
||||
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();
|
||||
@@ -209,23 +203,17 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all {@link DataSnapshot} entries for a user from the database.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @return The list of a user's {@link DataSnapshot} entries
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
@NotNull
|
||||
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
|
||||
try {
|
||||
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString());
|
||||
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 = UUID.fromString(doc.getString("version_uuid"));
|
||||
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();
|
||||
@@ -238,18 +226,11 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @param versionUuid The UUID of the {@link DataSnapshot} entry to get
|
||||
* @return An optional containing the {@link DataSnapshot}, if it exists
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
try {
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString()).append("version_uuid", versionUuid.toString());
|
||||
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();
|
||||
@@ -266,12 +247,6 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal)</b> Prune user data for a given user to the maximum value as configured.
|
||||
*
|
||||
* @param user The user to prune data for
|
||||
* @implNote Data snapshots marked as {@code pinned} are exempt from rotation
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateSnapshots(@NotNull User user) {
|
||||
@@ -281,7 +256,7 @@ public class MongoDbDatabase extends Database {
|
||||
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
|
||||
if (unpinnedUserData.size() > maxSnapshots) {
|
||||
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString()).append("pinned", false);
|
||||
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)
|
||||
@@ -297,17 +272,11 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @param versionUuid The UUID of the {@link DataSnapshot} entry to delete
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
try {
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString()).append("version_uuid", versionUuid.toString());
|
||||
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;
|
||||
@@ -320,19 +289,11 @@ public class MongoDbDatabase extends Database {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the most recent data snapshot by the given {@link User user}
|
||||
* The snapshot must have been created after {@link OffsetDateTime time} and NOT be pinned
|
||||
* Facilities the backup frequency feature, reducing redundant snapshots from being saved longer than needed
|
||||
*
|
||||
* @param user The user to delete a snapshot for
|
||||
* @param within The time to delete a snapshot after
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
|
||||
try {
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString()).append("pinned", false);
|
||||
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)
|
||||
@@ -352,18 +313,12 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>Internal</b> - Create user data in the database
|
||||
*
|
||||
* @param user The user to add data for
|
||||
* @param data The {@link DataSnapshot} to set.
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try {
|
||||
Document doc = new Document("player_uuid", user.getUuid().toString())
|
||||
.append("version_uuid", data.getId().toString())
|
||||
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())
|
||||
@@ -374,17 +329,11 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a saved {@link DataSnapshot} by given version UUID
|
||||
*
|
||||
* @param user The user whose data snapshot
|
||||
* @param data The {@link DataSnapshot} to update
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try {
|
||||
Document doc = new Document("player_uuid", user.getUuid().toString()).append("version_uuid", data.getId().toString());
|
||||
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()),
|
||||
@@ -396,10 +345,6 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipes <b>all</b> {@link User} entries from the database.
|
||||
* <b>This should only be used when preparing tables for a data migration.</b>
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public void wipeDatabase() {
|
||||
@@ -410,9 +355,6 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
@Override
|
||||
public void terminate() {
|
||||
if (mongoConnectionHandler != null) {
|
||||
|
||||
@@ -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));
|
||||
@@ -124,11 +127,11 @@ public class MySqlDatabase extends Database {
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new IllegalStateException("Failed to create database tables. Please ensure you are running MySQL v8.0+ " +
|
||||
"and that your connecting user account has privileges to create tables.", e);
|
||||
"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 MySQL database. " +
|
||||
"Please check the supplied database credentials in the config file", e);
|
||||
"Please check the supplied database credentials in the config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -51,12 +51,6 @@ public class PostgresDatabase extends Database {
|
||||
this.driverClass = "org.postgresql.Driver";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the auto-closeable connection from the hikariDataSource
|
||||
*
|
||||
* @return The {@link Connection} to the MySQL database
|
||||
* @throws SQLException if the connection fails for some reason
|
||||
*/
|
||||
@Blocking
|
||||
@NotNull
|
||||
private Connection getConnection() throws SQLException {
|
||||
@@ -114,6 +108,9 @@ public class PostgresDatabase extends Database {
|
||||
);
|
||||
dataSource.setDataSourceProperties(properties);
|
||||
|
||||
// Check config for if tables should be created
|
||||
if (!plugin.getSettings().getDatabase().isCreateTables()) return;
|
||||
|
||||
// Prepare database schema; make tables if they don't exist
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
final String[] databaseSchema = getSchemaStatements(String.format("database/%s_schema.sql", flavor));
|
||||
@@ -123,11 +120,11 @@ public class PostgresDatabase extends Database {
|
||||
}
|
||||
} 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);
|
||||
"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);
|
||||
"Please check the supplied database credentials in the config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,9 +137,9 @@ public class PostgresDatabase extends Database {
|
||||
// Update a user's name if it has changed in the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE "%users_table%"
|
||||
SET "username"=?
|
||||
WHERE "uuid"=?"""))) {
|
||||
UPDATE %users_table%
|
||||
SET username=?
|
||||
WHERE uuid=?;"""))) {
|
||||
|
||||
statement.setString(1, user.getUsername());
|
||||
statement.setObject(2, existingUser.getUuid());
|
||||
@@ -158,7 +155,7 @@ public class PostgresDatabase extends Database {
|
||||
// Insert new player data into the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO "%users_table%" ("uuid","username")
|
||||
INSERT INTO %users_table% (uuid,username)
|
||||
VALUES (?,?);"""))) {
|
||||
|
||||
statement.setObject(1, user.getUuid());
|
||||
@@ -177,9 +174,9 @@ public class PostgresDatabase extends Database {
|
||||
public Optional<User> getUser(@NotNull UUID uuid) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT "uuid", "username"
|
||||
FROM "%users_table%"
|
||||
WHERE "uuid"=?"""))) {
|
||||
SELECT uuid, username
|
||||
FROM %users_table%
|
||||
WHERE uuid=?;"""))) {
|
||||
|
||||
statement.setObject(1, uuid);
|
||||
|
||||
@@ -200,9 +197,9 @@ public class PostgresDatabase extends Database {
|
||||
public Optional<User> getUserByName(@NotNull String username) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT "uuid", "username"
|
||||
FROM "%users_table%"
|
||||
WHERE "username"=?"""))) {
|
||||
SELECT uuid, username
|
||||
FROM %users_table%
|
||||
WHERE username=?;"""))) {
|
||||
statement.setString(1, username);
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
@@ -217,15 +214,37 @@ public class PostgresDatabase extends Database {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public List<User> getAllUsers() {
|
||||
final List<User> users = Lists.newArrayList();
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT uuid, username
|
||||
FROM %users_table%;
|
||||
"""))) {
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
users.add(new User(UUID.fromString(resultSet.getString("uuid")),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user by name from the database", e);
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT "version_uuid", "timestamp", "data"
|
||||
FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=?
|
||||
ORDER BY "timestamp" DESC
|
||||
SELECT version_uuid, timestamp, data
|
||||
FROM %user_data_table%
|
||||
WHERE player_uuid=?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
@@ -251,10 +270,10 @@ public class PostgresDatabase extends Database {
|
||||
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT "version_uuid", "timestamp", "data"
|
||||
FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=?
|
||||
ORDER BY "timestamp" DESC;"""))) {
|
||||
SELECT version_uuid, timestamp, data
|
||||
FROM %user_data_table%
|
||||
WHERE player_uuid=?
|
||||
ORDER BY timestamp DESC;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
@@ -278,10 +297,10 @@ public class PostgresDatabase extends Database {
|
||||
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT "version_uuid", "timestamp", "data"
|
||||
FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=? AND "version_uuid"=?
|
||||
ORDER BY "timestamp" DESC
|
||||
SELECT version_uuid, timestamp, data
|
||||
FROM %user_data_table%
|
||||
WHERE player_uuid=? AND version_uuid=?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.setObject(2, versionUuid);
|
||||
@@ -309,11 +328,16 @@ public class PostgresDatabase extends Database {
|
||||
if (unpinnedUserData.size() > maxSnapshots) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=?
|
||||
AND "pinned" = FALSE
|
||||
ORDER BY "timestamp" ASC
|
||||
LIMIT %entry_count%;""".replace("%entry_count%",
|
||||
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();
|
||||
@@ -329,11 +353,10 @@ public class PostgresDatabase extends Database {
|
||||
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=? AND "version_uuid"=?
|
||||
LIMIT 1;"""))) {
|
||||
DELETE FROM %user_data_table%
|
||||
WHERE player_uuid=? AND version_uuid=?;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
statement.setObject(2, versionUuid);
|
||||
return statement.executeUpdate() > 0;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
@@ -347,12 +370,12 @@ public class PostgresDatabase extends Database {
|
||||
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=? AND "timestamp" = (
|
||||
SELECT "timestamp"
|
||||
FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=? AND "timestamp" > ? AND "pinned" = FALSE
|
||||
ORDER BY "timestamp" ASC
|
||||
DELETE FROM %user_data_table%
|
||||
WHERE player_uuid=? AND timestamp = (
|
||||
SELECT timestamp
|
||||
FROM %user_data_table%
|
||||
WHERE player_uuid=? AND timestamp > ? AND pinned=FALSE
|
||||
ORDER BY timestamp ASC
|
||||
LIMIT 1
|
||||
);"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
@@ -370,8 +393,8 @@ public class PostgresDatabase extends Database {
|
||||
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO "%user_data_table%"
|
||||
("player_uuid","version_uuid","timestamp","save_cause","pinned","data")
|
||||
INSERT INTO %user_data_table%
|
||||
(player_uuid,version_uuid,timestamp,save_cause,pinned,data)
|
||||
VALUES (?,?,?,?,?,?);"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.setObject(2, data.getId());
|
||||
@@ -391,10 +414,10 @@ public class PostgresDatabase extends Database {
|
||||
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE "%user_data_table%"
|
||||
SET "save_cause"=?,"pinned"=?,"data"=?
|
||||
WHERE "player_uuid"=? AND "version_uuid"=?
|
||||
LIMIT 1;"""))) {
|
||||
UPDATE %user_data_table%
|
||||
SET save_cause=?,pinned=?,data=?
|
||||
WHERE player_uuid=? AND version_uuid=?;
|
||||
"""))) {
|
||||
statement.setString(1, data.getSaveCause().name());
|
||||
statement.setBoolean(2, data.isPinned());
|
||||
statement.setBytes(3, data.asBytes(plugin));
|
||||
@@ -411,7 +434,7 @@ public class PostgresDatabase extends Database {
|
||||
public void wipeDatabase() {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
statement.executeUpdate(formatStatementTables("DELETE FROM \"%user_data_table%\";"));
|
||||
statement.executeUpdate(formatStatementTables("DELETE FROM %user_data_table%;"));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to wipe the database", e);
|
||||
|
||||
@@ -29,6 +29,7 @@ public class MongoCollectionHelper {
|
||||
|
||||
/**
|
||||
* Initialize the collection helper
|
||||
*
|
||||
* @param database Instance of {@link MongoConnectionHandler}
|
||||
*/
|
||||
public MongoCollectionHelper(@NotNull MongoConnectionHandler database) {
|
||||
@@ -37,6 +38,7 @@ public class MongoCollectionHelper {
|
||||
|
||||
/**
|
||||
* Create a collection
|
||||
*
|
||||
* @param collectionName the collection name
|
||||
*/
|
||||
public void createCollection(@NotNull String collectionName) {
|
||||
@@ -45,6 +47,7 @@ public class MongoCollectionHelper {
|
||||
|
||||
/**
|
||||
* Delete a collection
|
||||
*
|
||||
* @param collectionName the collection name
|
||||
*/
|
||||
public void deleteCollection(@NotNull String collectionName) {
|
||||
@@ -53,6 +56,7 @@ public class MongoCollectionHelper {
|
||||
|
||||
/**
|
||||
* Get a collection
|
||||
*
|
||||
* @param collectionName the collection name
|
||||
* @return MongoCollection<Document>
|
||||
*/
|
||||
@@ -62,8 +66,9 @@ public class MongoCollectionHelper {
|
||||
|
||||
/**
|
||||
* Add a document to a collection
|
||||
*
|
||||
* @param collectionName collection to add to
|
||||
* @param document Document to add
|
||||
* @param document Document to add
|
||||
*/
|
||||
public void insertDocument(@NotNull String collectionName, @NotNull Document document) {
|
||||
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
||||
@@ -72,9 +77,10 @@ public class MongoCollectionHelper {
|
||||
|
||||
/**
|
||||
* Update a document
|
||||
*
|
||||
* @param collectionName collection the document is in
|
||||
* @param document filter of document
|
||||
* @param updates Bson of updates
|
||||
* @param document filter of document
|
||||
* @param updates Bson of updates
|
||||
*/
|
||||
public void updateDocument(@NotNull String collectionName, @NotNull Document document, @NotNull Bson updates) {
|
||||
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
||||
@@ -83,8 +89,9 @@ public class MongoCollectionHelper {
|
||||
|
||||
/**
|
||||
* Delete a document
|
||||
*
|
||||
* @param collectionName collection the document is in
|
||||
* @param document filter to remove
|
||||
* @param document filter to remove
|
||||
*/
|
||||
public void deleteDocument(@NotNull String collectionName, @NotNull Document document) {
|
||||
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
||||
|
||||
@@ -35,9 +35,10 @@ public class MongoConnectionHandler {
|
||||
|
||||
/**
|
||||
* Initiate a connection to a Mongo Server
|
||||
*
|
||||
* @param uri The connection string
|
||||
*/
|
||||
public MongoConnectionHandler(@NotNull ConnectionString uri, @NotNull String databaseName) {
|
||||
public MongoConnectionHandler(@NotNull ConnectionString uri, @NotNull String databaseName) {
|
||||
try {
|
||||
final MongoClientSettings settings = MongoClientSettings.builder()
|
||||
.applyConnectionString(uri)
|
||||
@@ -48,7 +49,7 @@ public class MongoConnectionHandler {
|
||||
this.database = mongoClient.getDatabase(databaseName);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " +
|
||||
"Please check the supplied database credentials in the config file", e);
|
||||
"Please check the supplied database credentials in the config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -53,7 +53,7 @@ public abstract class EventListener {
|
||||
return;
|
||||
}
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
plugin.getDataSyncer().setUserData(user);
|
||||
plugin.getDataSyncer().syncApplyUserData(user);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +66,7 @@ public abstract class EventListener {
|
||||
return;
|
||||
}
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
plugin.getDataSyncer().saveUserData(user);
|
||||
plugin.getDataSyncer().syncSaveUserData(user);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +94,7 @@ public abstract class EventListener {
|
||||
protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull Data.Items items) {
|
||||
final SaveOnDeathSettings settings = plugin.getSettings().getSynchronization().getSaveOnDeath();
|
||||
if (plugin.isDisabling() || !settings.isEnabled() || plugin.isLocked(user.getUuid())
|
||||
|| user.isNpc() || (!settings.isSaveEmptyItems() && items.isEmpty())) {
|
||||
|| user.isNpc() || (!settings.isSaveEmptyItems() && items.isEmpty())) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ public abstract class EventListener {
|
||||
/**
|
||||
* 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())
|
||||
|
||||
@@ -54,4 +54,16 @@ public interface LockedHandler {
|
||||
@ApiStatus.Internal
|
||||
HuskSync getPlugin();
|
||||
|
||||
default void onLoad() {
|
||||
|
||||
}
|
||||
|
||||
default void onEnable() {
|
||||
|
||||
}
|
||||
|
||||
default void onDisable() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ public class RedisManager extends JedisPubSub {
|
||||
jedisPool.getResource().ping();
|
||||
} catch (JedisException e) {
|
||||
throw new IllegalStateException("Failed to establish connection with Redis. "
|
||||
+ "Please check the supplied credentials in the config file", e);
|
||||
+ "Please check the supplied credentials in the config file", e);
|
||||
}
|
||||
|
||||
// Subscribe using a thread (rather than a task)
|
||||
@@ -159,6 +159,7 @@ public class RedisManager extends JedisPubSub {
|
||||
switch (messageType) {
|
||||
case UPDATE_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
|
||||
user -> {
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
try {
|
||||
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload());
|
||||
user.applySnapshot(data, DataSnapshot.UpdateCause.UPDATED);
|
||||
@@ -281,16 +282,21 @@ public class RedisManager extends JedisPubSub {
|
||||
@Blocking
|
||||
public void setUserCheckedOut(@NotNull User user, boolean checkedOut) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
final String key = getKeyString(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId);
|
||||
if (checkedOut) {
|
||||
jedis.set(
|
||||
getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId),
|
||||
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);
|
||||
}
|
||||
@@ -418,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,
|
||||
|
||||
@@ -81,18 +81,18 @@ 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,
|
||||
@@ -150,7 +150,7 @@ public abstract class DataSyncer {
|
||||
private long getMaxListenAttempts() {
|
||||
return BASE_LISTEN_ATTEMPTS + (
|
||||
(Math.max(100, plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds()) / 1000)
|
||||
* 20 / LISTEN_DELAY
|
||||
* 20 / LISTEN_DELAY
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ public class DelayDataSyncer extends DataSyncer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserData(@NotNull OnlineUser user) {
|
||||
public void syncApplyUserData(@NotNull OnlineUser user) {
|
||||
plugin.runAsyncDelayed(
|
||||
() -> {
|
||||
// Fetch from the database if the user isn't changing servers
|
||||
@@ -58,7 +58,7 @@ public class DelayDataSyncer extends DataSyncer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveUserData(@NotNull OnlineUser onlineUser) {
|
||||
public void syncSaveUserData(@NotNull OnlineUser onlineUser) {
|
||||
plugin.runAsync(() -> {
|
||||
getRedis().setUserServerSwitch(onlineUser);
|
||||
saveData(
|
||||
|
||||
@@ -43,7 +43,7 @@ public class LockstepDataSyncer extends DataSyncer {
|
||||
|
||||
// Consume their data when they are checked in
|
||||
@Override
|
||||
public void setUserData(@NotNull OnlineUser user) {
|
||||
public void syncApplyUserData(@NotNull OnlineUser user) {
|
||||
this.listenForRedisData(user, () -> {
|
||||
if (getRedis().getUserCheckedOut(user).isPresent()) {
|
||||
return false;
|
||||
@@ -58,17 +58,14 @@ public class LockstepDataSyncer extends DataSyncer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveUserData(@NotNull OnlineUser onlineUser) {
|
||||
plugin.runAsync(() -> {
|
||||
getRedis().setUserServerSwitch(onlineUser);
|
||||
saveData(
|
||||
onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
|
||||
(user, data) -> {
|
||||
getRedis().setUserData(user, data, RedisKeyType.TTL_1_YEAR);
|
||||
getRedis().setUserCheckedOut(user, false);
|
||||
}
|
||||
);
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -89,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);
|
||||
|
||||
@@ -145,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,11 +179,11 @@ public class DataDumper {
|
||||
@NotNull
|
||||
private String getFileName() {
|
||||
return new StringJoiner("_")
|
||||
.add(user.getUsername())
|
||||
.add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")))
|
||||
.add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH))
|
||||
.add(snapshot.getShortId())
|
||||
+ ".json";
|
||||
.add(user.getUsername())
|
||||
.add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")))
|
||||
.add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH))
|
||||
.add(snapshot.getShortId())
|
||||
+ ".json";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ public class DataSnapshotOverview {
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
if (user.hasPermission("husksync.command.inventory.edit")
|
||||
&& user.hasPermission("husksync.command.enderchest.edit")) {
|
||||
&& user.hasPermission("husksync.command.enderchest.edit")) {
|
||||
locales.getLocale("data_manager_item_buttons", dataOwner.getUsername(), snapshot.getId().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
|
||||
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}'
|
||||
@@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS "%user_data_table%"
|
||||
timestamp timestamp NOT NULL,
|
||||
save_cause varchar(32) NOT NULL,
|
||||
pinned boolean NOT NULL DEFAULT FALSE,
|
||||
data longblob NOT NULL,
|
||||
data bytea NOT NULL,
|
||||
|
||||
PRIMARY KEY (version_uuid, player_uuid),
|
||||
FOREIGN KEY (player_uuid) REFERENCES "%users_table%" (uuid) ON DELETE CASCADE
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
locales:
|
||||
synchronization_complete: '[⏵ 資料已同步!](#00fb9a)'
|
||||
synchronization_complete: '[⏵資料已同步!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ 無法同步您的資料! 請聯繫管理員](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1% 的背包'
|
||||
ender_chest_viewer_menu_title: '&0%1% 的終界箱'
|
||||
inventory_viewer_opened: '[查看備份](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的背包備份](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[查看備份](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的終界箱備份](#00fb9a)'
|
||||
data_update_complete: '[🔔 你的數據資料已更新!](#00fb9a)'
|
||||
data_update_failed: '[🔔 無法更新你的數據資料! 請聯繫管理員](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ 用戶註冊完成!](#00fb9a)'
|
||||
data_manager_title: '[正在查看玩家](#00fb9a) [%3%](#00fb9a bold show_text=&7玩家UUID:\n&8%4%) [的數據備份](#00fb9a) [%1%](#00fb9a show_text=&7備份版本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=&7伺服器:\n&8數據保存所在伺服器的名稱)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7備份大小:\n&8備份的估計文件大小(以KiB為單位))\n'
|
||||
inventory_viewer_opened: '[查看](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的背包快照資料](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[查看](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的終界箱快照資料](#00fb9a)'
|
||||
data_update_complete: '[🔔 你的資料已更新!](#00fb9a)'
|
||||
data_update_failed: '[🔔 無法更新您的資料! 請聯繫管理員](#ff7e5e)'
|
||||
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=&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%小時](color=#62a9f5-#7ab8fa show_text=&7在遊戲内遊玩的時間\n&8⚠ 基於遊戲内的統計訊息)\n'
|
||||
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%)'
|
||||
data_manager_management_buttons: '[管理:](gray) [[❌ 刪除…]](#ff3300 show_text=&7點擊刪除此玩家數據的備份.\n&8這不會影響玩家的當前數據.\n&#ff3300&⚠ 此操作無法撤銷! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ 恢復…]](#00fb9a show_text=&7點擊還原此玩家的數據.\n&8這將會讓用户的數據恢復到此備份.\n&#ff3300&⚠ %1%當前的數據將被覆蓋! suggest_command=/husksync:userdata restore %1% %2%) [[※ 標記/取消標記…]](#d8ff2b show_text=&7點擊標記或取消標記此玩家數據備份\n&8已標記的備份不會按照備份時間自動排序 run_command=/userdata pin %1% %2%)'
|
||||
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% 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=&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&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%)'
|
||||
data_unpinned: '[※ 成功取消標記玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的數據備份](#00fb9a) [%1%.](#00fb9a show_text=&7備份版本UUID:\n&8%2%)'
|
||||
data_dumped: '[☂ 已成功將 %1% 的玩家數據快照 %2% 儲存至:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[頁數](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
data_manager_management_buttons: '[管理:](gray) [[❌ 刪除…]](#ff3300 show_text=&7點擊刪除這個快照\n&8這不會影像目前玩家的資料\n&#ff3300&⚠ 此操作不能取消! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ 恢復…]](#00fb9a show_text=&7點擊將玩家資料覆蓋為此快照\n&8這將導致玩家的資料會被此快照覆蓋\n&#ff3300&⚠ %1% 當前的資料將被覆蓋! suggest_command=/husksync:userdata restore %1% %2%) [[※ 標記…]](#d8ff2b show_text=&7點擊切換標記狀態\n&8被標記的快照將不會自動輪換更新 run_command=/userdata pin %1% %2%)'
|
||||
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=&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%)'
|
||||
data_unpinned: '[※ 成功解除](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [的標記](#00fb9a)'
|
||||
data_dumped: '[☂ 成功將 %2% 資料快照 %1% 儲存至:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[頁面](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7查看上一頁 run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7查看下一頁 run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
@@ -36,30 +36,30 @@ locales:
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: '斷開連接'
|
||||
save_cause_world_save: '保存世界'
|
||||
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遷移'
|
||||
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%(當前版本:v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 已重新載入配置和訊息文件.](#00fb9a)\n[⚠ 確保所有伺服器上的配置文件都是最新的!](#00fb9a)\n[需要重新啟動配置更改才能生效.](#00fb9a italic)'
|
||||
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[⚠ 確保所有伺服器上的配置文件都是最新的!](#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: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %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: '錯誤: 該指令只能在遊戲內執行.'
|
||||
error_no_data_to_display: '[錯誤:](#ff3300) [找不到任何可顯示的玩家數據.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[錯誤:](#ff3300) [找不到該版本UUID的任何玩家數據.](#ff7e5e)'
|
||||
husksync_command_description: '管理HuskSync插件'
|
||||
userdata_command_description: '查看、管理和恢復玩家用户數據'
|
||||
inventory_command_description: '查看和編輯玩家的背包'
|
||||
error_in_game_command_only: '[錯誤:](#ff3300) [該指令只能在遊戲內執行](#ff7e5e)'
|
||||
error_no_data_to_display: '[錯誤:](#ff3300) [找不到任何可顯示的用戶資料.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[錯誤:](#ff3300) [找不到正確的 Version UUID.](#ff7e5e)'
|
||||
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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
> **Warning:** API v2 is no longer supported or compatible with HuskSync v3.0. See [[Data Snapshot API]] for the equivalent v3 API. 🚨
|
||||
|
||||
HuskSync v2.0 provides an API for fetching and retrieving `UserData`; a snapshot of a user's synchronization.
|
||||
|
||||
> **Warning:** API v2 is no longer supported or compatible with HuskSync v3.0. See [[Data Snapshot API]] for the equivalent v3 API. 🚨
|
||||
|
||||
This page assumes you've read the general [[API]] introduction and imported HuskSync (v2.x) into your project, and added it as a dependency.
|
||||
|
||||
🚨 HuskSync API v2 only targets HuskSync v2.0-2.2.8. It is **not compatible with HuskSync v3.0+**. The equivalent API for HuskSync v3 is the [[Data Snapshot API]].
|
||||
|
||||
10
docs/API.md
10
docs/API.md
@@ -17,9 +17,9 @@ 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.
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Targeting older versions</summary>
|
||||
|
||||
@@ -52,12 +52,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>
|
||||
```
|
||||
@@ -75,11 +75,11 @@ 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>
|
||||
|
||||
35
docs/Compatibility.md
Normal file
35
docs/Compatibility.md
Normal file
@@ -0,0 +1,35 @@
|
||||
HuskSync supports the following versions of Minecraft. Since v3.7, you must download the correct version of HuskSync for your server:
|
||||
|
||||
| Minecraft | Latest HuskSync | Java Version | Platforms | Support Status |
|
||||
|:---------------:|:---------------:|:------------:|:--------------|:-----------------------------|
|
||||
| 1.21.4 | _latest_ | 21 | Paper, Fabric | ✅ **Active Release** |
|
||||
| 1.21.3 | 3.7.1 | 21 | Paper, Fabric | 🗃️ Archived (December 2024) |
|
||||
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **November 2025** (LTS) |
|
||||
| 1.20.6 | 3.6.8 | 17 | Paper | 🗃️ Archived (October 2024) |
|
||||
| 1.20.4 | 3.6.8 | 17 | Paper | 🗃️ Archived (July 2024) |
|
||||
| 1.20.1 | _latest_ | 17 | Paper, Fabric | ✅ **November 2025** (LTS) |
|
||||
| 1.17.1 - 1.19.4 | 3.6.8 | 17 | Paper | 🗃️ Archived |
|
||||
| 1.16.5 | 3.2.1 | 16 | Paper | 🗃️ Archived |
|
||||
|
||||
HuskSync is primarily developed against the latest release. Old Minecraft versions are allocated a support channel based on popularity, mod support, etc:
|
||||
|
||||
* Long Term Support (LTS) – Supported for up to 12-18 months
|
||||
* Non-Long Term Support (Non-LTS) – Supported for 3-6 months
|
||||
|
||||
## Incompatible versions
|
||||
This plugin does not support the following software-Minecraft version combinations. The plugin will fail to load if you attempt to run it with these versions. Apologies for the inconvenience.
|
||||
|
||||
| Minecraft | Server Software | Notes |
|
||||
|-------------------|-------------------------------------------|----------------------------------------|
|
||||
| 1.19.4 | Only: `Purpur, Pufferfish`† | Older Paper builds also not supported. |
|
||||
| 1.19.3 | Only: `Paper, Purpur, Pufferfish`† | Upgrade to 1.19.4 or use Spigot |
|
||||
| 1.16.5 | _All_ | Please use v3.3.1 or lower |
|
||||
| below 1.16.5 | _All_ | Upgrade to Minecraft 1.16.5 |
|
||||
|
||||
†Further downstream forks of this server software are also affected.
|
||||
|
||||
## Incompatible plugins / mods
|
||||
Please note the following plugins / mods can cause issues with HuskSync:
|
||||
|
||||
* Restart plugins / mods are not supported. These will cause [player data to not save correctly when your server restarts](troubleshooting#issues-with-player-data-going-out-of-sync-during-a-server-restart) due to the way these plugins utilise bash scripts. It's important to understand that restart plugins don't actually restart yur server, they just trigger some (often unstable) process-killing scripting logic to occur!
|
||||
* Combat logging plugins / mods are not supported. Some have built-in support for HuskSync and should work as expected, but for others you may wish to modify the [[Event Priorities]]
|
||||
@@ -28,13 +28,13 @@ check_for_updates: true
|
||||
cluster_id: ''
|
||||
# Enable development debug logging
|
||||
debug_logging: true
|
||||
# Whether to provide modern, rich TAB suggestions for commands (if available)
|
||||
brigadier_tab_completion: false
|
||||
# Whether to enable the Player Analytics hook.
|
||||
# Docs: https://william278.net/docs/husksync/plan-hook
|
||||
enable_plan_hook: true
|
||||
# Whether to cancel game event packets directly when handling locked players if ProtocolLib is installed
|
||||
# Whether to cancel game event packets directly when handling locked players if ProtocolLib or PacketEvents is installed
|
||||
cancel_packets: true
|
||||
# Add HuskSync commands to this list to prevent them from being registered (e.g. ['userdata'])
|
||||
disabled_commands: []
|
||||
# Database settings
|
||||
database:
|
||||
# Type of database to use (MYSQL, MARIADB, POSTGRES, MONGO)
|
||||
@@ -65,7 +65,7 @@ database:
|
||||
user_data: husksync_user_data
|
||||
# Redis settings
|
||||
redis:
|
||||
# Specify the credentials of your Redis database here. Set "password" to '' if you don't have one
|
||||
# Specify the credentials of your Redis server here. Set "password" to '' if you don't have one
|
||||
credentials:
|
||||
host: localhost
|
||||
port: 6379
|
||||
@@ -78,7 +78,7 @@ redis:
|
||||
# List of host:port pairs
|
||||
nodes: []
|
||||
password: ''
|
||||
# Redis settings
|
||||
# Data syncing settings
|
||||
synchronization:
|
||||
# The data synchronization mode to use (LOCKSTEP or DELAY). LOCKSTEP is recommended for most networks.
|
||||
# Docs: https://william278.net/docs/husksync/sync-modes
|
||||
@@ -109,7 +109,7 @@ synchronization:
|
||||
sync_dead_players_changing_server: true
|
||||
# Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing
|
||||
compress_data: true
|
||||
# Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)
|
||||
# Where to display sync notifications (ACTION_BAR, CHAT or NONE)
|
||||
notification_display_slot: ACTION_BAR
|
||||
# Persist maps locked in a Cartography Table to let them be viewed on any server
|
||||
persist_locked_maps: true
|
||||
@@ -134,9 +134,26 @@ synchronization:
|
||||
# Commands which should be blocked before a player has finished syncing (Use * to block all commands)
|
||||
blacklisted_commands_while_locked:
|
||||
- '*'
|
||||
# For attribute syncing, which attributes should be ignored/skipped when syncing
|
||||
# (e.g. ['minecraft:generic.max_health', 'minecraft:generic.attack_damage'])
|
||||
ignored_attributes: []
|
||||
# Configuration for how to sync attributes
|
||||
attributes:
|
||||
# Which attribute types should be saved as part of attribute syncing. Supports wildcard matching.
|
||||
# (e.g. ['minecraft:generic.max_health', 'minecraft:generic.*'])
|
||||
synced_attributes:
|
||||
- "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"
|
||||
# Which attribute modifiers should not be saved when syncing users. Supports wildcard matching.
|
||||
# (e.g. ['minecraft:effect.speed', 'minecraft:effect.*'])
|
||||
ignored_modifiers: ['minecraft:effect.*', 'minecraft:creative_mode_*']
|
||||
# Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts
|
||||
event_priorities:
|
||||
quit_listener: LOWEST
|
||||
|
||||
@@ -93,7 +93,7 @@ public class LoginParticleData extends BukkitData implements Adaptable {
|
||||
public class LoginParticleSerializer extends BukkitSerializer.Json<LoginParticleData> implements Serializer<LoginParticleData> {
|
||||
|
||||
// We need to create a constructor that takes our instance of the API
|
||||
public GameMode(@NotNull HuskSyncAPI api) {
|
||||
public LoginParticleSerializer(@NotNull HuskSyncAPI api) {
|
||||
super(api, LoginParticleData.class); // We pass the class type here so that Gson knows what class we're serializing
|
||||
}
|
||||
|
||||
@@ -116,12 +116,26 @@ public static Identifier LOGIN_PARTICLES_ID = Identifier.from("myplugin", "login
|
||||
huskSyncAPI.registerSerializer(LOGIN_PARTICLES_ID, new LoginParticleSerializer(HuskSyncAPI.getInstance()));
|
||||
```
|
||||
|
||||
### 3.1 Identifier dependencies
|
||||
* HuskSync lets you specify a set of `Dependency` objects when creating an `Identifier`. These are used to deterministically apply data in a specific order.
|
||||
* Dependencies are references to other data type identifiers. HuskSync will apply data in dependency-order; that is, it will apply the data of the dependencies before applying the data of the dependent.
|
||||
* This is useful when you have data that relies on other data to be applied first; for example, if you're writing an add-on for additional modded inventory data and you need to apply the base inventory data first.
|
||||
* You can specify whether a dependency is required or optional. HuskSync will not sync data of a type that has a required dependency that is missing (for instance, if it is disabled in the config, or - if provided by another plugin - has failed to register).
|
||||
* Use `Identifer#from(String, String, Set<Dependency>)` or `Identifier#from(Key, Set<Dependency>)` to create an identifier with dependencies
|
||||
* Dependencies can be created with `Dependency.optional(Identifier)` or `Dependency.required(Identifier)` for optional or required dependencies respectively.
|
||||
|
||||
## 4. Setting and getting our Data to/from a User
|
||||
* Now that we've registered our `Data` and `Serializer` classes, we can set our data to a user, applying it to them.
|
||||
* To do this, we use the `OnlineUser#setData(Identifier, Data)` method.
|
||||
* This method will apply the data to the user, and store the data to the plugin player custom data map, to allow the data to be retrieved later and be saved to snapshots.
|
||||
* Snapshots created on servers where the data type is registered will now contain our data and synchronise between instances!
|
||||
|
||||
```java
|
||||
// Create an identifier for our data requiring the user's location to have been set first
|
||||
public static Identifier LOGIN_PARTICLES_ID = Identifier.from("myplugin", "login_particles", Set.of(Dependency.optional(Key.key("husksync", "location"))));
|
||||
// We can then register this as we did previously (...)
|
||||
```
|
||||
|
||||
```java
|
||||
// Create an instance of our data
|
||||
LoginParticleData loginParticleData = new LoginParticleData("FIREWORKS_SPARK", 10);
|
||||
@@ -134,7 +148,7 @@ LoginParticleData loginParticleData = (LoginParticleData) huskSyncAPI.getUser(pl
|
||||
```
|
||||
|
||||
### 4.1 Persisting custom data on the DataSaveEvent
|
||||
Add an EventListener to the `DataSaveEvent` and use the `#editData` consumer method to apply custom data during standard DataSaves. This will persist data to users any time the data save routine executes (on user logout, server shutdownm, world save, etc).
|
||||
Add an EventListener to the `DataSaveEvent` and use the `#editData` consumer method to apply custom data during standard DataSaves. This will persist data to users any time the data save routine executes (on user logout, server shutdown, world save, etc.)
|
||||
|
||||
```java
|
||||
@EventHandler
|
||||
|
||||
@@ -43,6 +43,7 @@ huskSyncAPI.getUser(uuid).thenAccept(optionalUser -> {
|
||||
</details>
|
||||
|
||||
* If you have an online `org.bukkit.Player` object, you can use `BukkitPlayer#adapt(player)` to get an `OnlineUser` (extends `User`), representing a logged-in user.
|
||||
* You can also use `#getOnlineUser(UUID)` to get an OnlineUser by their UUID - note this only works for players online on the server the logic is called from, however.
|
||||
|
||||
<details>
|
||||
<summary>Code Example — Getting an online user</summary>
|
||||
@@ -213,8 +214,8 @@ huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> {
|
||||
// Get the health data
|
||||
Data.Health health = healthOptional.get();
|
||||
double currentHealth = health.getCurrentHealth(); // Current health
|
||||
double healthScale = health.getHealthScale(); // Health scale (e.g., 20 for 20 hearts)
|
||||
snapshot.setHealth(BukkitData.Health.from(20, 20));
|
||||
double healthScale = health.getHealthScale(); // Health scale (used to determine health/damage display hearts)
|
||||
snapshot.setHealth(BukkitData.Health.from(20, 20, true));
|
||||
// Need max health? Look at the Attributes data type.
|
||||
|
||||
// Get the game mode data
|
||||
|
||||
73
docs/Database.md
Normal file
73
docs/Database.md
Normal file
@@ -0,0 +1,73 @@
|
||||
HuskSync persists player data and snapshots in a database of your choice. This is separate from a [[Redis]] server, which HuskSync uses for caching and inter-server messaging, which is also required to use HuskSync.
|
||||
|
||||
## Database types
|
||||
> **Warning:** There is no automatic way of migrating between _database_ types. Changing the database type will cause data to be lost.
|
||||
|
||||
| Type | Database Software |
|
||||
|:--------------------------|:--------------------------|
|
||||
| `MYSQL` | MySQL 8.0 or newer |
|
||||
| `MARIADB` | MariaDB 5.0 or newer |
|
||||
| `POSTGRES` | PostgreSQL |
|
||||
| [`MONGO`](#mongodb-setup) | MongoDB |
|
||||
|
||||
## Configuring
|
||||
To change the database type, navigate to your [`config.yml`](Config-File) file and modify the properties under `database`.
|
||||
|
||||
<details>
|
||||
<summary>Database options (config.yml)</summary>
|
||||
|
||||
```yaml
|
||||
# Database settings
|
||||
database:
|
||||
# Type of database to use (MYSQL, MARIADB, POSTGRES, MONGO)
|
||||
type: MYSQL
|
||||
# Specify credentials here for your MYSQL, MARIADB, POSTGRES OR MONGO database
|
||||
credentials:
|
||||
host: localhost
|
||||
port: 3306
|
||||
database: minecraft
|
||||
username: root
|
||||
password: ''
|
||||
# Only change this if you're using MARIADB or POSTGRES
|
||||
parameters: ?autoReconnect=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8
|
||||
# MYSQL, MARIADB, POSTGRES database Hikari connection pool properties. Don't modify this unless you know what you're doing!
|
||||
connection_pool:
|
||||
maximum_pool_size: 10
|
||||
minimum_idle: 10
|
||||
maximum_lifetime: 1800000
|
||||
keepalive_time: 0
|
||||
connection_timeout: 5000
|
||||
# Advanced MongoDB settings. Don't modify unless you know what you're doing!
|
||||
mongo_settings:
|
||||
using_atlas: false
|
||||
parameters: ?retryWrites=true&w=majority&authSource=HuskSync
|
||||
# Names of tables to use on your database. Don't modify this unless you know what you're doing!
|
||||
table_names:
|
||||
users: husksync_users
|
||||
user_data: husksync_user_data
|
||||
```
|
||||
</details>
|
||||
|
||||
### Credentials
|
||||
You will need to specify the credentials (hostname, port, username, password and the database). These credentials are used to connect to your database server.
|
||||
|
||||
If your database server account doesn't have a password (not recommended), leave the password field blank (`password: ''`') and the plugin will attempt to connect without a password.
|
||||
|
||||
### Connection Pool properties
|
||||
If you're using MySQL, MariaDB, or PostgreSQL as your database type, you can modify the HikariCP connection pool properties if you know what you're doing.
|
||||
|
||||
Please note that modifying these values can cause issues if you don't know what you're doing. The default values should be fine for most users.
|
||||
|
||||
## MongoDB Setup
|
||||
If you're using a MongoDB database, in addition to setting the database type to `MONGO`, you'll need to perform slightly different configuration steps.
|
||||
|
||||
- Under `credentials` in the `database` section, enter the credentials of your MongoDB Database. You shouldn't touch the `connection_pool` properties.
|
||||
- Under `parameters` in the `mongo_settings` section, ensure the specified `&authSource=` matches the database you are using (default is `HuskSync`).
|
||||
|
||||
### MongoDB Atlas setup
|
||||
If you're using a MongoDB Atlas database, you'll also need to set the Atlas settings and adjust the connection parameters string.
|
||||
|
||||
- Set `using_atlas` in the `mongo_settings` section to `true`.
|
||||
- Remove `&authSource=HuskSync` from `parameters` in the `mongo_settings`.
|
||||
|
||||
Note that the `port` setting in `credentials` is ignored when using Atlas.
|
||||
63
docs/FAQs.md
63
docs/FAQs.md
@@ -1,9 +1,9 @@
|
||||
This page addresses a number of frequently asked questions about the plugin.
|
||||
This page addresses a number of frequently asked questions about HuskSync.
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
<details>
|
||||
<summary> <b>What data can be synchronized?</b></summary>
|
||||
<summary> <b>What data can be synced?</b></summary>
|
||||
|
||||
HuskSync supports synchronising a wide range of different data elements, each of which can be toggled to your liking. Please check out the [[Sync Features]] page for a full list.
|
||||
|
||||
@@ -12,28 +12,74 @@ HuskSync supports synchronising a wide range of different data elements, each of
|
||||
<details>
|
||||
<summary> <b>Are modded items supported?</b></summary>
|
||||
|
||||
If you're running HuskSync on Arclight or similar, please note we will not be able to provide you with support, but have been reported to save & sync correctly with HuskSync v3.x+.
|
||||
On Fabric, modded items should usually sync as you would expect with HuskSync. Note that mods which store additional data separate from item NBT on each server may not work as expected. Mod developers — check out the [[Custom Data API]] for information on how to get your mod's data syncing!
|
||||
|
||||
**TL;DR** — modded items may work, but since we can't guarantee compatibility, we do not officially mark them as supported. Be sure to test thoroughly before deploying on production!
|
||||
On Spigot, if you're running HuskSync on Arclight or similar, please note we will not be able to provide you with support, but have been reported to save & sync correctly with HuskSync v3.x+.
|
||||
|
||||
Please note we cannot guarantee compatibility with everything — test thoroughly!
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> <b>Are MMOItems / SlimeFun / ItemsAdder items supported?</b></summary>
|
||||
|
||||
These plugins, which provide custom items, should be supported as of HuskSync v3.x+; but do note we cannot guarantee compatibility with all methods of injecting custom data to create custom items. Be sure to test thoroughly before deploying on production!
|
||||
These custom item Spigot plugins should work as expected provided they inject data into item NBT in a standard way.
|
||||
|
||||
Please note we cannot guarantee compatibility with everything — test thoroughly!
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> <b>What versions of Minecraft does HuskSync support?</b></summary>
|
||||
|
||||
Check the [[Compatibility]] table. In addition to the latest release of Minecraft, the latest version of HuskSync will support specific older versions based on popularity and mod support.
|
||||
|
||||
If your server's version of Minecraft isn't supported by the latest release, there's plenty of older, stable versions of HuskSync you can download, though note support for these versions will be limited.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> <b>What do I need to run HuskSync?</b></summary>
|
||||
|
||||
See the [Requirements](setup#requirements) section under Setup.
|
||||
|
||||
You need a [[Database]] server, a [[Redis]] server, and [compatible Minecraft servers](compatibility).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> <b>Is Redis required? What is Redis?</b></summary>
|
||||
|
||||
HuskSync requires Redis to operate (for reasons demonstrated below). Redis is an in-memory database server used for caching data at scale and sending messages across a network. You have a Redis server in a similar fashion to the way you have a MySQL database server. If you're using a Minecraft hosting company, you'll want to contact their support and ask if they offer Redis. If you're looking for a host, I have a list of some popular hosts and whether they support Redis [available to read here.](https://william278.net/redis-hosts)
|
||||
Yes, HuskSync requires a [[Redis]] server **in addition to a [[Database]] server** to operate.
|
||||
|
||||
Redis is an in-memory database server used for caching data at scale and sending messages across a network. You have a Redis server in a similar fashion to the way you have a MySQL database server. If you're using a Minecraft hosting company, you'll want to contact their support and ask if they offer Redis. If you're looking for a host, I have a list of some popular hosts and whether they support Redis [available to view here.](https://william278.net/docs/website/redis-hosts)
|
||||
|
||||
For more information, check our [Redis setup instructions](redis).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> <b>How does the plugin synchronize data?</b></summary>
|
||||
<summary> <b>How much RAM does my Redis server need?</b></summary>
|
||||
|
||||
We recommend your Redis server has 1GB of RAM, and that your Redis server is installed locally (on the same server as your game servers, or at least on the server running your Velocity/BungeeCord/Waterfall proxy).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> <b>Is a Database required? What Databases are supported?</b></summary>
|
||||
|
||||
Yes. HuskSync requires both a [[Database]] server and a [[Redis]] server to operate.
|
||||
|
||||
HuskSync supports the following database types:
|
||||
* MySQL v8.0+
|
||||
* MariaDB v5.0+
|
||||
* PostgreSQL
|
||||
* MongoDB
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> <b>How does data syncing work?</b></summary>
|
||||
|
||||
HuskSync makes use of both MySQL and Redis for optimal data synchronization. You have the option of using one of two [[Sync Modes]], which synchronize data between servers (`DELAY` or `LOCKSTEP`)
|
||||
|
||||
@@ -65,9 +111,10 @@ Indeed, there exist economy plugins — such as [XConomy](https://github.com
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> <b>Is this better than MySQLPlayerDataBridge?</b></summary>
|
||||
<summary> <b>Is HuskSync better than MySQLPlayerDataBridge?</b></summary>
|
||||
|
||||
I can't provide a fair answer to this question! What I can say is that your mileage will of course vary.
|
||||
|
||||
The performance improvements offered by HuskSync's synchronization method will depend on your network environment and the economies of scale that come with your player count. In terms of featureset, HuskSync does feature greater rollback and snapshot backup/management features if this is something you are looking for.
|
||||
|
||||
</details>
|
||||
|
||||
25
docs/Home.md
25
docs/Home.md
@@ -1,24 +1,31 @@
|
||||
# [](https://github.com/WiIIiam278/HuskSync)
|
||||
Welcome! This is the plugin documentation for HuskSync v3.x+. Please click through to the topic you'd like to read about.
|
||||
|
||||
## Guides
|
||||
## Setup
|
||||
* 📚 [[Setup]]
|
||||
* 💾 [[Database]]
|
||||
* ✨ [[Redis]]
|
||||
* ⚠️ [[Compatibility]]
|
||||
* 📄 [[Config File]]
|
||||
* 🔗 [[Troubleshooting]]
|
||||
* ↪️ [[Data Rotation]]
|
||||
* ↗️ [[Legacy Migration]]
|
||||
* ✨ [[MPDB Migration]]
|
||||
* 🎏 [[Translations]]
|
||||
* ❓ [[FAQs]]
|
||||
|
||||
## Documentation
|
||||
## Features
|
||||
* 🖥️ [[Commands]]
|
||||
* ✅ [[Sync Features]]
|
||||
* ⚙️ [[Sync Modes]]
|
||||
* 🟩 [[Plan Hook]]
|
||||
* ↪️ [[Data Rotation]]
|
||||
* ❓ [[FAQs]]
|
||||
|
||||
## Guides
|
||||
* ↗️ [[Legacy Migration]]
|
||||
* ✨ [[MPDB Migration]]
|
||||
* ☂️ [[Dumping UserData]]
|
||||
* 🟩 [[Plan Hook]]
|
||||
* 📋 [[Event Priorities]]
|
||||
* ⚔️ [[Keep Inventory]]
|
||||
* 🎏 [[Translations]]
|
||||
|
||||
## Developers
|
||||
* 📦 [[API]] v3
|
||||
* 📝 [[Data Snapshot API]]
|
||||
* 📝 [[Custom Data API]]
|
||||
@@ -30,5 +37,5 @@ Welcome! This is the plugin documentation for HuskSync v3.x+. Please click throu
|
||||
* 📂 [Buy HuskSync](https://william278.net/project/husksync/)
|
||||
* 🚰 [Spigot](https://www.spigotmc.org/resources/husksync.97144/)
|
||||
* 🛒 [Polymart](https://polymart.org/resource/husksync.1634)
|
||||
* ⚒️ [Craftaro](https://craftaro.com/marketplace/product/husksync.758)
|
||||
* ⚒️ [BuiltByBit](https://craftaro.com/marketplace/product/husksync.758)
|
||||
* 💬 [Discord Support](https://discord.gg/tVYhJfyDWG)
|
||||
@@ -1,4 +1,4 @@
|
||||
If your server uses the `keepInventory` gamerule, where players keep the contents of their inventory after dying, HuskSync's built-in snapshot-on-death and dead-player synchronization features can saveCause a conflict leading to synchronization issues.
|
||||
If your server uses the [`keepInventory` game rule](https://minecraft.wiki/w/Keep_inventory), where players keep the contents of their inventory after dying, HuskSync's built-in snapshot-on-death and dead-player synchronization features can saveCause a conflict leading to synchronization issues.
|
||||
|
||||
To solve this issue, you will need to adjust three settings in your `config.yml` file, as described below.
|
||||
|
||||
|
||||
@@ -18,12 +18,12 @@ This guide will walk you through how to upgrade from HuskSync v1.4.x to HuskSync
|
||||
|
||||
### 3. Configure the migrator
|
||||
- With your servers back on and correctly configured to run HuskSync v3.x, ensure nobody is online.
|
||||
- Use the console on one of your Spigot servers to enter: `husksync migrate legacy`
|
||||
- Carefully read the migration configuration instructions. In most cases, you won't have to change the settings, but if you do need to adjust them, use `husksync migrate legacy set <setting> <value>`.
|
||||
- Use the console on one of your Spigot servers to enter: `husksync migrate help legacy`
|
||||
- Carefully read the migration configuration instructions. In most cases, you won't have to change the settings, but if you do need to adjust them, use `husksync migrate set legacy <setting> <value>`.
|
||||
- Migration will be carried out *from* the database you specify with the settings in console *to* the database configured in `config.yml`. If you're migrating from multiple clusters, ensure you run the migrator on the correct servers corresponding to the migrator.
|
||||
|
||||
### 4. Start the migrator
|
||||
- Run `husksync migrate legacy start` to begin the migration process. This may take some time, depending on the amount of data you're migrating.
|
||||
- Run `husksync migrate start legacy` to begin the migration process. This may take some time, depending on the amount of data you're migrating.
|
||||
|
||||
### 5. Ensure the migration was successful
|
||||
- HuskSync will notify in console when migration is complete. Verify that the migration went OK by logging in and using the `/userdata list <username>` command to see if the data was imported with the `legacy migration` saveCause.
|
||||
|
||||
@@ -13,12 +13,12 @@ This guide will walk you through how to migrate from MySQLPlayerDataBridge (MPDB
|
||||
|
||||
### 2. Configure the migrator
|
||||
- With your servers back on and correctly configured to run HuskSync v3.x, ensure nobody is online.
|
||||
- Use the console on one of your Spigot servers to enter: `husksync migrate mpdb`. If the MPDB migrator is not available, ensure MySQLPlayerDataBridge is still installed.
|
||||
- Adjust the migration setting as needed using the following command: `husksync migrate mpdb set <setting> <value>`.
|
||||
- Use the console on one of your Spigot servers to enter: `husksync migrate help mpdb`. If the MPDB migrator is not available, ensure MySQLPlayerDataBridge is still installed.
|
||||
- Adjust the migration setting as needed using the following command: `husksync migrate set mpdb <setting> <value>`.
|
||||
- Note that migration will be carried out *from* the database you specify with the settings in console *to* the database configured in `config.yml`.
|
||||
|
||||
### 3. Start the migrator
|
||||
- Run `husksync migrate mpdb start` to begin the migration process. This may take some time, depending on the amount of data you're migrating.
|
||||
- Run `husksync migrate start mpdb` to begin the migration process. This may take some time, depending on the amount of data you're migrating.
|
||||
|
||||
### 4. Uninstall MySQLPlayerDataBridge
|
||||
- HuskSync will display a message in console when data migration is complete.
|
||||
|
||||
80
docs/Redis.md
Normal file
80
docs/Redis.md
Normal file
@@ -0,0 +1,80 @@
|
||||
Redis is a piece of server used for data caching and cross-server messaging. A Redis server running Redis v5.0+ is **required** in addition to a compatible [[Database]] to use HuskSync. There are a number of ways of [installing or getting a Redis server](#getting-a-redis-server).
|
||||
|
||||
For the best results, we recommend a Redis server with 1GB of RAM, hosted locally (on the same machine as all your other servers). If your setup has multiple machines, install Redis on the machine with your Velocity/BungeeCord/Waterfall proxy server and ensure lockstep syncing mode is in use.
|
||||
|
||||
## What is Redis?
|
||||
[Redis](http://redis.io/) (**RE**mote **DI**ctionary **S**erver) is an open-source, in-memory data store server that can be used as a cache, message broker, streaming engine, or database.
|
||||
|
||||
HuskSync requires Redis and uses it for caching player data when they change server, and for pub/sub messaging to facilitate cross-server admin actions (such as the [`/invsee` command](Commands) to update a player's data on other servers). Check the [[FAQs]] for more details.
|
||||
|
||||
## Configuring
|
||||
To configure Redis, navigate to your [`config.yml`](Config-File) file and modify the properties under `redis`.
|
||||
|
||||
<details>
|
||||
<summary>Database options (config.yml)</summary>
|
||||
|
||||
```yaml
|
||||
# Redis settings
|
||||
redis:
|
||||
# Specify the credentials of your Redis server here. Set "password" to '' if you don't have one
|
||||
credentials:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: ''
|
||||
use_ssl: false
|
||||
# Options for if you're using Redis sentinel. Don't modify this unless you know what you're doing!
|
||||
sentinel:
|
||||
# The master set name for the Redis sentinel.
|
||||
master: ''
|
||||
# List of host:port pairs
|
||||
nodes: []
|
||||
password: ''
|
||||
```
|
||||
</details>
|
||||
|
||||
### Credentials
|
||||
Enter the hostname, port, and default user password of your Redis server.
|
||||
|
||||
If your Redis default user doesn't have a password, leave the password field blank (`password: ''`') and the plugin will attempt to connect without a password.
|
||||
|
||||
### Default user password
|
||||
Depending on the version of Redis you've installed, Redis may or may not set a random default user password. Please check this in your Redis server config. You can clear the password of the default user with the below command in `redis-cli`.
|
||||
|
||||
```bash
|
||||
requirepass thepassword
|
||||
user default on nopass ~* &* +@all
|
||||
```
|
||||
|
||||
### Using Redis Sentinel
|
||||
If you're using [Redis Sentinel](https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/), set this up by filling out the properties under the `sentinel` subsection.
|
||||
|
||||
You'll need to supply your master set name, your sentinel password, and a list of hosts/ports in the format `host:port`.
|
||||
|
||||
## Getting a Redis Server
|
||||
HuskSync requires a Redis server. Instructions for getting Redis on different servers are detailed below. HuskSync is tested for the official Redis package, but should also work with Redis forks or other compatible software.
|
||||
|
||||
For the best results, we recommend a Redis server with 1GB of RAM, hosted locally (on the same machine as all your other servers). If your setup has multiple machines, install Redis on the machine with your Velocity/BungeeCord/Waterfall proxy server and ensure lockstep syncing mode is in use.
|
||||
|
||||
### If you're using a Minecraft server hosting provider
|
||||
Please contact your host's customer support and request Redis. You can direct them to this page if you wish. Looking for a Minecraft Server host that supports Redis? We maintain a list of [server hosts which offer Redis](https://william278.net/docs/website/redis-hosts).
|
||||
|
||||
If your host doesn't offer Redis, you should consider whether HuskSync is the right plugin for you. If you still want to use HuskSync, you could choose to rent a cheap Redis server externally from a provider such as DigitalOcean, though note we don't recommend this as it increases the latency between your game servers and cache, which will impact syncing performance.
|
||||
|
||||
### Redis on Linux or macOS
|
||||
You can [install Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/) on your distribution of Linux. Redis is widely available on most package manager repositories.
|
||||
|
||||
You can also [install Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-mac-os/) on your macOS server.
|
||||
|
||||
### Redis on Windows
|
||||
Redis isn't officially supported on Windows, but there's a number of [unofficial ports](https://github.com/tporadowski/redis/releases) you can install which work great and run Redis as a Windows service.
|
||||
|
||||
You can also [install Redis via WSL](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-windows/) if you prefer.
|
||||
|
||||
### Pterodactyl / Pelican panel hosts
|
||||
If you're self-hosting your server on a Pterodactyl or Pelican panel, you will already have Redis installed and can use this server for HuskSync, too.
|
||||
|
||||
If you are hosting your Redis server on the same node as your servers, you need to use `172.18.0.1` as your host (or equivalent if you changed your network settings), and bind it in the Redis config `nano /etc/redis/redis.conf`.
|
||||
|
||||
You will also need to uncomment the `requirepass` directive and set a password to allow outside connections, or disable `protected-mode`. Once a password is set and Redis is restarted `systemctl restart redis`, you will also need to update the password in your pterodactyl `.env` (`nano /var/www/pterodactyl/.env`) and refresh the cache `cd /var/www/pterodactyl && php artisan config:clear`.
|
||||
|
||||
You may also need to allow connections from your firewall depending on your Linux distribution.
|
||||
@@ -1,50 +1,54 @@
|
||||
This will walk you through installing HuskSync on your network of Spigot servers.
|
||||
This will walk you through installing HuskSync on your network of Spigot or Fabric servers. Please check your server's [[Compatibility]] and download the correct version of HuskSync for your server.
|
||||
|
||||
## Requirements
|
||||
> **Note:** If the plugin fails to load, please check that you are not running an [incompatible version combination](Unsupported-Versions)
|
||||
HuskSync requires a Database server, a Redis server, and any number of compatible Minecraft servers:
|
||||
|
||||
* A MySQL Database (v8.0+) (MariaDB, PostrgreSQL or MongoDB are also supported)
|
||||
* A Redis Database (v5.0+) — see [[FAQs]] for more details.
|
||||
* Any number of Spigot servers, connected by a BungeeCord or Velocity-based proxy (Minecraft v1.17.1+, running Java 17+)
|
||||
* Any number of [compatible Fabric or Spigot/Paper-based](Compatibility) servers
|
||||
* Each server must be running the same exact version of Minecraft
|
||||
* It is not possible to sync data between a mixture of Fabric and Spigot servers
|
||||
* HuskSync should not be installed on your Velocity, BungeeCord, or Waterfall proxy
|
||||
* A [[Database]] server running MySQL v8.0+, MariaDB v5.0+, PostgreSQL or MongoDB
|
||||
* A [[Redis]] server running Redis v5.0+
|
||||
|
||||
## Setup Instructions
|
||||
Before you begin, switch off all servers on your network. It is recommended that you also take a backup.
|
||||
|
||||
### 1. Install the jar
|
||||
- Place the plugin jar file in the `/plugins/` directory of each Spigot server.
|
||||
- Place the plugin jar file in the `/plugins/` or `/mods/` directory of each Spigot/Fabric server respectively.
|
||||
- You do not need to install HuskSync as a proxy plugin.
|
||||
- You can additionally install [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) for better locked user handling, and [Plan](https://www.spigotmc.org/resources/plan-player-analytics.32536/) for analytics.
|
||||
- _Spigot users_: You can additionally install [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) or [PacketEvents](https://www.spigotmc.org/resources/packetevents-api.80279/) for better locked user handling.
|
||||
- _Fabric users_: Ensure the latest Fabric API mod jar is installed!
|
||||
|
||||
### 2. Restart servers
|
||||
- Start, then stop every server to let HuskSync generate the [[config file]].
|
||||
- HuskSync will throw an error in the console and disable itself as it is unable to connect to the database. You haven't set the credentials yet, so this is expected.
|
||||
- Advanced users: If you'd prefer, you can create one config.yml file and create symbolic links in each `/plugins/HuskSync/` folder to it to make updating it easier.
|
||||
|
||||
### 3. Enter Mysql & Redis database credentials
|
||||
- Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`)
|
||||
- Under `credentials` in the `database` section, enter the credentials of your (MySQL/MariaDB/MongoDB/PostgreSQL) Database. You shouldn't touch the `connection_pool` properties.
|
||||
- Under `credentials` in the `redis` section, enter the credentials of your Redis Database. If your Redis server doesn't have a password, leave the password blank as it is.
|
||||
### 3. Enter Database & Redis server credentials
|
||||
- Navigate to the new config file on each server (`~/plugins/HuskSync/config.yml` on Spigot, `~/config/husksync/config.yml` on Fabric)
|
||||
- Under `credentials` in the [`database`](Database) section, enter your database credentials. If you're using a Mongo database, [follow the instructions](database#mongodb-setup) here. You shouldn't need to modify the `connection_pool` properties.
|
||||
- Under `credentials` in the [`redis`](Redis) section, enter the credentials of your Redis server. If your Redis server doesn't have a password, leave the password blank as it is.
|
||||
- Unless you want to have multiple clusters of servers within your network, each with separate user data, you should not change the value of `cluster_id`.
|
||||
|
||||
<details>
|
||||
<summary>Important — MongoDB Users</summary>
|
||||
<summary>MongoDB users — additional instructions</summary>
|
||||
|
||||
- Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`)
|
||||
- Set `type` in the `database` section to `MONGO`
|
||||
- Under `credentials` in the `database` section, enter the credentials of your MongoDB Database. You shouldn't touch the `connection_pool` properties.
|
||||
<details>
|
||||
- Under `parameters` in the `mongo_settings` section, ensure the specified `&authSource=` matches the database you are using (default is `HuskSync`).
|
||||
|
||||
<summary>Additional configuration for MongoDB Atlas users</summary>
|
||||
#### Additional setup for MongoDB Atlas
|
||||
|
||||
- Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`)
|
||||
- Set `using_atlas` in the `mongo_settings` section to `true`.
|
||||
- Remove `&authSource=HuskSync` from `parameters` in the `mongo_settings`.
|
||||
|
||||
(The `port` setting in `credentials` is disregarded when using Atlas.)
|
||||
</details>
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
### 4. Set server names in server.yml files
|
||||
- Navigate to the HuskSync server name file on each server (`~/plugins/HuskSync/server.yml`)
|
||||
- Navigate to the server name file on each server (`~/plugins/HuskSync/server.yml` on Spigot, `~/config/husksync/server.yml` on Fabric)
|
||||
- Set the `name:` of the server in this file to the ID of this server as defined in the config of your proxy (e.g., if this is the "hub" server you access with `/server hub`, put `'hub'` here)
|
||||
|
||||
### 5. Start every server again
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
This page contains a list of the features HuskSync is and isn't able to syncrhonise on your server.
|
||||
This page contains a list of the features HuskSync is and isn't able to synchronise on your server.
|
||||
|
||||
You can customise how much data HuskSync saves about a player by [turning each synchronization feature on or off](#toggling-sync-features). When a synchronization feature is turned off, HuskSync won't touch that part of a player's profile; in other words, the data they will inherit when changing servers will be read from their player data file on the local server.
|
||||
|
||||
@@ -38,7 +38,7 @@ Although it's a common request, HuskSync doesn't synchronize economy data for a
|
||||
I strongly recommend making use of economy plugins that have cross-server economy balance synchronization built-in, of which there are a multitude of options available. Please see our [[FAQs]] section for more details on this decision.
|
||||
|
||||
## Toggling Sync Features
|
||||
All synchronization features, except location and locked map synchronising, are enabled by default. To toggle a feature, navigate to the `features:` section in the `synchronization:` part of your `config.yml` file, and change the option to `true`/`false` respectively.
|
||||
All synchronization features, except location and locked map synchronizing, are enabled by default. To toggle a feature, navigate to the `features:` section in the `synchronization:` part of your `config.yml` file, and change the option to `true`/`false` respectively.
|
||||
|
||||
<details>
|
||||
<summary>Example in config.yml</summary>
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
This page contains a number of common issues and how you can troubleshoot and resolve them.
|
||||
This page contains a number of common issues when using HuskSync and how you can troubleshoot and resolve them.
|
||||
|
||||
## Topics
|
||||
### Duplicate UUIDs in database
|
||||
This is most frequently caused by running a cracked "offline mode" network of servers. We [don't provide support](https://william278.net/terms) for problems caused by cracked servers and the most advice we can offer you is:
|
||||
- Ensure `bungee_online_mode` is set to the correct value in the `paper.yml` config file on each of your Bukkit servers
|
||||
- Ensure your authenticator plugin is passing valid, unique IDs to each backend Spigot server.
|
||||
- Ensure your authenticator plugin is passing valid, unique IDs to each backend Spigot/Fabric server.
|
||||
|
||||
### Cannot set data with newer Minecraft version than the server
|
||||
This is caused when you attempt to downgrade user data from a newer version of Minecraft to an older one, or when your Spigot servers are running mismatched Minecraft versions.
|
||||
This is caused when you attempt to downgrade user data from a newer version of Minecraft to an older one, or when your Spigot/Fabric servers are running mismatched Minecraft versions.
|
||||
|
||||
HuskSync will identify this and safely prevent the synchronization from occuring. Your Spigot servers must be running the same version of both Minecraft and HuskSync.
|
||||
HuskSync will identify this and safely prevent the synchronization from occurring. Your Spigot/Fabric servers must be running the same version of both Minecraft and HuskSync.
|
||||
|
||||
### User data failing to synchronize
|
||||
This can occur due to misaligned timings between your Spigot servers and your Redis server. HuskSync has a built in way of tuning this. Try continously increasing the `network_latency_milliseconds` option in your config to a higher value.
|
||||
This can occur due to misaligned timings between your Spigot/Fabric servers and your Redis server. HuskSync has a built in way of tuning this. Try continously increasing the `network_latency_milliseconds` option in your config to a higher value.
|
||||
|
||||
### Synchronization issues with Keep Inventory enabled
|
||||
On servers that use Keep Inventory move (where players keep their items when they die), you can run into synchronization issues. See [[Keep Inventory]] for details on why this happens and how to resolve it.
|
||||
On servers that use [[Keep Inventory]] (where players keep their items when they die), you can run into synchronization issues. See [[Keep Inventory]] for details on why this happens and how to resolve it.
|
||||
|
||||
### Exceptions when compressing data via Snappy (lightweight Linux distros)
|
||||
Some lightweight Linux distros such as Alpine Linux (used on Pterodactyl) might not have the dependencies needed for the [Snappy](https://github.com/xerial/snappy-java) compressor. It's possible to disable data compression by changing `compress_data` to false in your config. Note that after changing this setting you will need to reset your database. Alternatively, find the right libraries for your distro!
|
||||
|
||||
### Redis connection problems on Pterodactyl
|
||||
If you are hosting your Redis server on the same node as your servers, you need to use 172.18.0.1 (or equivelant if you changed your network settings) as your host. You may also need to [allow connections from your firewall](https://pterodactyl.io/community/games/minecraft.html#firewalls) depending on your distribution.
|
||||
### Redis connection problems on Pterodactyl / Pelican
|
||||
If you are hosting your [[Redis]] server on the same node as your servers, you need to use 172.18.0.1 (or equivelant if you changed your network settings) as your host. You may also need to [allow connections from your firewall](https://pterodactyl.io/community/games/minecraft.html#firewalls) depending on your distribution. See our tips for running [Redis on a Pterodactyl or Pelican panel](Redis#pterodactyl--pelican-panel-hosts)
|
||||
|
||||
### MySQL connection problems on Pterodactyl
|
||||
If you have more than one MySQL server connected to your panel, you may need to set `useSSL=true` in the parameters.
|
||||
### Database connection problems on Pterodactyl / Pelican
|
||||
If you have more than one [[Database]] server connected to your panel, you may need to set `useSSL=true` in the parameters.
|
||||
|
||||
### Issues with player data going out of sync during a server restart
|
||||
This can happen due to the way in which your server restarts. If your server uses either:
|
||||
|
||||
* `/restart` (this is a weird Spigot/Fabric command that uses legacy bash scripting)
|
||||
* ANY restart plugin, e.g. UltimateAutoRestart (these basically execute an API-called restart using the same legacy bash logic as per above)
|
||||
|
||||
These are **not compatible** with HuskSync in most cases due to the way in which this causes restart servers causing shutdown logic to process in strange and unpredictable orders, usually before HuskSync has had a chance to scan and perform its shutdown logic. To safely restart your server, please use:
|
||||
|
||||
* A Pterodactyl task to perform a Restart. This executes the Power Action program stopcode (and then execute the startup command when the container has terminated)
|
||||
* A cronjob to send a stop command / Power Action program stopcode, listen for the service to fully terminate, and then execute your startup command
|
||||
* For manual restarts, executing `/stop` and starting your server up with the startup command is totally fine.
|
||||
|
||||
It's not a great idea to use a plugin to handle restarts. Plugins are only able to operate when your server is turned on and must rely on scripts which don't safely shutdown servers when restarting.
|
||||
@@ -1,11 +0,0 @@
|
||||
This plugin does not support the following software-Minecraft version combinations. The plugin will fail to load if you attempt to run it with these versions. Apologies for the inconvenience.
|
||||
|
||||
## Incompatibility table
|
||||
| Minecraft Versions | Server Software | Notes |
|
||||
|--------------------|-------------------------------------------|----------------------------------------|
|
||||
| 1.19.4 | Only: `Purpur, Pufferfish`† | Older Paper builds also not supported. |
|
||||
| 1.19.3 | Only: `Paper, Purpur, Pufferfish`† | Upgrade to 1.19.4 or use Spigot |
|
||||
| 1.16.5 | _All_ | Please use v3.3.1 or lower |
|
||||
| below 1.16.5 | _All_ | Upgrade Minecraft 1.16.5 |
|
||||
|
||||
†Further downstream forks of this server software are also affected.
|
||||
@@ -1,21 +1,28 @@
|
||||
## Guides
|
||||
## Setup
|
||||
* 📚 [[Setup]]
|
||||
* 💾 [[Database]]
|
||||
* ✨ [[Redis]]
|
||||
* ⚠️ [[Compatibility]]
|
||||
* 📄 [[Config File]]
|
||||
* 🔗 [[Troubleshooting]]
|
||||
* ↪️ [[Data Rotation]]
|
||||
* ↗️ [[Legacy Migration]]
|
||||
* ✨ [[MPDB Migration]]
|
||||
* 🎏 [[Translations]]
|
||||
* ❓ [[FAQs]]
|
||||
|
||||
## Documentation
|
||||
## Features
|
||||
* 🖥️ [[Commands]]
|
||||
* ✅ [[Sync Features]]
|
||||
* ⚙️ [[Sync Modes]]
|
||||
* 🟩 [[Plan Hook]]
|
||||
* ↪️ [[Data Rotation]]
|
||||
* ❓ [[FAQs]]
|
||||
|
||||
## Guides
|
||||
* ↗️ [[Legacy Migration]]
|
||||
* ✨ [[MPDB Migration]]
|
||||
* ☂️ [[Dumping UserData]]
|
||||
* 🟩 [[Plan Hook]]
|
||||
* 📋 [[Event Priorities]]
|
||||
* ⚔️ [[Keep Inventory]]
|
||||
* 🎏 [[Translations]]
|
||||
|
||||
## Developers
|
||||
* 📦 [[API]] v3
|
||||
* 📝 [[Data Snapshot API]]
|
||||
* 📝 [[Custom Data API]]
|
||||
@@ -27,6 +34,5 @@
|
||||
* 📂 [Buy HuskSync](https://william278.net/project/husksync/)
|
||||
* 🚰 [Spigot](https://www.spigotmc.org/resources/husksync.97144/)
|
||||
* 🛒 [Polymart](https://polymart.org/resource/husksync.1634)
|
||||
* ⚒️ [Craftaro](https://craftaro.com/marketplace/product/husksync.758)
|
||||
* 🛒 [BuiltByBit](https://craftaro.com/marketplace/product/husksync.758)
|
||||
* 💬 [Discord Support](https://discord.gg/tVYhJfyDWG)
|
||||
* ⚒️ [BuiltByBit](https://craftaro.com/marketplace/product/husksync.758)
|
||||
* 💬 [Discord Support](https://discord.gg/tVYhJfyDWG)
|
||||
73
fabric/build.gradle
Normal file
73
fabric/build.gradle
Normal file
@@ -0,0 +1,73 @@
|
||||
plugins {
|
||||
id 'fabric-loom' version "$fabric_loom_version"
|
||||
}
|
||||
|
||||
apply plugin: 'fabric-loom'
|
||||
loom.serverOnlyMinecraftJar()
|
||||
|
||||
repositories {
|
||||
maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' }
|
||||
maven { url 'https://maven.nucleoid.xyz' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
minecraft "com.mojang:minecraft:${minecraft_version}"
|
||||
mappings "net.fabricmc:yarn:${fabric_yarn_mappings}:v2"
|
||||
modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}"
|
||||
|
||||
modImplementation include("net.kyori:adventure-platform-fabric:${fabric_adventure_platform_version}")
|
||||
modImplementation include("me.lucko:fabric-permissions-api:${fabric_permissions_api_version}")
|
||||
modImplementation include("eu.pb4:sgui:${fabric_sgui_version}")
|
||||
modImplementation include("net.william278.uniform:uniform-fabric:1.3+${minecraft_version}")
|
||||
modCompileOnly "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}"
|
||||
|
||||
implementation include('org.apache.commons:commons-pool2:2.12.0')
|
||||
implementation include("redis.clients:jedis:$jedis_version")
|
||||
implementation include("com.mysql:mysql-connector-j:$mysql_driver_version")
|
||||
implementation include("org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version")
|
||||
implementation include("org.postgresql:postgresql:$postgres_driver_version")
|
||||
implementation include("org.xerial.snappy:snappy-java:$snappy_version")
|
||||
|
||||
compileOnly 'org.jetbrains:annotations:26.0.1'
|
||||
compileOnly 'net.william278:DesertWell:2.0.4'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.36'
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.36'
|
||||
|
||||
shadow project(path: ":common")
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
configurations = [project.configurations.shadow]
|
||||
destinationDirectory.set(file("$projectDir/build/libs"))
|
||||
|
||||
exclude('net.fabricmc:.*')
|
||||
exclude('net.kyori:.*')
|
||||
exclude '/mappings/*'
|
||||
|
||||
relocate 'org.apache.commons.io', 'net.william278.husksync.libraries.commons.io'
|
||||
relocate 'org.apache.commons.text', 'net.william278.husksync.libraries.commons.text'
|
||||
relocate 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3'
|
||||
relocate 'com.google.gson', 'net.william278.husksync.libraries.gson'
|
||||
relocate 'com.fatboyindustrial', 'net.william278.husksync.libraries'
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'de.exlll', 'net.william278.husksync.libraries'
|
||||
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
|
||||
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
|
||||
relocate 'org.json', 'net.william278.husksync.libraries.json'
|
||||
}
|
||||
|
||||
remapJar {
|
||||
dependsOn tasks.shadowJar
|
||||
mustRunAfter tasks.shadowJar
|
||||
inputFile = shadowJar.archiveFile.get()
|
||||
addNestedDependencies = true
|
||||
|
||||
destinationDirectory.set(file("$rootDir/target/"))
|
||||
archiveClassifier.set('')
|
||||
}
|
||||
|
||||
shadowJar.finalizedBy(remapJar)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user