mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-23 16:49:19 +00:00
Compare commits
233 Commits
3.5.2
...
test/debug
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70d6b671f2 | ||
|
|
98576c72fb | ||
|
|
ae657acee3 | ||
|
|
34dc6a537d | ||
|
|
e99ba66271 | ||
|
|
546e663e4e | ||
|
|
0111f25865 | ||
|
|
02c8b899dc | ||
|
|
b725015318 | ||
|
|
11550e0ba3 | ||
|
|
33e20a0c0b | ||
|
|
0ae13d730d | ||
|
|
f5ad5c079f | ||
|
|
305f90f697 | ||
|
|
e56041eae2 | ||
|
|
904c65ba39 | ||
|
|
fbb8ec3048 | ||
|
|
2a59a0b3f5 | ||
|
|
b108d38598 | ||
|
|
8b7e891ab6 | ||
|
|
be6bebe361 | ||
|
|
1ff4cab88d | ||
|
|
e3c40a231b | ||
|
|
c8fd3f88fa | ||
|
|
f9ec1f3ebb | ||
|
|
033af3126c | ||
|
|
a15739fbb9 | ||
|
|
f4b9124636 | ||
|
|
fecda83fcb | ||
|
|
07228c3661 | ||
|
|
a0fb2e90b3 | ||
|
|
ae69c1c060 | ||
|
|
4992f4492c | ||
|
|
58bd3acdc3 | ||
|
|
af51c035a3 | ||
|
|
85ae2b5fb2 | ||
|
|
7ff10b33a0 | ||
|
|
431c9e13c9 | ||
|
|
c8579fb987 | ||
|
|
2f4eb46456 | ||
|
|
2d547507d5 | ||
|
|
8e4678468e | ||
|
|
c2a32cabc5 | ||
|
|
07f06aac68 | ||
|
|
7ae1001b1b | ||
|
|
e04c19acf5 | ||
|
|
1820a810f4 | ||
|
|
cedd12a048 | ||
|
|
7967d00208 | ||
|
|
00a68be2ad | ||
|
|
da5d991d2a | ||
|
|
c2f6d240ad | ||
|
|
4cde24c536 | ||
|
|
029617bc45 | ||
|
|
0627fb20e4 | ||
|
|
bc1f983684 | ||
|
|
31eb747c55 | ||
|
|
e8facf52ce | ||
|
|
5ee4bdd644 | ||
|
|
92c371e201 | ||
|
|
d27278454a | ||
|
|
16780c149c | ||
|
|
0445ba63bc | ||
|
|
b6aefd6f57 | ||
|
|
f803af0225 | ||
|
|
2675f4a377 | ||
|
|
03341c981f | ||
|
|
38cc654167 | ||
|
|
b347a8d060 | ||
|
|
8733b86b45 | ||
|
|
eda8e72633 | ||
|
|
c942a015d1 | ||
|
|
c00265f1f9 | ||
|
|
e303984dcf | ||
|
|
b449b5dee6 | ||
|
|
48f8c0c967 | ||
|
|
f88c4c3e2c | ||
|
|
e6273fa9a0 | ||
|
|
1ba5585d0d | ||
|
|
73547371ae | ||
|
|
fca6825394 | ||
|
|
53af114f44 | ||
|
|
311cc85c92 | ||
|
|
099a258cf8 | ||
|
|
480f59a166 | ||
|
|
45c2f5350f | ||
|
|
ed88d77852 | ||
|
|
e7fc9f015e | ||
|
|
cabde9e8d8 | ||
|
|
4df7d2def4 | ||
|
|
59ed77c169 | ||
|
|
53da3bd40c | ||
|
|
abdf8223fc | ||
|
|
a5efeecad3 | ||
|
|
4d26b24d13 | ||
|
|
29b3a60c64 | ||
|
|
da894f57c4 | ||
|
|
1bd703641b | ||
|
|
1b1d4c8e8d | ||
|
|
842ec0e28d | ||
|
|
2d5648408e | ||
|
|
41b3240741 | ||
|
|
bc03e8f3e3 | ||
|
|
86799f4c08 | ||
|
|
a3e004cf71 | ||
|
|
a7aeb1de21 | ||
|
|
1a703102c3 | ||
|
|
368c68f42b | ||
|
|
e191713bdc | ||
|
|
1604338498 | ||
|
|
c223797bf4 | ||
|
|
9b10adc8e4 | ||
|
|
5935f1ab5f | ||
|
|
3455b10a20 | ||
|
|
34e08b712d | ||
|
|
605d314a58 | ||
|
|
daaf5147a7 | ||
|
|
50eb9a7543 | ||
|
|
7d8ef7b6b3 | ||
|
|
347d2d0a8f | ||
|
|
bd560fcc99 | ||
|
|
b68aedc99a | ||
|
|
47373d8974 | ||
|
|
a57b8df994 | ||
|
|
17235637a5 | ||
|
|
cd5abd5a65 | ||
|
|
5c6631cdcf | ||
|
|
621afcd5c6 | ||
|
|
112a974a6c | ||
|
|
f9d46b4aff | ||
|
|
dfd828bca1 | ||
|
|
2df9fd897a | ||
|
|
ff2531539e | ||
|
|
52ec138273 | ||
|
|
0f7a866652 | ||
|
|
eeb52ac41e | ||
|
|
4c7ec9ec21 | ||
|
|
2f9064c4c6 | ||
|
|
5c234cdb1d | ||
|
|
7d8a74381b | ||
|
|
04a7793585 | ||
|
|
ea068529f6 | ||
|
|
fead3df0d8 | ||
|
|
0c5a42a344 | ||
|
|
75a2378ea8 | ||
|
|
662fc96ad5 | ||
|
|
f456443da0 | ||
|
|
07da1c04ce | ||
|
|
845abf370a | ||
|
|
83b5209a75 | ||
|
|
8e9850dd19 | ||
|
|
1d24209b68 | ||
|
|
da70a54d78 | ||
|
|
fc7330213a | ||
|
|
d8272ba52d | ||
|
|
315f0eeb2f | ||
|
|
8e83617ac4 | ||
|
|
212bb0beb8 | ||
|
|
c16231b12b | ||
|
|
93f7294859 | ||
|
|
32ac57e2a4 | ||
|
|
c949c976d6 | ||
|
|
ab736829f2 | ||
|
|
4433926ce7 | ||
|
|
f819fd4d5e | ||
|
|
e7659255fe | ||
|
|
0dee2e8319 | ||
|
|
7b35c47315 | ||
|
|
5056a794d8 | ||
|
|
5e6068431a | ||
|
|
8d69508689 | ||
|
|
efb6d8a7de | ||
|
|
79d9778378 | ||
|
|
6a6695e447 | ||
|
|
8862e6cd70 | ||
|
|
0b29de9efc | ||
|
|
962cdfce0b | ||
|
|
0c527202e5 | ||
|
|
d4e33aa9d2 | ||
|
|
2fcd58fc18 | ||
|
|
3d10b2324f | ||
|
|
31419f3b97 | ||
|
|
8105ac27fc | ||
|
|
44f251a948 | ||
|
|
463e707d27 | ||
|
|
2d85910744 | ||
|
|
268b279fdf | ||
|
|
a8ca3314d8 | ||
|
|
2bdd3dae37 | ||
|
|
e29564c4ad | ||
|
|
6b8bb23af9 | ||
|
|
91bbe05851 | ||
|
|
8ed6869aad | ||
|
|
7efdf0d329 | ||
|
|
49c32e3f98 | ||
|
|
f0574527b9 | ||
|
|
ad510a8fca | ||
|
|
303b287705 | ||
|
|
549508b9c1 | ||
|
|
6c8a577701 | ||
|
|
862177bec7 | ||
|
|
dbed4d83a2 | ||
|
|
aa2090d97a | ||
|
|
b168ede7c5 | ||
|
|
0e706d36c4 | ||
|
|
69d68de5c0 | ||
|
|
3d5395e5ae | ||
|
|
332c71f041 | ||
|
|
b9fbcd72dd | ||
|
|
68897e6265 | ||
|
|
04606a7c9a | ||
|
|
6286bbe2ad | ||
|
|
24ba209f8f | ||
|
|
05d588f681 | ||
|
|
9aa3606f54 | ||
|
|
fc05e4b17a | ||
|
|
7b2b47de83 | ||
|
|
be0b4e3397 | ||
|
|
dd1ba594de | ||
|
|
89368778f3 | ||
|
|
e3fb1762a1 | ||
|
|
516c243df8 | ||
|
|
b7aa75fcd5 | ||
|
|
549f013e0f | ||
|
|
14c56af465 | ||
|
|
bee01dd15a | ||
|
|
e97551e67e | ||
|
|
97023e8425 | ||
|
|
4fa7106a46 | ||
|
|
e0b81e4c76 | ||
|
|
c4adec3082 | ||
|
|
107238360c | ||
|
|
6141adbdb9 |
44
.github/workflows/ci.yml
vendored
44
.github/workflows/ci.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: CI Tests
|
name: CI Tests & Publish
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -18,10 +18,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: 'Checkout for CI 🛎️'
|
- name: 'Checkout for CI 🛎️'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: 'Set up JDK 17 📦'
|
- name: 'Set up JDK 21 📦'
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '21'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
- name: 'Build with Gradle 🏗️'
|
- name: 'Build with Gradle 🏗️'
|
||||||
uses: gradle/gradle-build-action@v3
|
uses: gradle/gradle-build-action@v3
|
||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||||
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||||
- name: 'Publish Test Report 📊'
|
- name: 'Publish Test Report 📊'
|
||||||
uses: mikepenz/action-junit-report@v4
|
uses: mikepenz/action-junit-report@v5
|
||||||
if: success() || failure() # Continue on failure
|
if: success() || failure() # Continue on failure
|
||||||
with:
|
with:
|
||||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||||
@@ -42,3 +42,39 @@ jobs:
|
|||||||
- name: Get Version
|
- name: Get Version
|
||||||
run: |
|
run: |
|
||||||
echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV
|
echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV
|
||||||
|
- name: 'Publish to William278.net 🚀'
|
||||||
|
uses: WiIIiam278/bones-publish-action@v1
|
||||||
|
with:
|
||||||
|
api-key: ${{ secrets.BONES_API_KEY }}
|
||||||
|
project: 'husksync'
|
||||||
|
channel: 'alpha'
|
||||||
|
version: ${{ env.version_name }}
|
||||||
|
changelog: ${{ github.event.head_commit.message }}
|
||||||
|
distro-names: |
|
||||||
|
paper-1.20.1
|
||||||
|
paper-1.21.1
|
||||||
|
paper-1.21.4
|
||||||
|
fabric-1.20.1
|
||||||
|
fabric-1.21.1
|
||||||
|
fabric-1.21.4
|
||||||
|
distro-groups: |
|
||||||
|
paper
|
||||||
|
paper
|
||||||
|
paper
|
||||||
|
fabric
|
||||||
|
fabric
|
||||||
|
fabric
|
||||||
|
distro-descriptions: |
|
||||||
|
Paper 1.20.1
|
||||||
|
Paper 1.21.1
|
||||||
|
Paper 1.21.4
|
||||||
|
Fabric 1.20.1
|
||||||
|
Fabric 1.21.1
|
||||||
|
Fabric 1.21.4
|
||||||
|
files: |
|
||||||
|
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.20.1.jar
|
||||||
|
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.1.jar
|
||||||
|
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.4.jar
|
||||||
|
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.20.1.jar
|
||||||
|
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.1.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:
|
steps:
|
||||||
- name: 'Checkout for CI 🛎'
|
- name: 'Checkout for CI 🛎'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: 'Set up JDK 17 📦'
|
- name: 'Set up JDK 21 📦'
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '21'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
- name: 'Build with Gradle 🏗️'
|
- name: 'Build with Gradle 🏗️'
|
||||||
uses: gradle/gradle-build-action@v3
|
uses: gradle/gradle-build-action@v3
|
||||||
with:
|
with:
|
||||||
arguments: test
|
arguments: test
|
||||||
- name: 'Publish Test Report 📊'
|
- name: 'Publish Test Report 📊'
|
||||||
uses: mikepenz/action-junit-report@v4
|
uses: mikepenz/action-junit-report@v5
|
||||||
if: success() || failure() # Continue on failure
|
if: success() || failure() # Continue on failure
|
||||||
with:
|
with:
|
||||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||||
44
.github/workflows/release.yml
vendored
44
.github/workflows/release.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Release Tests
|
name: Release Tests & Publish
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
@@ -14,10 +14,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: 'Checkout for CI 🛎️'
|
- name: 'Checkout for CI 🛎️'
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: 'Set up JDK 17 📦'
|
- name: 'Set up JDK 21 📦'
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '21'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
- name: 'Build with Gradle 🏗️'
|
- name: 'Build with Gradle 🏗️'
|
||||||
uses: gradle/gradle-build-action@v3
|
uses: gradle/gradle-build-action@v3
|
||||||
@@ -27,7 +27,43 @@ jobs:
|
|||||||
RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||||
RELEASES_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
RELEASES_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||||
- name: 'Publish Test Report 📊'
|
- name: 'Publish Test Report 📊'
|
||||||
uses: mikepenz/action-junit-report@v4
|
uses: mikepenz/action-junit-report@v5
|
||||||
if: success() || failure() # Continue on failure
|
if: success() || failure() # Continue on failure
|
||||||
with:
|
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.20.1
|
||||||
|
paper-1.21.1
|
||||||
|
paper-1.21.4
|
||||||
|
fabric-1.20.1
|
||||||
|
fabric-1.21.1
|
||||||
|
fabric-1.21.4
|
||||||
|
distro-groups: |
|
||||||
|
paper
|
||||||
|
paper
|
||||||
|
paper
|
||||||
|
fabric
|
||||||
|
fabric
|
||||||
|
fabric
|
||||||
|
distro-descriptions: |
|
||||||
|
Paper 1.20.1
|
||||||
|
Paper 1.21.1
|
||||||
|
Paper 1.21.4
|
||||||
|
Fabric 1.20.1
|
||||||
|
Fabric 1.21.1
|
||||||
|
Fabric 1.21.4
|
||||||
|
files: |
|
||||||
|
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.20.1.jar
|
||||||
|
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.1.jar
|
||||||
|
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.4.jar
|
||||||
|
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.20.1.jar
|
||||||
|
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.1.jar
|
||||||
|
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.4.jar
|
||||||
3
.github/workflows/update_docs.yml
vendored
3
.github/workflows/update_docs.yml
vendored
@@ -13,7 +13,8 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy-wiki:
|
update-docs:
|
||||||
|
name: 'Update Docs'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: 'Checkout for CI 🛎️'
|
- name: 'Checkout for CI 🛎️'
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -122,4 +122,3 @@ run/
|
|||||||
# Don't include generated test suite files
|
# Don't include generated test suite files
|
||||||
/test/servers/
|
/test/servers/
|
||||||
/test/HuskSync
|
/test/HuskSync
|
||||||
/test/config.yml
|
|
||||||
39
README.md
39
README.md
@@ -5,7 +5,7 @@
|
|||||||
<img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/ci.yml?branch=master&logo=github"/>
|
<img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/ci.yml?branch=master&logo=github"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://repo.william278.net/#/releases/net/william278/husksync/">
|
<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>
|
||||||
<a href="https://discord.gg/tVYhJfyDWG">
|
<a href="https://discord.gg/tVYhJfyDWG">
|
||||||
<img src="https://img.shields.io/discord/818135932103557162.svg?label=&logo=discord&logoColor=fff&color=7389D8&labelColor=6A7EC2" />
|
<img src="https://img.shields.io/discord/818135932103557162.svg?label=&logo=discord&logoColor=fff&color=7389D8&labelColor=6A7EC2" />
|
||||||
@@ -43,21 +43,44 @@
|
|||||||
|
|
||||||
**Ready?** [It's syncing time!](https://william278.net/docs/husksync/setup)
|
**Ready?** [It's syncing time!](https://william278.net/docs/husksync/setup)
|
||||||
|
|
||||||
## Setup
|
## Compatibility
|
||||||
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+.
|
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.
|
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.
|
4. Start every server again and synchronization will begin.
|
||||||
|
|
||||||
## Development
|
## 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
|
```bash
|
||||||
./gradlew clean build
|
./gradlew clean build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
HuskSync uses `essential-multi-version` (Fabric) and `preprocessor` (Bukkit) to target multiple versions of Minecraft in one codebase - [check here](https://github.com/WiIIiam278/PreProcessor?tab=readme-ov-file#code-example) for a preprocessor comment logic reference.
|
||||||
|
|
||||||
### License
|
### License
|
||||||
HuskSync is licensed under the Apache 2.0 license.
|
HuskSync is licensed under the Apache 2.0 license.
|
||||||
|
|
||||||
@@ -66,7 +89,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!
|
Contributions to the project are welcome—feel free to open a pull request with new features, improvements and/or fixes!
|
||||||
|
|
||||||
### Support
|
### 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
|
||||||
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.
|
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 +105,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
|
- [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.
|
||||||
|
|||||||
84
build.gradle
84
build.gradle
@@ -1,9 +1,11 @@
|
|||||||
import org.apache.tools.ant.filters.ReplaceTokens
|
import org.apache.tools.ant.filters.ReplaceTokens
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
id 'com.gradleup.shadow' version '8.3.6'
|
||||||
id 'org.cadixdev.licenser' version '0.6.1' apply false
|
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 'gg.essential.multi-version.root' apply false
|
||||||
|
id 'org.ajoberstar.grgit' version '5.3.0'
|
||||||
id 'maven-publish'
|
id 'maven-publish'
|
||||||
id 'java'
|
id 'java'
|
||||||
}
|
}
|
||||||
@@ -57,7 +59,12 @@ publishing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
apply plugin: 'com.github.johnrengelman.shadow'
|
// Ignore parent projects (no jars)
|
||||||
|
if (project.name == 'fabric' || project.name == 'bukkit') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.gradleup.shadow'
|
||||||
apply plugin: 'org.cadixdev.licenser'
|
apply plugin: 'org.cadixdev.licenser'
|
||||||
apply plugin: 'java'
|
apply plugin: 'java'
|
||||||
|
|
||||||
@@ -69,25 +76,23 @@ allprojects {
|
|||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
mavenCentral()
|
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://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
|
||||||
maven { url "https://repo.dmulloy2.net/repository/public/" }
|
maven { url 'https://repo.papermc.io/repository/maven-public/' }
|
||||||
maven { url 'https://repo.codemc.io/repository/maven-public/' }
|
maven { url 'https://repo.codemc.io/repository/maven-public/' }
|
||||||
maven { url 'https://repo.minebench.de/' }
|
maven { url 'https://repo.minebench.de/' }
|
||||||
maven { url 'https://repo.alessiodp.com/releases/' }
|
maven { url 'https://repo.alessiodp.com/releases/' }
|
||||||
maven { url 'https://jitpack.io' }
|
maven { url 'https://jitpack.io' }
|
||||||
maven { url 'https://mvn-repo.arim.space/lesser-gpl3/' }
|
maven { url 'https://mvn-repo.arim.space/lesser-gpl3/' }
|
||||||
maven { url 'https://libraries.minecraft.net/' }
|
maven { url 'https://libraries.minecraft.net/' }
|
||||||
maven { url 'https://repo.william278.net/releases/' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2'
|
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.4'
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2'
|
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.4'
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2'
|
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.4'
|
||||||
}
|
testCompileOnly 'org.jetbrains:annotations:26.0.2'
|
||||||
|
|
||||||
test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
license {
|
license {
|
||||||
@@ -96,17 +101,45 @@ allprojects {
|
|||||||
newLine = true
|
newLine = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
processResources {
|
processResources {
|
||||||
|
def tokenMap = rootProject.ext.properties
|
||||||
|
tokenMap.merge("grgit",'',(s, s2) -> s)
|
||||||
filesMatching(['**/*.json', '**/*.yml']) {
|
filesMatching(['**/*.json', '**/*.yml']) {
|
||||||
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
|
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
|
||||||
tokens: rootProject.ext.properties
|
tokens: tokenMap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
|
// Ignore parent projects (no jars)
|
||||||
|
if (['fabric', 'bukkit'].contains(project.name)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Project naming
|
||||||
version rootProject.version
|
version rootProject.version
|
||||||
archivesBaseName = "${rootProject.name}-${project.name.capitalize()}"
|
def name = "$rootProject.name"
|
||||||
|
if (rootProject != project.parent) {
|
||||||
|
name += "-${project.parent.name.capitalize()}"
|
||||||
|
} else {
|
||||||
|
name += "-${project.name.capitalize()}"
|
||||||
|
}
|
||||||
|
archivesBaseName = name
|
||||||
|
|
||||||
|
// Version-specific configuration
|
||||||
|
if (['fabric', 'bukkit'].contains(project.parent?.name)) {
|
||||||
|
compileJava.options.release.set (project.name == '1.20.1' ? 17 : 21) // 1.20.1 requires Java 17
|
||||||
|
version += "+mc.${project.name}"
|
||||||
|
|
||||||
|
if (project.parent?.name?.equals('fabric')) {
|
||||||
|
apply plugin: 'fabric-loom'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
from '../LICENSE'
|
from '../LICENSE'
|
||||||
@@ -118,7 +151,7 @@ subprojects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// API publishing
|
// API publishing
|
||||||
if (['common', 'bukkit'].contains(project.name)) {
|
if (project.name == 'common' || ['fabric', 'bukkit'].contains(project.parent?.name)) {
|
||||||
java {
|
java {
|
||||||
withSourcesJar()
|
withSourcesJar()
|
||||||
withJavadocJar()
|
withJavadocJar()
|
||||||
@@ -145,22 +178,35 @@ subprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['bukkit'].contains(project.name)) {
|
if (project.parent?.name?.equals('bukkit')) {
|
||||||
publications {
|
publications {
|
||||||
mavenJavaBukkit(MavenPublication) {
|
"mavenJavaBukkit_${project.name.replace('.', '_')}"(MavenPublication) {
|
||||||
groupId = 'net.william278.husksync'
|
groupId = 'net.william278.husksync'
|
||||||
artifactId = 'husksync-bukkit'
|
artifactId = 'husksync-bukkit'
|
||||||
version = "$rootProject.version"
|
version = "$rootProject.version+$project.name"
|
||||||
artifact shadowJar
|
artifact shadowJar
|
||||||
artifact sourcesJar
|
artifact sourcesJar
|
||||||
artifact javadocJar
|
artifact javadocJar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (project.parent?.name?.equals('fabric')) {
|
||||||
|
publications {
|
||||||
|
"mavenJavaFabric_${project.name.replace('.', '_')}"(MavenPublication) {
|
||||||
|
groupId = 'net.william278.husksync'
|
||||||
|
artifactId = 'husksync-fabric'
|
||||||
|
version = "$rootProject.version+$project.name"
|
||||||
|
artifact remapJar
|
||||||
|
artifact sourcesJar
|
||||||
|
artifact javadocJar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jar.dependsOn(shadowJar)
|
jar.dependsOn shadowJar
|
||||||
clean.delete "$rootDir/target"
|
clean.delete "$rootDir/target"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
bukkit/1.20.1/gradle.properties
Normal file
3
bukkit/1.20.1/gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
minecraft_version_numeric=12001
|
||||||
|
minecraft_api_version=1.20
|
||||||
|
paper_api_version=1.20.1-R0.1-SNAPSHOT
|
||||||
3
bukkit/1.21.1/gradle.properties
Normal file
3
bukkit/1.21.1/gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
minecraft_version_numeric=12101
|
||||||
|
minecraft_api_version=1.21
|
||||||
|
paper_api_version=1.21.1-R0.1-SNAPSHOT
|
||||||
3
bukkit/1.21.4/gradle.properties
Normal file
3
bukkit/1.21.4/gradle.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
minecraft_version_numeric=12104
|
||||||
|
minecraft_api_version=1.21
|
||||||
|
paper_api_version=1.21.4-R0.1-SNAPSHOT
|
||||||
@@ -1,37 +1,67 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
id 'net.william278.preprocessor' version '1.0'
|
||||||
|
id 'xyz.jpenilla.run-paper' version '2.3.1'
|
||||||
|
id 'maven-publish'
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(path: ':common')
|
implementation project(path: ':common')
|
||||||
|
|
||||||
implementation 'org.bstats:bstats-bukkit:3.0.2'
|
implementation 'net.william278.uniform:uniform-bukkit:1.3.1'
|
||||||
|
implementation 'net.william278.uniform:uniform-paper:1.3.1'
|
||||||
|
implementation 'net.william278.toilet:toilet-bukkit:1.0.12'
|
||||||
implementation 'net.william278:mpdbdataconverter:1.0.1'
|
implementation 'net.william278:mpdbdataconverter:1.0.1'
|
||||||
implementation 'net.william278:hsldataconverter:1.0'
|
implementation 'net.william278:hsldataconverter:1.0'
|
||||||
implementation 'net.william278:mapdataapi:1.0.3'
|
implementation 'net.william278:mapdataapi:2.0'
|
||||||
implementation 'net.william278:andjam:1.0.2'
|
implementation 'org.bstats:bstats-bukkit:3.1.0'
|
||||||
implementation 'me.lucko:commodore:2.2'
|
implementation 'net.kyori:adventure-platform-bukkit:4.3.4'
|
||||||
implementation 'net.kyori:adventure-platform-bukkit:4.3.2'
|
implementation 'dev.triumphteam:triumph-gui:3.1.11'
|
||||||
implementation 'dev.triumphteam:triumph-gui:3.1.7'
|
|
||||||
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
|
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 "io.papermc.paper:paper-api:${paper_api_version}"
|
||||||
compileOnly 'com.github.retrooper.packetevents:spigot:2.3.0'
|
compileOnly 'com.github.retrooper:packetevents-spigot:2.7.0'
|
||||||
compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0'
|
compileOnly 'com.github.dmulloy2:ProtocolLib:5.3.0'
|
||||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
compileOnly 'org.projectlombok:lombok:1.18.36'
|
||||||
compileOnly 'commons-io:commons-io:2.16.1'
|
compileOnly 'commons-io:commons-io:2.18.0'
|
||||||
compileOnly 'org.json:json:20240303'
|
compileOnly 'org.json:json:20250107'
|
||||||
compileOnly 'net.william278:minedown:1.8.2'
|
compileOnly 'net.william278:minedown:1.8.2'
|
||||||
compileOnly 'de.exlll:configlib-yaml:4.5.0'
|
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:DesertWell:2.0.4'
|
||||||
compileOnly 'net.william278:AdvancementAPI:97a9583413'
|
compileOnly 'net.william278:AdvancementAPI:97a9583413'
|
||||||
compileOnly "redis.clients:jedis:$jedis_version"
|
compileOnly "redis.clients:jedis:$jedis_version"
|
||||||
|
|
||||||
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
annotationProcessor 'org.projectlombok:lombok:1.18.36'
|
||||||
|
}
|
||||||
|
|
||||||
|
processResources {
|
||||||
|
filesMatching(['**/*.json', '**/*.yml']) {
|
||||||
|
expand([
|
||||||
|
version: version,
|
||||||
|
paper_api_version: paper_api_version,
|
||||||
|
minecraft_version: project.name,
|
||||||
|
minecraft_api_version: minecraft_api_version
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets.main {
|
||||||
|
java.srcDirs '../src/main/java'
|
||||||
|
resources.srcDirs '../src/main/resources'
|
||||||
|
}
|
||||||
|
javadoc.setSource('./build/generated/preprocessed/main/java')
|
||||||
|
|
||||||
|
preprocess {
|
||||||
|
vars.put('MC', minecraft_version_numeric)
|
||||||
}
|
}
|
||||||
|
|
||||||
shadowJar {
|
shadowJar {
|
||||||
dependencies {
|
dependencies {
|
||||||
exclude(dependency('com.mojang:brigadier'))
|
exclude(dependency('com.mojang:brigadier'))
|
||||||
}
|
}
|
||||||
|
|
||||||
relocate 'org.apache.commons.io', 'net.william278.husksync.libraries.commons.io'
|
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.text', 'net.william278.husksync.libraries.commons.text'
|
||||||
relocate 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3'
|
relocate 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3'
|
||||||
@@ -42,16 +72,16 @@ shadowJar {
|
|||||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||||
relocate 'de.exlll', 'net.william278.husksync.libraries'
|
relocate 'de.exlll', 'net.william278.husksync.libraries'
|
||||||
|
relocate 'net.william278.uniform', 'net.william278.husksync.libraries.uniform'
|
||||||
|
relocate 'net.william278.toilet', 'net.william278.husksync.libraries.toilet'
|
||||||
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
|
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
|
||||||
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
|
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
|
||||||
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
|
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.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
|
||||||
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
|
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
|
||||||
relocate 'org.json', 'net.william278.husksync.libraries.json'
|
relocate 'org.json', 'net.william278.husksync.libraries.json'
|
||||||
relocate 'net.querz', 'net.william278.husksync.libraries.nbtparser'
|
relocate 'net.querz', 'net.william278.husksync.libraries.nbtparser'
|
||||||
relocate 'net.roxeez', 'net.william278.husksync.libraries'
|
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 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||||
relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
|
relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
|
||||||
relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib'
|
relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib'
|
||||||
@@ -59,3 +89,9 @@ shadowJar {
|
|||||||
|
|
||||||
minimize()
|
minimize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
runServer {
|
||||||
|
minecraftVersion(project.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
5
bukkit/gradle.properties
Normal file
5
bukkit/gradle.properties
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
org.gradle.daemon=false
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.configureoncommand=true
|
||||||
|
org.gradle.parallel.threads=4
|
||||||
|
org.gradle.jvmargs=-Xmx8G
|
||||||
0
bukkit/root.gradle
Normal file
0
bukkit/root.gradle
Normal file
@@ -23,6 +23,7 @@ import com.google.common.collect.Lists;
|
|||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
import de.tr7zw.changeme.nbtapi.utils.DataFixerUtil;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
@@ -34,7 +35,7 @@ import net.william278.husksync.adapter.DataAdapter;
|
|||||||
import net.william278.husksync.adapter.GsonAdapter;
|
import net.william278.husksync.adapter.GsonAdapter;
|
||||||
import net.william278.husksync.adapter.SnappyGsonAdapter;
|
import net.william278.husksync.adapter.SnappyGsonAdapter;
|
||||||
import net.william278.husksync.api.BukkitHuskSyncAPI;
|
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.Locales;
|
||||||
import net.william278.husksync.config.Server;
|
import net.william278.husksync.config.Server;
|
||||||
import net.william278.husksync.config.Settings;
|
import net.william278.husksync.config.Settings;
|
||||||
@@ -46,6 +47,7 @@ import net.william278.husksync.database.PostgresDatabase;
|
|||||||
import net.william278.husksync.event.BukkitEventDispatcher;
|
import net.william278.husksync.event.BukkitEventDispatcher;
|
||||||
import net.william278.husksync.hook.PlanHook;
|
import net.william278.husksync.hook.PlanHook;
|
||||||
import net.william278.husksync.listener.BukkitEventListener;
|
import net.william278.husksync.listener.BukkitEventListener;
|
||||||
|
import net.william278.husksync.listener.LockedHandler;
|
||||||
import net.william278.husksync.migrator.LegacyMigrator;
|
import net.william278.husksync.migrator.LegacyMigrator;
|
||||||
import net.william278.husksync.migrator.Migrator;
|
import net.william278.husksync.migrator.Migrator;
|
||||||
import net.william278.husksync.migrator.MpdbMigrator;
|
import net.william278.husksync.migrator.MpdbMigrator;
|
||||||
@@ -54,16 +56,20 @@ import net.william278.husksync.sync.DataSyncer;
|
|||||||
import net.william278.husksync.user.BukkitUser;
|
import net.william278.husksync.user.BukkitUser;
|
||||||
import net.william278.husksync.user.OnlineUser;
|
import net.william278.husksync.user.OnlineUser;
|
||||||
import net.william278.husksync.util.BukkitLegacyConverter;
|
import net.william278.husksync.util.BukkitLegacyConverter;
|
||||||
import net.william278.husksync.util.BukkitMapPersister;
|
import net.william278.husksync.maps.BukkitMapHandler;
|
||||||
import net.william278.husksync.util.BukkitTask;
|
import net.william278.husksync.util.BukkitTask;
|
||||||
import net.william278.husksync.util.LegacyConverter;
|
import net.william278.husksync.util.LegacyConverter;
|
||||||
|
import net.william278.toilet.BukkitToilet;
|
||||||
|
import net.william278.toilet.Toilet;
|
||||||
|
import net.william278.uniform.Uniform;
|
||||||
|
import net.william278.uniform.bukkit.BukkitUniform;
|
||||||
import org.bstats.bukkit.Metrics;
|
import org.bstats.bukkit.Metrics;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.map.MapView;
|
import org.bukkit.map.MapView;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import space.arim.morepaperlib.MorePaperLib;
|
import space.arim.morepaperlib.MorePaperLib;
|
||||||
import space.arim.morepaperlib.commands.CommandRegistration;
|
|
||||||
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
|
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
|
||||||
import space.arim.morepaperlib.scheduling.AttachedScheduler;
|
import space.arim.morepaperlib.scheduling.AttachedScheduler;
|
||||||
import space.arim.morepaperlib.scheduling.GracefulScheduling;
|
import space.arim.morepaperlib.scheduling.GracefulScheduling;
|
||||||
@@ -76,8 +82,9 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier,
|
public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier,
|
||||||
BukkitEventDispatcher, BukkitMapPersister {
|
BukkitEventDispatcher, BukkitMapHandler {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metrics ID for <a href="https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140">HuskSync on Bukkit</a>.
|
* Metrics ID for <a href="https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140">HuskSync on Bukkit</a>.
|
||||||
@@ -85,7 +92,9 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
private static final int METRICS_ID = 13140;
|
private static final int METRICS_ID = 13140;
|
||||||
private static final String PLATFORM_TYPE_ID = "bukkit";
|
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<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap();
|
||||||
private final Map<Integer, MapView> mapViews = Maps.newConcurrentMap();
|
private final Map<Integer, MapView> mapViews = Maps.newConcurrentMap();
|
||||||
private final List<Migrator> availableMigrators = Lists.newArrayList();
|
private final List<Migrator> availableMigrators = Lists.newArrayList();
|
||||||
@@ -94,6 +103,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
private boolean disabling;
|
private boolean disabling;
|
||||||
private Gson gson;
|
private Gson gson;
|
||||||
private AudienceProvider audiences;
|
private AudienceProvider audiences;
|
||||||
|
private Toilet toilet;
|
||||||
private MorePaperLib paperLib;
|
private MorePaperLib paperLib;
|
||||||
private Database database;
|
private Database database;
|
||||||
private RedisManager redisManager;
|
private RedisManager redisManager;
|
||||||
@@ -123,6 +133,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
loadSettings();
|
loadSettings();
|
||||||
loadLocales();
|
loadLocales();
|
||||||
loadServer();
|
loadServer();
|
||||||
|
validateConfigFiles();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.eventListener = createEventListener();
|
this.eventListener = createEventListener();
|
||||||
@@ -132,6 +143,14 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
this.audiences = BukkitAudiences.create(this);
|
this.audiences = BukkitAudiences.create(this);
|
||||||
|
this.toilet = BukkitToilet.create(getDumpOptions());
|
||||||
|
|
||||||
|
// Check compatibility
|
||||||
|
checkCompatibility();
|
||||||
|
|
||||||
|
// Register commands
|
||||||
|
initialize("commands", (plugin) -> getUniform().register(PluginCommand.Type.create(this)));
|
||||||
|
|
||||||
// Prepare data adapter
|
// Prepare data adapter
|
||||||
initialize("data adapter", (plugin) -> {
|
initialize("data adapter", (plugin) -> {
|
||||||
if (settings.getSynchronization().isCompressData()) {
|
if (settings.getSynchronization().isCompressData()) {
|
||||||
@@ -143,19 +162,20 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
|
|
||||||
// Prepare serializers
|
// Prepare serializers
|
||||||
initialize("data serializers", (plugin) -> {
|
initialize("data serializers", (plugin) -> {
|
||||||
|
registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
|
||||||
registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this));
|
registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this));
|
||||||
registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this));
|
registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this));
|
||||||
registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(this));
|
registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(this));
|
||||||
registerSerializer(Identifier.LOCATION, new BukkitSerializer.Json<>(this, BukkitData.Location.class));
|
registerSerializer(Identifier.STATISTICS, new Serializer.Json<>(this, BukkitData.Statistics.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.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this));
|
registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this));
|
||||||
registerSerializer(Identifier.STATISTICS, new BukkitSerializer.Json<>(this, BukkitData.Statistics.class));
|
registerSerializer(Identifier.GAME_MODE, new Serializer.Json<>(this, BukkitData.GameMode.class));
|
||||||
registerSerializer(Identifier.EXPERIENCE, new BukkitSerializer.Json<>(this, BukkitData.Experience.class));
|
registerSerializer(Identifier.FLIGHT_STATUS, new Serializer.Json<>(this, BukkitData.FlightStatus.class));
|
||||||
registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
|
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
|
// Setup available migrators
|
||||||
@@ -192,9 +212,6 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
// Register events
|
// Register events
|
||||||
initialize("events", (plugin) -> eventListener.onEnable());
|
initialize("events", (plugin) -> eventListener.onEnable());
|
||||||
|
|
||||||
// Register commands
|
|
||||||
initialize("commands", (plugin) -> BukkitCommand.Type.registerCommands(this));
|
|
||||||
|
|
||||||
// Register plugin hooks
|
// Register plugin hooks
|
||||||
initialize("hooks", (plugin) -> {
|
initialize("hooks", (plugin) -> {
|
||||||
if (isDependencyLoaded("Plan") && getSettings().isEnablePlanHook()) {
|
if (isDependencyLoaded("Plan") && getSettings().isEnablePlanHook()) {
|
||||||
@@ -260,6 +277,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
this.dataSyncer = dataSyncer;
|
this.dataSyncer = dataSyncer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public Uniform getUniform() {
|
||||||
|
return BukkitUniform.getInstance(this);
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
|
public Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
|
||||||
@@ -277,7 +300,8 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isDependencyLoaded(@NotNull String name) {
|
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
|
// Register bStats metrics
|
||||||
@@ -289,7 +313,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
try {
|
try {
|
||||||
new Metrics(this, metricsId);
|
new Metrics(this, metricsId);
|
||||||
} catch (Throwable e) {
|
} 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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,17 +338,45 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
return Version.fromString(getServer().getBukkitVersion());
|
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
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public String getPlatformType() {
|
public String getPlatformType() {
|
||||||
return PLATFORM_TYPE_ID;
|
return PLATFORM_TYPE_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public String getServerVersion() {
|
||||||
|
return String.format("%s/%s", getServer().getName(), getServer().getVersion());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<LegacyConverter> getLegacyConverter() {
|
public Optional<LegacyConverter> getLegacyConverter() {
|
||||||
return Optional.of(legacyConverter);
|
return Optional.of(legacyConverter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public LockedHandler getLockedHandler() {
|
||||||
|
return eventListener.getLockedHandler();
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public GracefulScheduling getScheduler() {
|
public GracefulScheduling getScheduler() {
|
||||||
return paperLib.scheduling();
|
return paperLib.scheduling();
|
||||||
@@ -347,11 +399,6 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
return getScheduler().entitySpecificScheduler(((BukkitUser) user).getPlayer());
|
return getScheduler().entitySpecificScheduler(((BukkitUser) user).getPlayer());
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public CommandRegistration getCommandRegistrar() {
|
|
||||||
return paperLib.commandRegistration();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@NotNull
|
@NotNull
|
||||||
public Path getConfigDirectory() {
|
public Path getConfigDirectory() {
|
||||||
|
|||||||
@@ -20,14 +20,17 @@
|
|||||||
package net.william278.husksync;
|
package net.william278.husksync;
|
||||||
|
|
||||||
import net.kyori.adventure.audience.Audience;
|
import net.kyori.adventure.audience.Audience;
|
||||||
|
import net.william278.desertwell.util.Version;
|
||||||
import net.william278.husksync.listener.BukkitEventListener;
|
import net.william278.husksync.listener.BukkitEventListener;
|
||||||
import net.william278.husksync.listener.PaperEventListener;
|
import net.william278.husksync.listener.PaperEventListener;
|
||||||
|
import net.william278.uniform.Uniform;
|
||||||
|
import net.william278.uniform.paper.PaperUniform;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings({"unused"})
|
||||||
public class PaperHuskSync extends BukkitHuskSync {
|
public class PaperHuskSync extends BukkitHuskSync {
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -43,4 +46,15 @@ public class PaperHuskSync extends BukkitHuskSync {
|
|||||||
return player == null || !player.isOnline() ? Audience.empty() : player;
|
return player == null || !player.isOnline() ? Audience.empty() : player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public Version getMinecraftVersion() {
|
||||||
|
return Version.fromString(getServer().getMinecraftVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public Uniform getUniform() {
|
||||||
|
return PaperUniform.getInstance(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@ import net.william278.husksync.user.OnlineUser;
|
|||||||
import net.william278.husksync.user.User;
|
import net.william278.husksync.user.User;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
@@ -59,6 +60,10 @@ public class BukkitHuskSyncAPI extends HuskSyncAPI {
|
|||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
public static BukkitHuskSyncAPI getInstance() {
|
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) {
|
if (instance == null) {
|
||||||
throw new NotRegisteredException();
|
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,20 @@ import net.william278.desertwell.util.ThrowingConsumer;
|
|||||||
import net.william278.husksync.BukkitHuskSync;
|
import net.william278.husksync.BukkitHuskSync;
|
||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
import net.william278.husksync.adapter.Adaptable;
|
import net.william278.husksync.adapter.Adaptable;
|
||||||
|
import net.william278.husksync.config.Settings.SynchronizationSettings.AttributeSettings;
|
||||||
import net.william278.husksync.user.BukkitUser;
|
import net.william278.husksync.user.BukkitUser;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.*;
|
||||||
import org.bukkit.Material;
|
|
||||||
import org.bukkit.Registry;
|
|
||||||
import org.bukkit.Statistic;
|
|
||||||
import org.bukkit.advancement.AdvancementProgress;
|
import org.bukkit.advancement.AdvancementProgress;
|
||||||
import org.bukkit.attribute.AttributeInstance;
|
import org.bukkit.attribute.AttributeInstance;
|
||||||
import org.bukkit.attribute.AttributeModifier;
|
import org.bukkit.attribute.AttributeModifier;
|
||||||
|
import org.bukkit.entity.EntityType;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.inventory.InventoryType;
|
import org.bukkit.event.inventory.InventoryType;
|
||||||
import org.bukkit.inventory.EquipmentSlot;
|
//#if MC==12001
|
||||||
|
//$$ import org.bukkit.inventory.EquipmentSlot;
|
||||||
|
//#else
|
||||||
|
import org.bukkit.inventory.EquipmentSlotGroup;
|
||||||
|
//#endif
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.persistence.PersistentDataContainer;
|
import org.bukkit.persistence.PersistentDataContainer;
|
||||||
import org.bukkit.potion.PotionEffect;
|
import org.bukkit.potion.PotionEffect;
|
||||||
@@ -47,6 +50,7 @@ import org.bukkit.potion.PotionEffectType;
|
|||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.jetbrains.annotations.Range;
|
import org.jetbrains.annotations.Range;
|
||||||
|
import org.jetbrains.annotations.Unmodifiable;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
@@ -69,7 +73,6 @@ public abstract class BukkitData implements Data {
|
|||||||
private final @Nullable ItemStack @NotNull [] contents;
|
private final @Nullable ItemStack @NotNull [] contents;
|
||||||
|
|
||||||
private Items(@Nullable ItemStack @NotNull [] contents) {
|
private Items(@Nullable ItemStack @NotNull [] contents) {
|
||||||
|
|
||||||
this.contents = Arrays.stream(contents.clone())
|
this.contents = Arrays.stream(contents.clone())
|
||||||
.map(i -> i == null || i.getType() == Material.AIR ? null : i)
|
.map(i -> i == null || i.getType() == Material.AIR ? null : i)
|
||||||
.toArray(ItemStack[]::new);
|
.toArray(ItemStack[]::new);
|
||||||
@@ -127,8 +130,6 @@ public abstract class BukkitData implements Data {
|
|||||||
@Getter
|
@Getter
|
||||||
public static class Inventory extends BukkitData.Items implements Data.Items.Inventory {
|
public static class Inventory extends BukkitData.Items implements Data.Items.Inventory {
|
||||||
|
|
||||||
public static final int INVENTORY_SLOT_COUNT = 41;
|
|
||||||
|
|
||||||
@Range(from = 0, to = 8)
|
@Range(from = 0, to = 8)
|
||||||
private int heldItemSlot;
|
private int heldItemSlot;
|
||||||
|
|
||||||
@@ -158,8 +159,9 @@ public abstract class BukkitData implements Data {
|
|||||||
this.clearInventoryCraftingSlots(player);
|
this.clearInventoryCraftingSlots(player);
|
||||||
player.setItemOnCursor(null);
|
player.setItemOnCursor(null);
|
||||||
player.getInventory().setContents(plugin.setMapViews(getContents()));
|
player.getInventory().setContents(plugin.setMapViews(getContents()));
|
||||||
player.updateInventory();
|
|
||||||
player.getInventory().setHeldItemSlot(heldItemSlot);
|
player.getInventory().setHeldItemSlot(heldItemSlot);
|
||||||
|
//noinspection UnstableApiUsage
|
||||||
|
player.updateInventory();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearInventoryCraftingSlots(@NotNull Player player) {
|
private void clearInventoryCraftingSlots(@NotNull Player player) {
|
||||||
@@ -175,15 +177,18 @@ public abstract class BukkitData implements Data {
|
|||||||
|
|
||||||
public static class EnderChest extends BukkitData.Items implements Data.Items.EnderChest {
|
public static class EnderChest extends BukkitData.Items implements Data.Items.EnderChest {
|
||||||
|
|
||||||
public static final int ENDER_CHEST_SLOT_COUNT = 27;
|
private EnderChest(@Nullable ItemStack @NotNull [] contents) {
|
||||||
|
|
||||||
private EnderChest(@NotNull ItemStack[] contents) {
|
|
||||||
super(contents);
|
super(contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public static BukkitData.Items.EnderChest adapt(@NotNull ItemStack[] items) {
|
public static BukkitData.Items.EnderChest adapt(@Nullable ItemStack @NotNull [] contents) {
|
||||||
return new BukkitData.Items.EnderChest(items);
|
return new BukkitData.Items.EnderChest(contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public static BukkitData.Items.EnderChest adapt(@NotNull Collection<ItemStack> items) {
|
||||||
|
return adapt(items.toArray(ItemStack[]::new));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -200,7 +205,7 @@ public abstract class BukkitData implements Data {
|
|||||||
|
|
||||||
public static class ItemArray extends BukkitData.Items implements Data.Items {
|
public static class ItemArray extends BukkitData.Items implements Data.Items {
|
||||||
|
|
||||||
private ItemArray(@NotNull ItemStack[] contents) {
|
private ItemArray(@Nullable ItemStack @NotNull [] contents) {
|
||||||
super(contents);
|
super(contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +215,7 @@ public abstract class BukkitData implements Data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public static ItemArray adapt(@NotNull ItemStack[] drops) {
|
public static ItemArray adapt(@Nullable ItemStack @NotNull [] drops) {
|
||||||
return new ItemArray(drops);
|
return new ItemArray(drops);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,33 +236,33 @@ public abstract class BukkitData implements Data {
|
|||||||
private final Collection<PotionEffect> effects;
|
private final Collection<PotionEffect> effects;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> effects) {
|
public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> sei) {
|
||||||
return new BukkitData.PotionEffects(effects);
|
return new BukkitData.PotionEffects(Lists.newArrayList(sei.stream().filter(e -> !e.isAmbient()).toList()));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public static BukkitData.PotionEffects adapt(@NotNull Collection<Effect> effects) {
|
public static BukkitData.PotionEffects adapt(@NotNull Collection<Effect> effects) {
|
||||||
return from(
|
return from(effects.stream()
|
||||||
effects.stream()
|
.map(effect -> {
|
||||||
.map(effect -> new PotionEffect(
|
final PotionEffectType type = matchEffectType(effect.type());
|
||||||
Objects.requireNonNull(
|
return type != null ? new PotionEffect(
|
||||||
PotionEffectType.getByName(effect.type()),
|
type,
|
||||||
"Invalid potion effect type"
|
|
||||||
),
|
|
||||||
effect.duration(),
|
effect.duration(),
|
||||||
effect.amplifier(),
|
effect.amplifier(),
|
||||||
effect.isAmbient(),
|
effect.isAmbient(),
|
||||||
effect.showParticles(),
|
effect.showParticles(),
|
||||||
effect.hasIcon()
|
effect.hasIcon()
|
||||||
))
|
) : null;
|
||||||
.toList()
|
})
|
||||||
);
|
.filter(Objects::nonNull)
|
||||||
|
.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public static BukkitData.PotionEffects empty() {
|
public static BukkitData.PotionEffects empty() {
|
||||||
return new BukkitData.PotionEffects(List.of());
|
return new BukkitData.PotionEffects(Lists.newArrayList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -273,10 +278,11 @@ public abstract class BukkitData implements Data {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
|
@Unmodifiable
|
||||||
public List<Effect> getActiveEffects() {
|
public List<Effect> getActiveEffects() {
|
||||||
return effects.stream()
|
return effects.stream()
|
||||||
.map(potionEffect -> new Effect(
|
.map(potionEffect -> new Effect(
|
||||||
potionEffect.getType().getName().toLowerCase(Locale.ENGLISH),
|
potionEffect.getType().getKey().toString(),
|
||||||
potionEffect.getAmplifier(),
|
potionEffect.getAmplifier(),
|
||||||
potionEffect.getDuration(),
|
potionEffect.getDuration(),
|
||||||
potionEffect.isAmbient(),
|
potionEffect.isAmbient(),
|
||||||
@@ -342,9 +348,12 @@ public abstract class BukkitData implements Data {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setAdvancement(@NotNull HuskSync plugin, @NotNull org.bukkit.advancement.Advancement advancement,
|
private void setAdvancement(@NotNull HuskSync plugin,
|
||||||
@NotNull Player player, @NotNull BukkitUser user,
|
@NotNull org.bukkit.advancement.Advancement advancement,
|
||||||
@NotNull Collection<String> toAward, @NotNull Collection<String> toRevoke) {
|
@NotNull Player player,
|
||||||
|
@NotNull BukkitUser user,
|
||||||
|
@NotNull Collection<String> toAward,
|
||||||
|
@NotNull Collection<String> toRevoke) {
|
||||||
plugin.runSync(() -> {
|
plugin.runSync(() -> {
|
||||||
// Track player exp level & progress
|
// Track player exp level & progress
|
||||||
final int expLevel = player.getLevel();
|
final int expLevel = player.getLevel();
|
||||||
@@ -356,7 +365,8 @@ public abstract class BukkitData implements Data {
|
|||||||
toRevoke.forEach(progress::revokeCriteria);
|
toRevoke.forEach(progress::revokeCriteria);
|
||||||
|
|
||||||
// Set player experience and level (prevent advancement awards applying twice), reset game rule
|
// 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.setLevel(expLevel);
|
||||||
player.setExp(expProgress);
|
player.setExp(expProgress);
|
||||||
}
|
}
|
||||||
@@ -365,6 +375,9 @@ public abstract class BukkitData implements Data {
|
|||||||
|
|
||||||
// Performs a consuming function for every advancement registered on the server
|
// Performs a consuming function for every advancement registered on the server
|
||||||
private static void forEachAdvancement(@NotNull ThrowingConsumer<org.bukkit.advancement.Advancement> consumer) {
|
private static void forEachAdvancement(@NotNull ThrowingConsumer<org.bukkit.advancement.Advancement> consumer) {
|
||||||
|
final StringJoiner joiner = new StringJoiner(", ");
|
||||||
|
Bukkit.getServer().advancementIterator().forEachRemaining(a -> joiner.add(a.toString()));
|
||||||
|
Bukkit.getLogger().log(Level.INFO, "Advancements: %s".formatted(joiner.toString()));
|
||||||
Bukkit.getServer().advancementIterator().forEachRemaining(consumer);
|
Bukkit.getServer().advancementIterator().forEachRemaining(consumer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,9 +460,10 @@ public abstract class BukkitData implements Data {
|
|||||||
Registry.STATISTIC.forEach(id -> {
|
Registry.STATISTIC.forEach(id -> {
|
||||||
switch (id.getType()) {
|
switch (id.getType()) {
|
||||||
case UNTYPED -> addStatistic(player, id, generic);
|
case UNTYPED -> addStatistic(player, id, generic);
|
||||||
case BLOCK -> addMaterialStatistic(player, id, blocks, true);
|
// Todo - Future - Use BLOCK and ITEM registries when API stabilizes
|
||||||
case ITEM -> addMaterialStatistic(player, id, items, false);
|
case BLOCK -> addStatistic(player, id, Registry.MATERIAL, blocks);
|
||||||
case ENTITY -> addEntityStatistic(player, id, entities);
|
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);
|
return new BukkitData.Statistics(generic, blocks, items, entities);
|
||||||
@@ -470,43 +484,31 @@ public abstract class BukkitData implements Data {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addMaterialStatistic(@NotNull Player p, @NotNull Statistic id,
|
private static <R extends Keyed> void addStatistic(@NotNull Player p, @NotNull Statistic id,
|
||||||
@NotNull Map<String, Map<String, Integer>> map, boolean isBlock) {
|
@NotNull Registry<R> registry,
|
||||||
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) {
|
@NotNull Map<String, Map<String, Integer>> map) {
|
||||||
Registry.ENTITY_TYPE.forEach(entity -> {
|
registry.forEach(i -> {
|
||||||
if (!entity.isAlive()) {
|
try {
|
||||||
return;
|
final int stat = i instanceof Material m ? p.getStatistic(id, m) :
|
||||||
}
|
(i instanceof EntityType e ? p.getStatistic(id, e) : -1);
|
||||||
final int stat = p.getStatistic(id, entity);
|
|
||||||
if (stat != 0) {
|
if (stat != 0) {
|
||||||
map.computeIfAbsent(id.getKey().getKey(), k -> Maps.newHashMap())
|
map.compute(id.getKey().getKey(), (k, v) -> v == null ? Maps.newHashMap() : v)
|
||||||
.put(entity.getKey().getKey(), stat);
|
.put(i.getKey().getKey(), stat);
|
||||||
|
}
|
||||||
|
} catch (IllegalStateException ignored) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) {
|
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync p) {
|
||||||
genericStatistics.forEach((id, v) -> applyStat(user, id, Statistic.Type.UNTYPED, v));
|
genericStatistics.forEach((k, v) -> applyStat(p, user, k, Statistic.Type.UNTYPED, v));
|
||||||
blockStatistics.forEach((id, m) -> m.forEach((b, v) -> applyStat(user, id, Statistic.Type.BLOCK, v, b)));
|
blockStatistics.forEach((k, m) -> m.forEach((b, v) -> applyStat(p, user, k, Statistic.Type.BLOCK, v, b)));
|
||||||
itemStatistics.forEach((id, m) -> m.forEach((i, v) -> applyStat(user, id, Statistic.Type.ITEM, v, i)));
|
itemStatistics.forEach((k, m) -> m.forEach((i, v) -> applyStat(p, user, k, Statistic.Type.ITEM, v, i)));
|
||||||
entityStatistics.forEach((id, m) -> m.forEach((e, v) -> applyStat(user, id, Statistic.Type.ENTITY, v, e)));
|
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) {
|
@NotNull Statistic.Type type, int value, @NotNull String... key) {
|
||||||
final Player player = ((BukkitUser) user).getPlayer();
|
final Player player = ((BukkitUser) user).getPlayer();
|
||||||
final Statistic stat = matchStatistic(id);
|
final Statistic stat = matchStatistic(id);
|
||||||
@@ -520,7 +522,8 @@ public abstract class BukkitData implements Data {
|
|||||||
case BLOCK, ITEM -> player.setStatistic(stat, Objects.requireNonNull(matchMaterial(key[0])), value);
|
case BLOCK, ITEM -> player.setStatistic(stat, Objects.requireNonNull(matchMaterial(key[0])), value);
|
||||||
case ENTITY -> player.setStatistic(stat, Objects.requireNonNull(matchEntityType(key[0])), value);
|
case 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 +558,7 @@ public abstract class BukkitData implements Data {
|
|||||||
@Getter
|
@Getter
|
||||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
@SuppressWarnings("UnstableApiUsage")
|
||||||
public static class Attributes extends BukkitData implements Data.Attributes, Adaptable {
|
public static class Attributes extends BukkitData implements Data.Attributes, Adaptable {
|
||||||
|
|
||||||
private List<Attribute> attributes;
|
private List<Attribute> attributes;
|
||||||
@@ -562,14 +566,13 @@ public abstract class BukkitData implements Data {
|
|||||||
@NotNull
|
@NotNull
|
||||||
public static BukkitData.Attributes adapt(@NotNull Player player, @NotNull HuskSync plugin) {
|
public static BukkitData.Attributes adapt(@NotNull Player player, @NotNull HuskSync plugin) {
|
||||||
final List<Attribute> attributes = Lists.newArrayList();
|
final List<Attribute> attributes = Lists.newArrayList();
|
||||||
|
final AttributeSettings settings = plugin.getSettings().getSynchronization().getAttributes();
|
||||||
Registry.ATTRIBUTE.forEach(id -> {
|
Registry.ATTRIBUTE.forEach(id -> {
|
||||||
final AttributeInstance instance = player.getAttribute(id);
|
final AttributeInstance instance = player.getAttribute(id);
|
||||||
if (instance == null || instance.getValue() == instance.getDefaultValue() || plugin
|
if (settings.isIgnoredAttribute(id.getKey().toString()) || instance == null) {
|
||||||
.getSettings().getSynchronization().isIgnoredAttribute(id.getKey().toString())) {
|
return; // We don't sync attributes not marked as to be synced
|
||||||
// We don't sync unmodified or disabled attributes
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
attributes.add(adapt(instance));
|
attributes.add(adapt(instance, settings));
|
||||||
});
|
});
|
||||||
return new BukkitData.Attributes(attributes);
|
return new BukkitData.Attributes(attributes);
|
||||||
}
|
}
|
||||||
@@ -588,47 +591,87 @@ public abstract class BukkitData implements Data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private static Attribute adapt(@NotNull AttributeInstance instance) {
|
private static Attribute adapt(@NotNull AttributeInstance instance, @NotNull AttributeSettings settings) {
|
||||||
return new Attribute(
|
return new Attribute(
|
||||||
instance.getAttribute().getKey().toString(),
|
instance.getAttribute().getKey().toString(),
|
||||||
instance.getBaseValue(),
|
instance.getBaseValue(),
|
||||||
instance.getModifiers().stream().map(BukkitData.Attributes::adapt).collect(Collectors.toSet())
|
instance.getModifiers().stream()
|
||||||
|
.filter(modifier -> !settings.isIgnoredModifier(modifier.getName()))
|
||||||
|
//#if MC==12001
|
||||||
|
//$$ .filter(modifier -> modifier.getSlot() == null)
|
||||||
|
//#else
|
||||||
|
.filter(modifier -> modifier.getSlotGroup() != EquipmentSlotGroup.ANY)
|
||||||
|
//#endif
|
||||||
|
.map(BukkitData.Attributes::adapt).collect(Collectors.toSet())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private static Modifier adapt(@NotNull AttributeModifier modifier) {
|
private static Modifier adapt(@NotNull AttributeModifier modifier) {
|
||||||
|
//#if MC==12001
|
||||||
|
//$$ return new Modifier(
|
||||||
|
//$$ modifier.getUniqueId(),
|
||||||
|
//$$ modifier.getName(),
|
||||||
|
//$$ modifier.getAmount(),
|
||||||
|
//$$ modifier.getOperation().ordinal(),
|
||||||
|
//$$ modifier.getSlot() != null ? modifier.getSlot().ordinal() : -1
|
||||||
|
//$$ );
|
||||||
|
//#else
|
||||||
return new Modifier(
|
return new Modifier(
|
||||||
modifier.getUniqueId(),
|
modifier.getKey().toString(),
|
||||||
modifier.getName(),
|
|
||||||
modifier.getAmount(),
|
modifier.getAmount(),
|
||||||
modifier.getOperation().ordinal(),
|
modifier.getOperation().ordinal(),
|
||||||
modifier.getSlot() != null ? modifier.getSlot().ordinal() : -1
|
modifier.getSlotGroup().toString()
|
||||||
);
|
);
|
||||||
}
|
//#endif
|
||||||
|
|
||||||
@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) {
|
private static void applyAttribute(@Nullable AttributeInstance instance, @Nullable Attribute attribute) {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
instance.setBaseValue(attribute == null ? instance.getDefaultValue() : instance.getBaseValue());
|
|
||||||
instance.getModifiers().forEach(instance::removeModifier);
|
instance.getModifiers().forEach(instance::removeModifier);
|
||||||
|
instance.setBaseValue(attribute == null ? instance.getValue() : attribute.baseValue());
|
||||||
if (attribute != null) {
|
if (attribute != null) {
|
||||||
attribute.modifiers().forEach(modifier -> instance.addModifier(new AttributeModifier(
|
attribute.modifiers().stream()
|
||||||
modifier.uuid(),
|
.filter(mod -> instance.getModifiers().stream().map(AttributeModifier::getName)
|
||||||
modifier.name(),
|
.noneMatch(n -> n.equals(mod.name())))
|
||||||
modifier.amount(),
|
.distinct().filter(mod -> !mod.hasUuid())
|
||||||
AttributeModifier.Operation.values()[modifier.operationType()],
|
.forEach(mod -> instance.addModifier(adapt(mod)));
|
||||||
modifier.equipmentSlot() != -1 ? EquipmentSlot.values()[modifier.equipmentSlot()] : null
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static AttributeModifier adapt(@NotNull Modifier modifier) {
|
||||||
|
//#if MC==12001
|
||||||
|
//$$ return new AttributeModifier(
|
||||||
|
//$$ modifier.uuid(),
|
||||||
|
//$$ modifier.name(),
|
||||||
|
//$$ modifier.amount(),
|
||||||
|
//$$ AttributeModifier.Operation.values()[modifier.operation()],
|
||||||
|
//$$ modifier.equipmentSlot() != -1 ? EquipmentSlot.values()[modifier.equipmentSlot()] : null
|
||||||
|
//$$ );
|
||||||
|
//#else
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
//#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
@Getter
|
||||||
@@ -640,24 +683,38 @@ public abstract class BukkitData implements Data {
|
|||||||
private double health;
|
private double health;
|
||||||
@SerializedName("health_scale")
|
@SerializedName("health_scale")
|
||||||
private double healthScale;
|
private double healthScale;
|
||||||
|
@SerializedName("is_health_scaled")
|
||||||
|
private boolean isHealthScaled;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public static BukkitData.Health from(double health, double healthScale) {
|
public static BukkitData.Health from(double health, double scale, boolean isScaled) {
|
||||||
return new BukkitData.Health(health, healthScale);
|
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
|
@NotNull
|
||||||
@Deprecated(forRemoval = true, since = "3.5")
|
@Deprecated(forRemoval = true, since = "3.5")
|
||||||
@SuppressWarnings("unused")
|
public static BukkitData.Health from(double health, @SuppressWarnings("unused") double max, double scale) {
|
||||||
public static BukkitData.Health from(double health, double maxHealth, double healthScale) {
|
return from(health, scale, false);
|
||||||
return from(health, healthScale);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public static BukkitData.Health adapt(@NotNull Player player) {
|
public static BukkitData.Health adapt(@NotNull Player player) {
|
||||||
return from(
|
return from(
|
||||||
player.getHealth(),
|
player.getHealth(),
|
||||||
player.isHealthScaled() ? player.getHealthScale() : 0d
|
player.getHealthScale(),
|
||||||
|
player.isHealthScaled()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,16 +731,12 @@ public abstract class BukkitData implements Data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set health scale
|
// Set health scale
|
||||||
|
double scale = healthScale <= 0 ? player.getMaxHealth() : healthScale;
|
||||||
try {
|
try {
|
||||||
if (healthScale != 0d) {
|
player.setHealthScale(scale);
|
||||||
player.setHealthScaled(true);
|
player.setHealthScaled(isHealthScaled);
|
||||||
player.setHealthScale(healthScale);
|
|
||||||
} else {
|
|
||||||
player.setHealthScaled(false);
|
|
||||||
player.setHealthScale(player.getMaxHealth());
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
} 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.AccessLevel;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import net.william278.desertwell.util.Version;
|
import net.william278.desertwell.util.Version;
|
||||||
|
import net.william278.husksync.BukkitHuskSync;
|
||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
import net.william278.husksync.adapter.Adaptable;
|
import net.william278.husksync.adapter.Adaptable;
|
||||||
import net.william278.husksync.api.HuskSyncAPI;
|
import net.william278.husksync.api.HuskSyncAPI;
|
||||||
@@ -41,6 +42,8 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static net.william278.husksync.data.BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
|
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)
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
public class BukkitSerializer {
|
public class BukkitSerializer {
|
||||||
@@ -60,8 +63,6 @@ public class BukkitSerializer {
|
|||||||
|
|
||||||
public static class Inventory extends BukkitSerializer implements Serializer<BukkitData.Items.Inventory>,
|
public static class Inventory extends BukkitSerializer implements Serializer<BukkitData.Items.Inventory>,
|
||||||
ItemDeserializer {
|
ItemDeserializer {
|
||||||
private static final String ITEMS_TAG = "items";
|
|
||||||
private static final String HELD_ITEM_SLOT_TAG = "held_item_slot";
|
|
||||||
|
|
||||||
public Inventory(@NotNull HuskSync plugin) {
|
public Inventory(@NotNull HuskSync plugin) {
|
||||||
super(plugin);
|
super(plugin);
|
||||||
@@ -74,7 +75,7 @@ public class BukkitSerializer {
|
|||||||
final ReadWriteNBT items = root.hasTag(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null;
|
final ReadWriteNBT items = root.hasTag(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null;
|
||||||
return BukkitData.Items.Inventory.from(
|
return BukkitData.Items.Inventory.from(
|
||||||
items != null ? getItems(items, dataMcVersion) : new ItemStack[INVENTORY_SLOT_COUNT],
|
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
|
@Nullable
|
||||||
default ItemStack[] getItems(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion) {
|
default ItemStack[] getItems(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion) {
|
||||||
if (mcVersion.compareTo(getPlugin().getMinecraftVersion()) < 0) {
|
if (mcVersion.compareTo(getPlugin().getMinecraftVersion()) < 0) {
|
||||||
return upgradeItemStack((NBTCompound) tag, mcVersion);
|
return upgradeItemStacks((NBTCompound) tag, mcVersion);
|
||||||
}
|
}
|
||||||
return NBT.itemStackArrayFromNBT(tag);
|
return NBT.itemStackArrayFromNBT(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private ItemStack @NotNull [] upgradeItemStack(@NotNull NBTCompound compound, @NotNull Version mcVersion) {
|
private ItemStack @NotNull [] upgradeItemStacks(@NotNull NBTCompound itemsNbt, @NotNull Version mcVersion) {
|
||||||
final ReadWriteNBTCompoundList items = compound.getCompoundList("items");
|
final ReadWriteNBTCompoundList items = itemsNbt.getCompoundList("items");
|
||||||
final ItemStack[] itemStacks = new ItemStack[compound.getInteger("size")];
|
final ItemStack[] itemStacks = new ItemStack[itemsNbt.getInteger("size")];
|
||||||
for (int i = 0; i < items.size(); i++) {
|
for (int i = 0; i < items.size(); i++) {
|
||||||
if (items.get(i) == null) {
|
if (items.get(i) == null) {
|
||||||
itemStacks[i] = new ItemStack(Material.AIR);
|
itemStacks[i] = new ItemStack(Material.AIR);
|
||||||
@@ -152,19 +153,11 @@ public class BukkitSerializer {
|
|||||||
@NotNull
|
@NotNull
|
||||||
private ReadWriteNBT upgradeItemData(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion)
|
private ReadWriteNBT upgradeItemData(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion)
|
||||||
throws NoSuchFieldException, IllegalAccessException {
|
throws NoSuchFieldException, IllegalAccessException {
|
||||||
return DataFixerUtil.fixUpItemData(tag, getDataVersion(mcVersion), DataFixerUtil.getCurrentVersion());
|
return DataFixerUtil.fixUpItemData(
|
||||||
}
|
tag,
|
||||||
|
getPlugin().getDataVersion(mcVersion),
|
||||||
private int getDataVersion(@NotNull Version mcVersion) {
|
DataFixerUtil.getCurrentVersion()
|
||||||
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();
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -226,7 +219,7 @@ public class BukkitSerializer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BukkitData.PersistentData deserialize(@NotNull String serialized) throws DeserializationException {
|
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
|
@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, @NotNull Class<T> type) {
|
||||||
|
super(plugin, 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
public BukkitHuskSync getPlugin() {
|
||||||
public String serialize(@NotNull T element) throws SerializationException {
|
return (BukkitHuskSync) plugin;
|
||||||
return plugin.getDataAdapter().toJson(element);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
package net.william278.husksync.data;
|
package net.william278.husksync.data;
|
||||||
|
|
||||||
import net.william278.husksync.BukkitHuskSync;
|
import net.william278.husksync.BukkitHuskSync;
|
||||||
import net.william278.husksync.util.BukkitMapPersister;
|
import net.william278.husksync.maps.BukkitMapHandler;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.inventory.PlayerInventory;
|
import org.bukkit.inventory.PlayerInventory;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -67,9 +67,9 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
|||||||
.isSyncDeadPlayersChangingServer())) {
|
.isSyncDeadPlayersChangingServer())) {
|
||||||
return Optional.of(BukkitData.Items.Inventory.empty());
|
return Optional.of(BukkitData.Items.Inventory.empty());
|
||||||
}
|
}
|
||||||
final PlayerInventory inventory = getBukkitPlayer().getInventory();
|
final PlayerInventory inventory = getPlayer().getInventory();
|
||||||
return Optional.of(BukkitData.Items.Inventory.from(
|
return Optional.of(BukkitData.Items.Inventory.from(
|
||||||
getMapPersister().persistLockedMaps(inventory.getContents(), getBukkitPlayer()),
|
getMapPersister().persistLockedMaps(inventory.getContents(), getPlayer()),
|
||||||
inventory.getHeldItemSlot()
|
inventory.getHeldItemSlot()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -78,83 +78,92 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
|||||||
@Override
|
@Override
|
||||||
default Optional<Data.Items.EnderChest> getEnderChest() {
|
default Optional<Data.Items.EnderChest> getEnderChest() {
|
||||||
return Optional.of(BukkitData.Items.EnderChest.adapt(
|
return Optional.of(BukkitData.Items.EnderChest.adapt(
|
||||||
getMapPersister().persistLockedMaps(getBukkitPlayer().getEnderChest().getContents(), getBukkitPlayer())
|
getMapPersister().persistLockedMaps(getPlayer().getEnderChest().getContents(), getPlayer())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
default Optional<Data.PotionEffects> getPotionEffects() {
|
default Optional<Data.PotionEffects> getPotionEffects() {
|
||||||
return Optional.of(BukkitData.PotionEffects.from(getBukkitPlayer().getActivePotionEffects()));
|
return Optional.of(BukkitData.PotionEffects.from(getPlayer().getActivePotionEffects()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
default Optional<Data.Advancements> getAdvancements() {
|
default Optional<Data.Advancements> getAdvancements() {
|
||||||
return Optional.of(BukkitData.Advancements.adapt(getBukkitPlayer()));
|
return Optional.of(BukkitData.Advancements.adapt(getPlayer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
default Optional<Data.Location> getLocation() {
|
default Optional<Data.Location> getLocation() {
|
||||||
return Optional.of(BukkitData.Location.adapt(getBukkitPlayer().getLocation()));
|
return Optional.of(BukkitData.Location.adapt(getPlayer().getLocation()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
default Optional<Data.Statistics> getStatistics() {
|
default Optional<Data.Statistics> getStatistics() {
|
||||||
return Optional.of(BukkitData.Statistics.adapt(getBukkitPlayer()));
|
return Optional.of(BukkitData.Statistics.adapt(getPlayer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
default Optional<Data.Health> getHealth() {
|
default Optional<Data.Health> getHealth() {
|
||||||
return Optional.of(BukkitData.Health.adapt(getBukkitPlayer()));
|
return Optional.of(BukkitData.Health.adapt(getPlayer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
default Optional<Data.Hunger> getHunger() {
|
default Optional<Data.Hunger> getHunger() {
|
||||||
return Optional.of(BukkitData.Hunger.adapt(getBukkitPlayer()));
|
return Optional.of(BukkitData.Hunger.adapt(getPlayer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
default Optional<Data.Attributes> getAttributes() {
|
default Optional<Data.Attributes> getAttributes() {
|
||||||
return Optional.of(BukkitData.Attributes.adapt(getBukkitPlayer(), getPlugin()));
|
return Optional.of(BukkitData.Attributes.adapt(getPlayer(), getPlugin()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
default Optional<Data.Experience> getExperience() {
|
default Optional<Data.Experience> getExperience() {
|
||||||
return Optional.of(BukkitData.Experience.adapt(getBukkitPlayer()));
|
return Optional.of(BukkitData.Experience.adapt(getPlayer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
default Optional<Data.GameMode> getGameMode() {
|
default Optional<Data.GameMode> getGameMode() {
|
||||||
return Optional.of(BukkitData.GameMode.adapt(getBukkitPlayer()));
|
return Optional.of(BukkitData.GameMode.adapt(getPlayer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
default Optional<Data.FlightStatus> getFlightStatus() {
|
default Optional<Data.FlightStatus> getFlightStatus() {
|
||||||
return Optional.of(BukkitData.FlightStatus.adapt(getBukkitPlayer()));
|
return Optional.of(BukkitData.FlightStatus.adapt(getPlayer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
default Optional<Data.PersistentData> getPersistentData() {
|
default Optional<Data.PersistentData> getPersistentData() {
|
||||||
return Optional.of(BukkitData.PersistentData.adapt(getBukkitPlayer().getPersistentDataContainer()));
|
return Optional.of(BukkitData.PersistentData.adapt(getPlayer().getPersistentDataContainer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isDead();
|
boolean isDead();
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
Player getBukkitPlayer();
|
Player getPlayer();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link #getPlayer()} instead
|
||||||
|
*/
|
||||||
|
@Deprecated(since = "3.6")
|
||||||
|
@NotNull
|
||||||
|
default Player getBukkitPlayer() {
|
||||||
|
return getPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
default BukkitMapPersister getMapPersister() {
|
default BukkitMapHandler getMapPersister() {
|
||||||
return (BukkitHuskSync) getPlugin();
|
return (BukkitHuskSync) getPlugin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
package net.william278.husksync.listener;
|
package net.william278.husksync.listener;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
import net.william278.husksync.BukkitHuskSync;
|
import net.william278.husksync.BukkitHuskSync;
|
||||||
import net.william278.husksync.data.BukkitData;
|
import net.william278.husksync.data.BukkitData;
|
||||||
import net.william278.husksync.user.BukkitUser;
|
import net.william278.husksync.user.BukkitUser;
|
||||||
@@ -36,6 +37,7 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Getter
|
||||||
public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
|
public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
|
||||||
BukkitDeathEventListener, Listener {
|
BukkitDeathEventListener, Listener {
|
||||||
|
|
||||||
@@ -133,7 +135,7 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
|
|||||||
@EventHandler(ignoreCancelled = true)
|
@EventHandler(ignoreCancelled = true)
|
||||||
public void onMapInitialize(@NotNull MapInitializeEvent event) {
|
public void onMapInitialize(@NotNull MapInitializeEvent event) {
|
||||||
if (plugin.getSettings().getSynchronization().isPersistLockedMaps() && event.getMap().isLocked()) {
|
if (plugin.getSettings().getSynchronization().isPersistLockedMaps() && event.getMap().isLocked()) {
|
||||||
getPlugin().runAsync(() -> ((BukkitHuskSync) plugin).renderMapFromFile(event.getMap()));
|
getPlugin().runAsync(() -> ((BukkitHuskSync) plugin).renderPersistedMap(event.getMap()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import com.github.retrooper.packetevents.PacketEvents;
|
|||||||
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
|
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
|
||||||
import com.github.retrooper.packetevents.event.PacketListenerPriority;
|
import com.github.retrooper.packetevents.event.PacketListenerPriority;
|
||||||
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
|
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
|
||||||
|
import com.github.retrooper.packetevents.event.PacketSendEvent;
|
||||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
|
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
|
||||||
@@ -39,12 +40,11 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@SuppressWarnings("UnstableApiUsage")
|
||||||
public void onLoad() {
|
public void onLoad() {
|
||||||
super.onLoad();
|
super.onLoad();
|
||||||
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(getPlugin()));
|
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(getPlugin()));
|
||||||
PacketEvents.getAPI().getSettings().reEncodeByDefault(false)
|
PacketEvents.getAPI().getSettings().reEncodeByDefault(false).checkForUpdates(false);
|
||||||
.checkForUpdates(false)
|
|
||||||
.bStats(true);
|
|
||||||
PacketEvents.getAPI().load();
|
PacketEvents.getAPI().load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +60,7 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
|
|||||||
|
|
||||||
private static final Set<PacketType.Play.Client> ALLOWED_PACKETS = Set.of(
|
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.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.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.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.HELD_ITEM_CHANGE, PacketType.Play.Client.ANIMATION, PacketType.Play.Client.TELEPORT_CONFIRM, // Animation packets
|
||||||
@@ -89,6 +90,19 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
// Returns the set of ALL Server packets, excluding the set of allowed packets
|
||||||
@NotNull
|
@NotNull
|
||||||
private static Set<PacketType.Play.Client> getPacketsToListenFor() {
|
private static Set<PacketType.Play.Client> getPacketsToListenFor() {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public class BukkitProtocolLibLockedPacketListener extends BukkitLockedEventList
|
|||||||
|
|
||||||
private static class PlayerPacketAdapter extends PacketAdapter {
|
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(
|
private static final Set<PacketType> ALLOWED_PACKETS = Set.of(
|
||||||
Client.KEEP_ALIVE, Client.PONG, Client.CUSTOM_PAYLOAD, // Connection packets
|
Client.KEEP_ALIVE, Client.PONG, Client.CUSTOM_PAYLOAD, // Connection packets
|
||||||
Client.CHAT_COMMAND, Client.CLIENT_COMMAND, Client.CHAT, Client.CHAT_SESSION_UPDATE, // Chat / command packets
|
Client.CHAT_COMMAND, Client.CLIENT_COMMAND, Client.CHAT, Client.CHAT_SESSION_UPDATE, // Chat / command packets
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ public class PaperEventListener extends BukkitEventListener {
|
|||||||
super(plugin);
|
super(plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
getPlugin().getServer().getPluginManager().registerEvents(this, getPlugin());
|
||||||
|
lockedHandler.onEnable();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handlePlayerDeath(@NotNull PlayerDeathEvent event) {
|
public void handlePlayerDeath(@NotNull PlayerDeathEvent event) {
|
||||||
// If the player is locked or the plugin disabling, clear their drops
|
// If the player is locked or the plugin disabling, clear their drops
|
||||||
@@ -17,43 +17,46 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package net.william278.husksync.util;
|
package net.william278.husksync.maps;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import de.tr7zw.changeme.nbtapi.NBT;
|
import de.tr7zw.changeme.nbtapi.NBT;
|
||||||
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
|
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
|
||||||
import de.tr7zw.changeme.nbtapi.iface.ReadableNBT;
|
import de.tr7zw.changeme.nbtapi.iface.ReadableNBT;
|
||||||
import net.querz.nbt.io.NBTUtil;
|
|
||||||
import net.querz.nbt.tag.CompoundTag;
|
|
||||||
import net.william278.husksync.BukkitHuskSync;
|
import net.william278.husksync.BukkitHuskSync;
|
||||||
|
import net.william278.husksync.redis.RedisManager;
|
||||||
import net.william278.mapdataapi.MapBanner;
|
import net.william278.mapdataapi.MapBanner;
|
||||||
import net.william278.mapdataapi.MapData;
|
import net.william278.mapdataapi.MapData;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Container;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.inventory.meta.BlockStateMeta;
|
||||||
|
import org.bukkit.inventory.meta.BundleMeta;
|
||||||
import org.bukkit.inventory.meta.MapMeta;
|
import org.bukkit.inventory.meta.MapMeta;
|
||||||
import org.bukkit.map.*;
|
import org.bukkit.map.*;
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
|
import org.jetbrains.annotations.Blocking;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.File;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
public interface BukkitMapPersister {
|
public interface BukkitMapHandler {
|
||||||
|
|
||||||
// The map used to store HuskSync data in ItemStack NBT
|
// The map used to store HuskSync data in ItemStack NBT
|
||||||
String MAP_DATA_KEY = "husksync:persisted_locked_map";
|
String MAP_DATA_KEY = "husksync:persisted_locked_map";
|
||||||
// The key used to store the serialized map data in NBT
|
// Name of server the map originates from
|
||||||
String MAP_PIXEL_DATA_KEY = "canvas_data";
|
String MAP_ORIGIN_KEY = "origin";
|
||||||
// The key used to store the map of World UIDs to MapView IDs in NBT
|
// Original map id
|
||||||
String MAP_VIEW_ID_MAPPINGS_KEY = "id_mappings";
|
String MAP_ID_KEY = "id";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist locked maps in an array of {@link ItemStack}s
|
* Persist locked maps in an array of {@link ItemStack}s
|
||||||
@@ -94,11 +97,111 @@ public interface BukkitMapPersister {
|
|||||||
}
|
}
|
||||||
if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) {
|
if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) {
|
||||||
items[i] = function.apply(item);
|
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);
|
||||||
|
item.setItemMeta(b);
|
||||||
|
} else if (item.getItemMeta() instanceof BundleMeta bundle) {
|
||||||
|
bundle.setItems(List.of(forEachMap(bundle.getItems().toArray(ItemStack[]::new), function)));
|
||||||
|
item.setItemMeta(bundle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
private void writeMapData(@NotNull String serverName, int mapId, MapData data) {
|
||||||
|
final byte[] dataBytes = getPlugin().getDataAdapter().toBytes(new AdaptableMapData(data));
|
||||||
|
getRedisManager().setMapData(serverName, mapId, dataBytes);
|
||||||
|
getPlugin().getDatabase().saveMapData(serverName, mapId, dataBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Blocking
|
||||||
|
private Map.Entry<MapData, Boolean> readMapData(@NotNull String serverName, int mapId) {
|
||||||
|
final Map.Entry<byte[], Boolean> readData = fetchMapData(serverName, mapId);
|
||||||
|
if (readData == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return deserializeMapData(readData);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Blocking
|
||||||
|
private Map.Entry<byte[], Boolean> fetchMapData(@NotNull String serverName, int mapId) {
|
||||||
|
return fetchMapData(serverName, mapId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Blocking
|
||||||
|
private Map.Entry<byte[], Boolean> fetchMapData(@NotNull String serverName, int mapId, boolean doReverseLookup) {
|
||||||
|
// Read from Redis cache
|
||||||
|
final byte[] redisData = getRedisManager().getMapData(serverName, mapId);
|
||||||
|
if (redisData != null) {
|
||||||
|
return new AbstractMap.SimpleImmutableEntry<>(redisData, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from database and set to Redis
|
||||||
|
@Nullable Map.Entry<byte[], Boolean> databaseData = getPlugin().getDatabase().getMapData(serverName, mapId);
|
||||||
|
if (databaseData != null) {
|
||||||
|
getRedisManager().setMapData(serverName, mapId, databaseData.getKey());
|
||||||
|
return databaseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, lookup a reverse map binding
|
||||||
|
if (doReverseLookup) {
|
||||||
|
return fetchReversedMapData(serverName, mapId);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Map.Entry<byte[], Boolean> fetchReversedMapData(@NotNull String serverName, int mapId) {
|
||||||
|
// Lookup binding from Redis cache, then fetch data if found
|
||||||
|
Map.Entry<String, Integer> binding = getRedisManager().getReversedMapBound(serverName, mapId);
|
||||||
|
if (binding != null) {
|
||||||
|
return fetchMapData(binding.getKey(), binding.getValue(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup binding from database, then set to Redis & fetch data if found
|
||||||
|
binding = getPlugin().getDatabase().getMapBinding(serverName, mapId);
|
||||||
|
if (binding != null) {
|
||||||
|
getRedisManager().bindMapIds(binding.getKey(), binding.getValue(), serverName, mapId);
|
||||||
|
return fetchMapData(binding.getKey(), binding.getValue(), false);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Map.Entry<MapData, Boolean> deserializeMapData(@NotNull Map.Entry<byte[], Boolean> data) {
|
||||||
|
try {
|
||||||
|
return new AbstractMap.SimpleImmutableEntry<>(
|
||||||
|
getPlugin().getDataAdapter().fromBytes(data.getKey(), AdaptableMapData.class)
|
||||||
|
.getData(getPlugin().getDataVersion(getPlugin().getMinecraftVersion())),
|
||||||
|
data.getValue()
|
||||||
|
);
|
||||||
|
} catch (IOException e) {
|
||||||
|
getPlugin().log(Level.WARNING, "Failed to deserialize map data", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the bound map ID
|
||||||
|
private int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName) {
|
||||||
|
// Get the map ID from Redis, if set
|
||||||
|
final Optional<Integer> redisId = getRedisManager().getBoundMapId(fromServerName, fromMapId, toServerName);
|
||||||
|
if (redisId.isPresent()) {
|
||||||
|
return redisId.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get from the database; if found, set to Redis
|
||||||
|
final int result = getPlugin().getDatabase().getBoundMapId(fromServerName, fromMapId, toServerName);
|
||||||
|
if (result != -1) {
|
||||||
|
getPlugin().getRedisManager().bindMapIds(fromServerName, fromMapId, toServerName, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private ItemStack persistMapView(@NotNull ItemStack map, @NotNull Player delegateRenderer) {
|
private ItemStack persistMapView(@NotNull ItemStack map, @NotNull Player delegateRenderer) {
|
||||||
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
|
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
|
||||||
@@ -117,7 +220,8 @@ public interface BukkitMapPersister {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render the map
|
// 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()) {
|
for (MapRenderer renderer : view.getRenderers()) {
|
||||||
renderer.render(view, canvas, delegateRenderer);
|
renderer.render(view, canvas, delegateRenderer);
|
||||||
getPlugin().debug(String.format("Rendered locked map canvas to view (#%s)", view.getId()));
|
getPlugin().debug(String.format("Rendered locked map canvas to view (#%s)", view.getId()));
|
||||||
@@ -125,14 +229,18 @@ public interface BukkitMapPersister {
|
|||||||
|
|
||||||
// Persist map data
|
// Persist map data
|
||||||
final ReadWriteNBT mapData = nbt.getOrCreateCompound(MAP_DATA_KEY);
|
final ReadWriteNBT mapData = nbt.getOrCreateCompound(MAP_DATA_KEY);
|
||||||
final String worldUid = view.getWorld().getUID().toString();
|
final String serverName = getPlugin().getServerName();
|
||||||
mapData.setByteArray(MAP_PIXEL_DATA_KEY, canvas.extractMapData().toBytes());
|
mapData.setString(MAP_ORIGIN_KEY, serverName);
|
||||||
nbt.getOrCreateCompound(MAP_VIEW_ID_MAPPINGS_KEY).setInteger(worldUid, view.getId());
|
mapData.setInteger(MAP_ID_KEY, meta.getMapId());
|
||||||
getPlugin().debug(String.format("Saved data for locked map (#%s, UID: %s)", view.getId(), worldUid));
|
if (readMapData(serverName, meta.getMapId()) == null) {
|
||||||
|
writeMapData(serverName, meta.getMapId(), canvas.extractMapData());
|
||||||
|
}
|
||||||
|
getPlugin().debug(String.format("Saved data for locked map (#%s, server: %s)", view.getId(), serverName));
|
||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
@NotNull
|
@NotNull
|
||||||
private ItemStack applyMapView(@NotNull ItemStack map) {
|
private ItemStack applyMapView(@NotNull ItemStack map) {
|
||||||
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
|
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
|
||||||
@@ -141,77 +249,67 @@ public interface BukkitMapPersister {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final ReadableNBT mapData = nbt.getCompound(MAP_DATA_KEY);
|
final ReadableNBT mapData = nbt.getCompound(MAP_DATA_KEY);
|
||||||
final ReadableNBT mapIds = nbt.getCompound(MAP_VIEW_ID_MAPPINGS_KEY);
|
if (mapData == null) {
|
||||||
if (mapData == null || mapIds == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search for an existing map view
|
// Determine map ID
|
||||||
Optional<String> world = Optional.empty();
|
final String originServerName = mapData.getString(MAP_ORIGIN_KEY);
|
||||||
for (String worldUid : mapIds.getKeys()) {
|
final String currentServerName = getPlugin().getServerName();
|
||||||
world = getPlugin().getServer().getWorlds().stream()
|
final int originalMapId = mapData.getInteger(MAP_ID_KEY);
|
||||||
.map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid))
|
int newId = currentServerName.equals(originServerName)
|
||||||
.findFirst();
|
? originalMapId : getBoundMapId(originServerName, originalMapId, currentServerName);
|
||||||
if (world.isPresent()) {
|
if (newId != -1) {
|
||||||
break;
|
meta.setMapId(newId);
|
||||||
}
|
|
||||||
}
|
|
||||||
if (world.isPresent()) {
|
|
||||||
final String uid = world.get();
|
|
||||||
final Optional<MapView> existingView = this.getMapView(mapIds.getInteger(uid));
|
|
||||||
if (existingView.isPresent()) {
|
|
||||||
final MapView view = existingView.get();
|
|
||||||
view.setLocked(true);
|
|
||||||
meta.setMapView(view);
|
|
||||||
map.setItemMeta(meta);
|
map.setItemMeta(meta);
|
||||||
getPlugin().debug(String.format("View exists (#%s); updated map (UID: %s)", view.getId(), uid));
|
getPlugin().debug(String.format("Map ID set to %s", newId));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Read the pixel data and generate a map view otherwise
|
// Read the pixel data and generate a map view otherwise
|
||||||
final MapData canvasData;
|
|
||||||
try {
|
|
||||||
getPlugin().debug("Deserializing map data from NBT and generating view...");
|
getPlugin().debug("Deserializing map data from NBT and generating view...");
|
||||||
canvasData = MapData.fromByteArray(Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY),
|
final @Nullable Map.Entry<MapData, Boolean> readMapData = readMapData(originServerName, originalMapId);
|
||||||
"Map pixel data is null"));
|
if (readMapData == null) {
|
||||||
} catch (Throwable e) {
|
getPlugin().debug("Read pixel data was not found in database, skipping...");
|
||||||
getPlugin().log(Level.WARNING, "Failed to deserialize map data from NBT", e);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a renderer to the map with the data and save to file
|
// Add a renderer to the map with the data and save to file
|
||||||
|
final MapData canvasData = Objects.requireNonNull(readMapData, "Pixel data null!").getKey();
|
||||||
final MapView view = generateRenderedMap(canvasData);
|
final MapView view = generateRenderedMap(canvasData);
|
||||||
final String worldUid = getDefaultMapWorld().getUID().toString();
|
|
||||||
meta.setMapView(view);
|
meta.setMapView(view);
|
||||||
map.setItemMeta(meta);
|
map.setItemMeta(meta);
|
||||||
saveMapToFile(canvasData, view.getId());
|
|
||||||
|
|
||||||
// Set the map view ID in NBT
|
// Bind in the database & Redis
|
||||||
NBT.modify(map, editable -> {
|
final int id = view.getId();
|
||||||
Objects.requireNonNull(editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY),
|
getRedisManager().bindMapIds(originServerName, originalMapId, currentServerName, id);
|
||||||
"Map view ID mappings compound is null")
|
getPlugin().getDatabase().setMapBinding(originServerName, originalMapId, currentServerName, id);
|
||||||
.setInteger(worldUid, view.getId());
|
|
||||||
});
|
getPlugin().debug(String.format("Bound map to view (#%s) on server %s", id, currentServerName));
|
||||||
getPlugin().debug(String.format("Generated view (#%s) and updated map (UID: %s)", view.getId(), worldUid));
|
|
||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
default void renderMapFromFile(@NotNull MapView view) {
|
default void renderPersistedMap(@NotNull MapView view) {
|
||||||
final File mapFile = new File(getMapCacheFolder(), view.getId() + ".dat");
|
if (getMapView(view.getId()).isPresent()) {
|
||||||
if (!mapFile.exists()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final MapData canvasData;
|
@Nullable final Map.Entry<MapData, Boolean> data = readMapData(getPlugin().getServerName(), view.getId());
|
||||||
try {
|
if (data == null) {
|
||||||
canvasData = MapData.fromNbt(mapFile);
|
final World world = view.getWorld() == null ? getDefaultMapWorld() : view.getWorld();
|
||||||
} catch (Throwable e) {
|
getPlugin().debug("Not rendering map: no data in DB for world %s, map #%s."
|
||||||
getPlugin().log(Level.WARNING, "Failed to deserialize map data from file", e);
|
.formatted(world.getName(), view.getId()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.getValue()) {
|
||||||
|
// from this server, doesn't need tweaking
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final MapData canvasData = data.getKey();
|
||||||
|
|
||||||
// Create a new map view renderer with the map data color at each pixel
|
// Create a new map view renderer with the map data color at each pixel
|
||||||
// use view.removeRenderer() to remove all this maps renderers
|
// use view.removeRenderer() to remove all this maps renderers
|
||||||
view.getRenderers().forEach(view::removeRenderer);
|
view.getRenderers().forEach(view::removeRenderer);
|
||||||
@@ -225,32 +323,6 @@ public interface BukkitMapPersister {
|
|||||||
setMapView(view);
|
setMapView(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
default void saveMapToFile(@NotNull MapData data, int id) {
|
|
||||||
getPlugin().runAsync(() -> {
|
|
||||||
final File mapFile = new File(getMapCacheFolder(), id + ".dat");
|
|
||||||
if (mapFile.exists()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final CompoundTag rootTag = new CompoundTag();
|
|
||||||
rootTag.put("data", data.toNBT().getTag());
|
|
||||||
NBTUtil.write(rootTag, mapFile);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
getPlugin().log(Level.WARNING, "Failed to serialize map data to file", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private File getMapCacheFolder() {
|
|
||||||
final File mapCache = new File(getPlugin().getDataFolder(), "maps");
|
|
||||||
if (!mapCache.exists() && !mapCache.mkdirs()) {
|
|
||||||
getPlugin().log(Level.WARNING, "Failed to create maps folder");
|
|
||||||
}
|
|
||||||
return mapCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sets the renderer of a map, and returns the generated MapView
|
// Sets the renderer of a map, and returns the generated MapView
|
||||||
@NotNull
|
@NotNull
|
||||||
private MapView generateRenderedMap(@NotNull MapData canvasData) {
|
private MapView generateRenderedMap(@NotNull MapData canvasData) {
|
||||||
@@ -289,6 +361,7 @@ public interface BukkitMapPersister {
|
|||||||
/**
|
/**
|
||||||
* A {@link MapRenderer} that can be used to render persistently serialized {@link MapData} to a {@link MapView}
|
* A {@link MapRenderer} that can be used to render persistently serialized {@link MapData} to a {@link MapView}
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
class PersistentMapRenderer extends MapRenderer {
|
class PersistentMapRenderer extends MapRenderer {
|
||||||
|
|
||||||
private final MapData canvasData;
|
private final MapData canvasData;
|
||||||
@@ -350,13 +423,18 @@ public interface BukkitMapPersister {
|
|||||||
/**
|
/**
|
||||||
* A {@link MapCanvas} implementation used for pre-rendering maps to be converted into {@link MapData}
|
* A {@link MapCanvas} implementation used for pre-rendering maps to be converted into {@link MapData}
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings({"deprecation", "removal"})
|
||||||
class PersistentMapCanvas implements MapCanvas {
|
class PersistentMapCanvas implements MapCanvas {
|
||||||
|
|
||||||
|
private static final String BANNER_PREFIX = "banner_";
|
||||||
|
|
||||||
|
private final int mapDataVersion;
|
||||||
private final MapView mapView;
|
private final MapView mapView;
|
||||||
private final int[][] pixels = new int[128][128];
|
private final int[][] pixels = new int[128][128];
|
||||||
private MapCursorCollection cursors;
|
private MapCursorCollection cursors;
|
||||||
|
|
||||||
private PersistentMapCanvas(@NotNull MapView mapView) {
|
private PersistentMapCanvas(@NotNull MapView mapView, int mapDataVersion) {
|
||||||
|
this.mapDataVersion = mapDataVersion;
|
||||||
this.mapView = mapView;
|
this.mapView = mapView;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,18 +456,38 @@ public interface BukkitMapPersister {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Deprecated
|
||||||
public void setPixel(int x, int y, byte color) {
|
public void setPixel(int x, int y, byte color) {
|
||||||
pixels[x][y] = color;
|
pixels[x][y] = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Deprecated
|
||||||
public byte getPixel(int x, int y) {
|
public byte getPixel(int x, int y) {
|
||||||
return (byte) pixels[x][y];
|
return (byte) pixels[x][y];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Deprecated
|
||||||
public byte getBasePixel(int x, int y) {
|
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
|
@Override
|
||||||
@@ -419,10 +517,13 @@ public interface BukkitMapPersister {
|
|||||||
@NotNull
|
@NotNull
|
||||||
private MapData extractMapData() {
|
private MapData extractMapData() {
|
||||||
final List<MapBanner> banners = Lists.newArrayList();
|
final List<MapBanner> banners = Lists.newArrayList();
|
||||||
final String BANNER_PREFIX = "banner_";
|
|
||||||
for (int i = 0; i < getCursors().size(); i++) {
|
for (int i = 0; i < getCursors().size(); i++) {
|
||||||
final MapCursor cursor = getCursors().getCursor(i);
|
final MapCursor cursor = getCursors().getCursor(i);
|
||||||
final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH);
|
//#if MC==12001
|
||||||
|
//$$ final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH);
|
||||||
|
//#else
|
||||||
|
final String type = cursor.getType().getKey().getKey();
|
||||||
|
//#endif
|
||||||
if (type.startsWith(BANNER_PREFIX)) {
|
if (type.startsWith(BANNER_PREFIX)) {
|
||||||
banners.add(new MapBanner(
|
banners.add(new MapBanner(
|
||||||
type.replaceAll(BANNER_PREFIX, ""),
|
type.replaceAll(BANNER_PREFIX, ""),
|
||||||
@@ -432,14 +533,18 @@ public interface BukkitMapPersister {
|
|||||||
cursor.getY()
|
cursor.getY()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return MapData.fromPixels(pixels, getDimension(), (byte) 2, banners, List.of());
|
return MapData.fromPixels(mapDataVersion, pixels, getDimension(), (byte) 2, banners, List.of());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
Map<Integer, MapView> getMapViews();
|
Map<Integer, MapView> getMapViews();
|
||||||
|
|
||||||
|
@ApiStatus.Internal
|
||||||
|
RedisManager getRedisManager();
|
||||||
|
|
||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
@NotNull
|
@NotNull
|
||||||
BukkitHuskSync getPlugin();
|
BukkitHuskSync getPlugin();
|
||||||
@@ -146,7 +146,7 @@ public class LegacyMigrator extends Migrator {
|
|||||||
try {
|
try {
|
||||||
plugin.getDatabase().addSnapshot(data.user(), convertedData);
|
plugin.getDatabase().addSnapshot(data.user(), convertedData);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
plugin.log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().getUsername() + ": " + e.getMessage());
|
plugin.log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().getName() + ": " + e.getMessage());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +330,7 @@ public class LegacyMigrator extends Migrator {
|
|||||||
))
|
))
|
||||||
|
|
||||||
// Health, hunger, experience & game mode
|
// 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))
|
.hunger(BukkitData.Hunger.from(hunger, saturation, saturationExhaustion))
|
||||||
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
|
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
|
||||||
.gameMode(BukkitData.GameMode.from(gameMode))
|
.gameMode(BukkitData.GameMode.from(gameMode))
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ public class MpdbMigrator extends Migrator {
|
|||||||
If any of these are not correct, please correct them
|
If any of these are not correct, please correct them
|
||||||
using the command:
|
using the command:
|
||||||
"husksync migrate mpdb set <parameter> <value>"
|
"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
|
STEP 3] HuskSync will migrate data into the database
|
||||||
tables configures in the config.yml file of this
|
tables configures in the config.yml file of this
|
||||||
@@ -263,7 +263,7 @@ public class MpdbMigrator extends Migrator {
|
|||||||
before proceeding.
|
before proceeding.
|
||||||
|
|
||||||
STEP 4] To start the migration, please run:
|
STEP 4] To start the migration, please run:
|
||||||
"husksync migrate mpdb start"
|
"husksync migrate start mpdb"
|
||||||
|
|
||||||
NOTE: This migrator currently WORKS WITH MPDB version
|
NOTE: This migrator currently WORKS WITH MPDB version
|
||||||
v4.9.2 and below!
|
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.builder.gui.StorageBuilder;
|
||||||
import dev.triumphteam.gui.guis.Gui;
|
import dev.triumphteam.gui.guis.Gui;
|
||||||
import dev.triumphteam.gui.guis.StorageGui;
|
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.HuskSync;
|
||||||
import net.william278.husksync.data.BukkitData;
|
import net.william278.husksync.data.BukkitData;
|
||||||
import net.william278.husksync.data.BukkitUserDataHolder;
|
import net.william278.husksync.data.BukkitUserDataHolder;
|
||||||
import net.william278.husksync.data.Data;
|
import net.william278.husksync.data.Data;
|
||||||
import org.bukkit.Material;
|
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
@@ -40,8 +36,6 @@ import java.util.Arrays;
|
|||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import static net.william278.husksync.util.BukkitKeyedAdapter.matchMaterial;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bukkit platform implementation of an {@link OnlineUser}
|
* Bukkit platform implementation of an {@link OnlineUser}
|
||||||
*/
|
*/
|
||||||
@@ -62,37 +56,18 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
|||||||
return new BukkitUser(player, plugin);
|
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
|
@Override
|
||||||
public boolean isOffline() {
|
public boolean isOffline() {
|
||||||
return player == null || !player.isOnline();
|
return player == null || !player.isOnline();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Deprecated(since = "3.6.7")
|
||||||
public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
|
public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
|
||||||
@NotNull String iconMaterial, @NotNull String backgroundType) {
|
@NotNull String iconMaterial, @NotNull String backgroundType) {
|
||||||
try {
|
plugin.log(Level.WARNING, "Toast notifications are deprecated. " +
|
||||||
final Material material = matchMaterial(iconMaterial);
|
"Please change your notification display slot to CHAT, ACTION_BAR or NONE.");
|
||||||
Toast.builder((BukkitHuskSync) plugin)
|
this.sendActionBar(title);
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -103,7 +78,7 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
|||||||
if (!editable) {
|
if (!editable) {
|
||||||
builder.disableAllInteractions();
|
builder.disableAllInteractions();
|
||||||
}
|
}
|
||||||
final StorageGui gui = builder.enableOtherActions()
|
final StorageGui gui = builder
|
||||||
.apply(a -> a.getInventory().setContents(contents))
|
.apply(a -> a.getInventory().setContents(contents))
|
||||||
.title(title.toComponent()).create();
|
.title(title.toComponent()).create();
|
||||||
gui.setCloseGuiAction((close) -> onClose.accept(BukkitData.Items.ItemArray.adapt(
|
gui.setCloseGuiAction((close) -> onClose.accept(BukkitData.Items.ItemArray.adapt(
|
||||||
@@ -132,9 +107,14 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
|||||||
return player.hasMetadata("NPC");
|
return player.hasMetadata("NPC");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Bukkit {@link Player} instance of this user
|
||||||
|
*
|
||||||
|
* @return the {@link Player} instance
|
||||||
|
* @since 3.6
|
||||||
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
public Player getPlayer() {
|
||||||
public Player getBukkitPlayer() {
|
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package net.william278.husksync.util;
|
|||||||
import org.bukkit.*;
|
import org.bukkit.*;
|
||||||
import org.bukkit.attribute.Attribute;
|
import org.bukkit.attribute.Attribute;
|
||||||
import org.bukkit.entity.EntityType;
|
import org.bukkit.entity.EntityType;
|
||||||
|
import org.bukkit.potion.PotionEffectType;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
@@ -48,6 +49,15 @@ public final class BukkitKeyedAdapter {
|
|||||||
return getRegistryValue(Registry.ATTRIBUTE, key);
|
return getRegistryValue(Registry.ATTRIBUTE, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static PotionEffectType matchEffectType(@NotNull String key) {
|
||||||
|
//#if MC==12001
|
||||||
|
//$$ return PotionEffectType.getByName(key);
|
||||||
|
//#else
|
||||||
|
return getRegistryValue(Registry.EFFECT, key);
|
||||||
|
//#endif
|
||||||
|
}
|
||||||
|
|
||||||
private static <T extends Keyed> T getRegistryValue(@NotNull Registry<T> registry, @NotNull String keyString) {
|
private static <T extends Keyed> T getRegistryValue(@NotNull Registry<T> registry, @NotNull String keyString) {
|
||||||
final NamespacedKey key = NamespacedKey.fromString(keyString);
|
final NamespacedKey key = NamespacedKey.fromString(keyString);
|
||||||
return key != null ? registry.get(key) : null;
|
return key != null ? registry.get(key) : null;
|
||||||
|
|||||||
@@ -82,32 +82,33 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
|||||||
|
|
||||||
final JSONObject status = object.getJSONObject("status_data");
|
final JSONObject status = object.getJSONObject("status_data");
|
||||||
final HashMap<Identifier, Data> containers = Maps.newHashMap();
|
final HashMap<Identifier, Data> containers = Maps.newHashMap();
|
||||||
if (shouldImport(Identifier.HEALTH)) {
|
if (Identifier.HEALTH.isEnabled()) {
|
||||||
containers.put(Identifier.HEALTH, BukkitData.Health.from(
|
containers.put(Identifier.HEALTH, BukkitData.Health.from(
|
||||||
status.getDouble("health"),
|
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(
|
containers.put(Identifier.HUNGER, BukkitData.Hunger.from(
|
||||||
status.getInt("hunger"),
|
status.getInt("hunger"),
|
||||||
status.getFloat("saturation"),
|
status.getFloat("saturation"),
|
||||||
status.getFloat("saturation_exhaustion")
|
status.getFloat("saturation_exhaustion")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if (shouldImport(Identifier.EXPERIENCE)) {
|
if (Identifier.EXPERIENCE.isEnabled()) {
|
||||||
containers.put(Identifier.EXPERIENCE, BukkitData.Experience.from(
|
containers.put(Identifier.EXPERIENCE, BukkitData.Experience.from(
|
||||||
status.getInt("total_experience"),
|
status.getInt("total_experience"),
|
||||||
status.getInt("experience_level"),
|
status.getInt("experience_level"),
|
||||||
status.getFloat("experience_progress")
|
status.getFloat("experience_progress")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if (shouldImport(Identifier.GAME_MODE)) {
|
if (Identifier.GAME_MODE.isEnabled()) {
|
||||||
containers.put(Identifier.GAME_MODE, BukkitData.GameMode.from(
|
containers.put(Identifier.GAME_MODE, BukkitData.GameMode.from(
|
||||||
status.getString("game_mode")
|
status.getString("game_mode")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if (shouldImport(Identifier.FLIGHT_STATUS)) {
|
if (Identifier.FLIGHT_STATUS.isEnabled()) {
|
||||||
containers.put(Identifier.FLIGHT_STATUS, BukkitData.FlightStatus.from(
|
containers.put(Identifier.FLIGHT_STATUS, BukkitData.FlightStatus.from(
|
||||||
status.getBoolean("is_flying"),
|
status.getBoolean("is_flying"),
|
||||||
status.getBoolean("is_flying")
|
status.getBoolean("is_flying")
|
||||||
@@ -118,7 +119,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private Optional<Data.Items.Inventory> readInventory(@NotNull JSONObject object) {
|
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();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +131,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private Optional<Data.Items.EnderChest> readEnderChest(@NotNull JSONObject object) {
|
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();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +143,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private Optional<Data.Location> readLocation(@NotNull JSONObject object) {
|
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();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +164,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private Optional<Data.Advancements> readAdvancements(@NotNull JSONObject object) {
|
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();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +187,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private Optional<Data.Statistics> readStatistics(@NotNull JSONObject object) {
|
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();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,11 +281,6 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
|||||||
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
|
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private boolean shouldImport(@NotNull Identifier type) {
|
|
||||||
return plugin.getSettings().getSynchronization().isFeatureEnabled(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private Date parseDate(@NotNull String dateString) {
|
private Date parseDate(@NotNull String dateString) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2
bukkit/src/main/resources/compatibility.yml
Normal file
2
bukkit/src/main/resources/compatibility.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# File used for checking Minecraft server compatibility with this version of HuskSync
|
||||||
|
minecraft_version: '${minecraft_version}'
|
||||||
@@ -5,7 +5,7 @@ website: 'https://william278.net/'
|
|||||||
main: 'net.william278.husksync.PaperHuskSync'
|
main: 'net.william278.husksync.PaperHuskSync'
|
||||||
loader: 'net.william278.husksync.PaperHuskSyncLoader'
|
loader: 'net.william278.husksync.PaperHuskSyncLoader'
|
||||||
version: '${version}'
|
version: '${version}'
|
||||||
api-version: '1.19'
|
api-version: '${minecraft_api_version}'
|
||||||
folia-supported: true
|
folia-supported: true
|
||||||
dependencies:
|
dependencies:
|
||||||
server:
|
server:
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
name: 'HuskSync'
|
name: 'HuskSync'
|
||||||
version: '${version}'
|
version: '${version}'
|
||||||
main: 'net.william278.husksync.BukkitHuskSync'
|
main: 'net.william278.husksync.BukkitHuskSync'
|
||||||
api-version: 1.17
|
api-version: '${minecraft_api_version}'
|
||||||
author: 'William278'
|
author: 'William278'
|
||||||
description: '${description}'
|
description: '${description}'
|
||||||
website: 'https://william278.net'
|
website: 'https://william278.net'
|
||||||
|
|||||||
@@ -3,24 +3,30 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'commons-io:commons-io:2.16.1'
|
api 'commons-io:commons-io:2.18.0'
|
||||||
api 'org.apache.commons:commons-text:1.12.0'
|
api 'org.apache.commons:commons-text:1.13.0'
|
||||||
api 'net.william278:minedown:1.8.2'
|
api 'net.william278:minedown:1.8.2'
|
||||||
api 'org.json:json:20240303'
|
api 'net.william278:mapdataapi:2.0'
|
||||||
api 'com.google.code.gson:gson:2.11.0'
|
api 'org.json:json:20250107'
|
||||||
|
api 'com.google.code.gson:gson:2.12.1'
|
||||||
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
|
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
|
||||||
api 'de.exlll:configlib-yaml:4.5.0'
|
api 'de.exlll:configlib-yaml:4.5.0'
|
||||||
api 'net.william278:paginedown:1.1.2'
|
api 'net.william278:paginedown:1.1.2'
|
||||||
api 'net.william278:DesertWell:2.0.4'
|
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'
|
exclude module: 'slf4j-api'
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
compileOnlyApi 'net.william278.toilet:toilet-common:1.0.12'
|
||||||
compileOnly 'org.jetbrains:annotations:24.1.0'
|
|
||||||
compileOnly 'net.kyori:adventure-api:4.17.0'
|
compileOnly 'net.william278.uniform:uniform-common:1.3.1'
|
||||||
compileOnly 'net.kyori:adventure-platform-api:4.3.2'
|
compileOnly 'com.mojang:brigadier:1.1.8'
|
||||||
compileOnly 'com.google.guava:guava:33.2.0-jre'
|
compileOnly 'org.projectlombok:lombok:1.18.36'
|
||||||
|
compileOnly 'org.jetbrains:annotations:26.0.2'
|
||||||
|
compileOnly 'net.kyori:adventure-api:4.19.0'
|
||||||
|
compileOnly 'net.kyori:adventure-platform-api:4.3.4'
|
||||||
|
compileOnly "net.kyori:adventure-text-serializer-plain:4.19.0"
|
||||||
|
compileOnly 'com.google.guava:guava:33.4.0-jre'
|
||||||
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
|
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||||
compileOnly "redis.clients:jedis:$jedis_version"
|
compileOnly "redis.clients:jedis:$jedis_version"
|
||||||
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
|
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
|
||||||
@@ -31,10 +37,10 @@ dependencies {
|
|||||||
|
|
||||||
testImplementation "redis.clients:jedis:$jedis_version"
|
testImplementation "redis.clients:jedis:$jedis_version"
|
||||||
testImplementation "org.xerial.snappy:snappy-java:$snappy_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'
|
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||||
testCompileOnly 'de.exlll:configlib-yaml:4.5.0'
|
testCompileOnly 'de.exlll:configlib-yaml:4.5.0'
|
||||||
testCompileOnly 'org.jetbrains:annotations:24.1.0'
|
testCompileOnly 'org.jetbrains:annotations:26.0.2'
|
||||||
|
|
||||||
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
annotationProcessor 'org.projectlombok:lombok:1.18.36'
|
||||||
}
|
}
|
||||||
@@ -31,19 +31,22 @@ import net.william278.husksync.adapter.DataAdapter;
|
|||||||
import net.william278.husksync.config.ConfigProvider;
|
import net.william278.husksync.config.ConfigProvider;
|
||||||
import net.william278.husksync.data.Data;
|
import net.william278.husksync.data.Data;
|
||||||
import net.william278.husksync.data.Identifier;
|
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.database.Database;
|
||||||
import net.william278.husksync.event.EventDispatcher;
|
import net.william278.husksync.event.EventDispatcher;
|
||||||
|
import net.william278.husksync.listener.LockedHandler;
|
||||||
import net.william278.husksync.migrator.Migrator;
|
import net.william278.husksync.migrator.Migrator;
|
||||||
import net.william278.husksync.redis.RedisManager;
|
import net.william278.husksync.redis.RedisManager;
|
||||||
import net.william278.husksync.sync.DataSyncer;
|
import net.william278.husksync.sync.DataSyncer;
|
||||||
import net.william278.husksync.user.ConsoleUser;
|
import net.william278.husksync.user.ConsoleUser;
|
||||||
import net.william278.husksync.user.OnlineUser;
|
import net.william278.husksync.user.OnlineUser;
|
||||||
|
import net.william278.husksync.util.CompatibilityChecker;
|
||||||
|
import net.william278.husksync.util.DumpProvider;
|
||||||
import net.william278.husksync.util.LegacyConverter;
|
import net.william278.husksync.util.LegacyConverter;
|
||||||
import net.william278.husksync.util.Task;
|
import net.william278.husksync.util.Task;
|
||||||
|
import net.william278.uniform.Uniform;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -52,7 +55,8 @@ import java.util.logging.Level;
|
|||||||
/**
|
/**
|
||||||
* Abstract implementation of the HuskSync plugin.
|
* Abstract implementation of the HuskSync plugin.
|
||||||
*/
|
*/
|
||||||
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider {
|
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider, SerializerRegistry,
|
||||||
|
CompatibilityChecker, DumpProvider {
|
||||||
|
|
||||||
int SPIGOT_RESOURCE_ID = 97144;
|
int SPIGOT_RESOURCE_ID = 97144;
|
||||||
|
|
||||||
@@ -86,7 +90,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
|||||||
*
|
*
|
||||||
* @return the {@link RedisManager} implementation
|
* @return the {@link RedisManager} implementation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
RedisManager getRedisManager();
|
RedisManager getRedisManager();
|
||||||
|
|
||||||
@@ -98,43 +101,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
|||||||
@NotNull
|
@NotNull
|
||||||
DataAdapter getDataAdapter();
|
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
|
* Returns the data syncer implementation
|
||||||
*
|
*
|
||||||
@@ -150,6 +116,14 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
|||||||
*/
|
*/
|
||||||
void setDataSyncer(@NotNull DataSyncer dataSyncer);
|
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
|
* Returns a list of available data {@link Migrator}s
|
||||||
*
|
*
|
||||||
@@ -159,7 +133,17 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
|||||||
List<Migrator> getAvailableMigrators();
|
List<Migrator> getAvailableMigrators();
|
||||||
|
|
||||||
@NotNull
|
@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.
|
* Initialize a faucet of the plugin.
|
||||||
@@ -193,14 +177,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
|||||||
*/
|
*/
|
||||||
InputStream getResource(@NotNull String name);
|
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
|
* Log a message to the console
|
||||||
*
|
*
|
||||||
@@ -275,6 +251,14 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
|||||||
@NotNull
|
@NotNull
|
||||||
Version getMinecraftVersion();
|
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
|
* Returns the platform type
|
||||||
*
|
*
|
||||||
@@ -283,6 +267,14 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
|||||||
@NotNull
|
@NotNull
|
||||||
String getPlatformType();
|
String getPlatformType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the server software version
|
||||||
|
*
|
||||||
|
* @return the server software version string
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
String getServerVersion();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the legacy data converter if it exists
|
* Returns the legacy data converter if it exists
|
||||||
*
|
*
|
||||||
@@ -312,6 +304,9 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
LockedHandler getLockedHandler();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the set of UUIDs of "locked players", for which events will be canceled.
|
* Get the set of UUIDs of "locked players", for which events will be canceled.
|
||||||
* </p>
|
* </p>
|
||||||
@@ -358,7 +353,11 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
|||||||
|
|
||||||
Caused by: %s""";
|
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);
|
super(String.format(FORMAT, message), cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public class GsonAdapter implements DataAdapter {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@NotNull
|
@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);
|
return this.fromJson(new String(data, StandardCharsets.UTF_8), type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ public class SnappyGsonAdapter extends GsonAdapter {
|
|||||||
super(plugin);
|
super(plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
@Override
|
||||||
public <A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException {
|
public <A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException {
|
||||||
try {
|
try {
|
||||||
@@ -43,7 +42,7 @@ public class SnappyGsonAdapter extends GsonAdapter {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@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 {
|
try {
|
||||||
return super.fromBytes(decompressBytes(data), type);
|
return super.fromBytes(decompressBytes(data), type);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|||||||
@@ -99,6 +99,18 @@ public class HuskSyncAPI {
|
|||||||
return plugin.supplyAsync(() -> plugin.getDatabase().getUser(uuid));
|
return plugin.supplyAsync(() -> plugin.getDatabase().getUser(uuid));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an {@link OnlineUser} by their UUID
|
||||||
|
*
|
||||||
|
* @param uuid the UUID of the user to get
|
||||||
|
* @return The {@link OnlineUser} wrapped in an optional, if they are online on <i>this</i> server.
|
||||||
|
* @since 3.7.2
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
|
||||||
|
return plugin.getOnlineUser(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a {@link User} by their username
|
* Get a {@link User} by their username
|
||||||
*
|
*
|
||||||
@@ -137,7 +149,7 @@ public class HuskSyncAPI {
|
|||||||
*/
|
*/
|
||||||
public CompletableFuture<Optional<DataSnapshot.Unpacked>> getCurrentData(@NotNull User user) {
|
public CompletableFuture<Optional<DataSnapshot.Unpacked>> getCurrentData(@NotNull User user) {
|
||||||
return plugin.getRedisManager()
|
return plugin.getRedisManager()
|
||||||
.getUserData(UUID.randomUUID(), user)
|
.getOnlineUserData(UUID.randomUUID(), user, DataSnapshot.SaveCause.API)
|
||||||
.thenApply(data -> data.or(() -> plugin.getDatabase().getLatestSnapshot(user)))
|
.thenApply(data -> data.or(() -> plugin.getDatabase().getLatestSnapshot(user)))
|
||||||
.thenApply(data -> data.map(snapshot -> snapshot.unpack(plugin)));
|
.thenApply(data -> data.map(snapshot -> snapshot.unpack(plugin)));
|
||||||
}
|
}
|
||||||
@@ -378,6 +390,17 @@ public class HuskSyncAPI {
|
|||||||
plugin.registerSerializer(identifier, serializer);
|
plugin.registerSerializer(identifier, serializer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a registered data serializer by its identifier
|
||||||
|
*
|
||||||
|
* @param identifier The identifier of the data type to get the serializer for
|
||||||
|
* @return The serializer for the given identifier, or an empty optional if the serializer isn't registered
|
||||||
|
* @since 3.5.4
|
||||||
|
*/
|
||||||
|
public Optional<Serializer<Data>> getDataSerializer(@NotNull Identifier identifier) {
|
||||||
|
return plugin.getSerializer(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a {@link DataSnapshot.Unpacked} from a {@link DataSnapshot.Packed}
|
* Get a {@link DataSnapshot.Unpacked} from a {@link DataSnapshot.Packed}
|
||||||
*
|
*
|
||||||
@@ -500,17 +523,19 @@ public class HuskSyncAPI {
|
|||||||
*/
|
*/
|
||||||
static final class NotRegisteredException extends IllegalStateException {
|
static final class NotRegisteredException extends IllegalStateException {
|
||||||
|
|
||||||
private static final String MESSAGE = """
|
private static final String REASONS = """
|
||||||
Could not access the HuskSync API as it has not yet been registered. This could be because:
|
This may be because:
|
||||||
1) HuskSync has failed to enable successfully
|
1) HuskSync has failed to enable successfully
|
||||||
2) Your plugin isn't set to load after HuskSync has
|
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?)
|
(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.
|
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.""";
|
NotRegisteredException(@NotNull String reasons) {
|
||||||
|
super("Could not access the HuskSync API as it has not yet been registered. %s".formatted(reasons));
|
||||||
|
}
|
||||||
|
|
||||||
NotRegisteredException() {
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,6 @@ import de.themoep.minedown.adventure.MineDown;
|
|||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
import net.william278.husksync.data.Data;
|
import net.william278.husksync.data.Data;
|
||||||
import net.william278.husksync.data.DataSnapshot;
|
import net.william278.husksync.data.DataSnapshot;
|
||||||
import net.william278.husksync.redis.RedisKeyType;
|
|
||||||
import net.william278.husksync.redis.RedisManager;
|
import net.william278.husksync.redis.RedisManager;
|
||||||
import net.william278.husksync.user.OnlineUser;
|
import net.william278.husksync.user.OnlineUser;
|
||||||
import net.william278.husksync.user.User;
|
import net.william278.husksync.user.User;
|
||||||
@@ -37,7 +36,7 @@ import java.util.Optional;
|
|||||||
public class EnderChestCommand extends ItemsCommand {
|
public class EnderChestCommand extends ItemsCommand {
|
||||||
|
|
||||||
public EnderChestCommand(@NotNull HuskSync plugin) {
|
public EnderChestCommand(@NotNull HuskSync plugin) {
|
||||||
super(plugin, List.of("enderchest", "echest", "openechest"));
|
super("enderchest", List.of("echest", "openechest"), DataSnapshot.SaveCause.ENDERCHEST_COMMAND, plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -51,7 +50,7 @@ public class EnderChestCommand extends ItemsCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Display opening message
|
// Display opening message
|
||||||
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(),
|
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getName(),
|
||||||
snapshot.getTimestamp().format(DateTimeFormatter
|
snapshot.getTimestamp().format(DateTimeFormatter
|
||||||
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
|
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
|
||||||
.ifPresent(viewer::sendMessage);
|
.ifPresent(viewer::sendMessage);
|
||||||
@@ -60,8 +59,8 @@ public class EnderChestCommand extends ItemsCommand {
|
|||||||
final Data.Items.EnderChest enderChest = optionalEnderChest.get();
|
final Data.Items.EnderChest enderChest = optionalEnderChest.get();
|
||||||
viewer.showGui(
|
viewer.showGui(
|
||||||
enderChest,
|
enderChest,
|
||||||
plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getUsername())
|
plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getName())
|
||||||
.orElse(new MineDown(String.format("%s's Ender Chest", user.getUsername()))),
|
.orElse(new MineDown(String.format("%s's Ender Chest", user.getName()))),
|
||||||
allowEdit,
|
allowEdit,
|
||||||
enderChest.getSlotCount(),
|
enderChest.getSlotCount(),
|
||||||
(itemsOnClose) -> {
|
(itemsOnClose) -> {
|
||||||
@@ -84,18 +83,17 @@ public class EnderChestCommand extends ItemsCommand {
|
|||||||
|
|
||||||
// Create and pack the snapshot with the updated enderChest
|
// Create and pack the snapshot with the updated enderChest
|
||||||
final DataSnapshot.Packed snapshot = latestData.get().copy();
|
final DataSnapshot.Packed snapshot = latestData.get().copy();
|
||||||
|
boolean pin = plugin.getSettings().getSynchronization().doAutoPin(saveCause);
|
||||||
snapshot.edit(plugin, (data) -> {
|
snapshot.edit(plugin, (data) -> {
|
||||||
data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items));
|
data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items));
|
||||||
data.setSaveCause(DataSnapshot.SaveCause.ENDERCHEST_COMMAND);
|
data.setSaveCause(saveCause);
|
||||||
data.setPinned(
|
data.setPinned(pin);
|
||||||
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save data
|
// Save data
|
||||||
final RedisManager redis = plugin.getRedisManager();
|
final RedisManager redis = plugin.getRedisManager();
|
||||||
plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
|
plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
|
||||||
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
|
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot));
|
||||||
redis.sendUserDataUpdate(user, data);
|
redis.sendUserDataUpdate(user, data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,45 +19,44 @@
|
|||||||
|
|
||||||
package net.william278.husksync.command;
|
package net.william278.husksync.command;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.context.CommandContext;
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
import de.themoep.minedown.adventure.MineDown;
|
import de.themoep.minedown.adventure.MineDown;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.JoinConfiguration;
|
import net.kyori.adventure.text.JoinConfiguration;
|
||||||
import net.kyori.adventure.text.event.HoverEvent;
|
import net.kyori.adventure.text.event.ClickEvent;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
import net.kyori.adventure.text.format.TextColor;
|
import net.kyori.adventure.text.format.TextColor;
|
||||||
|
import net.kyori.adventure.text.format.TextDecoration;
|
||||||
import net.william278.desertwell.about.AboutMenu;
|
import net.william278.desertwell.about.AboutMenu;
|
||||||
import net.william278.desertwell.util.UpdateChecker;
|
import net.william278.desertwell.util.UpdateChecker;
|
||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
|
import net.william278.husksync.data.DataSnapshot;
|
||||||
import net.william278.husksync.database.Database;
|
import net.william278.husksync.database.Database;
|
||||||
import net.william278.husksync.migrator.Migrator;
|
import net.william278.husksync.migrator.Migrator;
|
||||||
import net.william278.husksync.user.CommandUser;
|
import net.william278.husksync.user.CommandUser;
|
||||||
import net.william278.husksync.user.OnlineUser;
|
import net.william278.husksync.util.LegacyConverter;
|
||||||
import org.apache.commons.text.WordUtils;
|
import net.william278.husksync.util.StatusLine;
|
||||||
|
import net.william278.uniform.BaseCommand;
|
||||||
|
import net.william278.uniform.CommandProvider;
|
||||||
|
import net.william278.uniform.Permission;
|
||||||
|
import net.william278.uniform.element.ArgumentElement;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.*;
|
import java.time.OffsetDateTime;
|
||||||
import java.util.function.Function;
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class HuskSyncCommand extends Command implements TabProvider {
|
public class HuskSyncCommand extends PluginCommand {
|
||||||
|
|
||||||
private static final Map<String, Boolean> SUB_COMMANDS = Map.of(
|
|
||||||
"about", false,
|
|
||||||
"status", true,
|
|
||||||
"reload", true,
|
|
||||||
"migrate", true,
|
|
||||||
"update", true
|
|
||||||
);
|
|
||||||
|
|
||||||
private final UpdateChecker updateChecker;
|
private final UpdateChecker updateChecker;
|
||||||
private final AboutMenu aboutMenu;
|
private final AboutMenu aboutMenu;
|
||||||
|
|
||||||
public HuskSyncCommand(@NotNull HuskSync plugin) {
|
public HuskSyncCommand(@NotNull HuskSync plugin) {
|
||||||
super("husksync", List.of(), "[" + String.join("|", SUB_COMMANDS.keySet()) + "]", plugin);
|
super("husksync", List.of(), Permission.Default.TRUE, ExecutionScope.ALL, plugin);
|
||||||
addAdditionalPermissions(SUB_COMMANDS);
|
|
||||||
|
|
||||||
this.updateChecker = plugin.getUpdateChecker();
|
this.updateChecker = plugin.getUpdateChecker();
|
||||||
this.aboutMenu = AboutMenu.builder()
|
this.aboutMenu = AboutMenu.builder()
|
||||||
.title(Component.text("HuskSync"))
|
.title(Component.text("HuskSync"))
|
||||||
@@ -68,7 +67,10 @@ public class HuskSyncCommand extends Command implements TabProvider {
|
|||||||
.credits("Contributors",
|
.credits("Contributors",
|
||||||
AboutMenu.Credit.of("HarvelsX").description("Code"),
|
AboutMenu.Credit.of("HarvelsX").description("Code"),
|
||||||
AboutMenu.Credit.of("HookWoods").description("Code"),
|
AboutMenu.Credit.of("HookWoods").description("Code"),
|
||||||
AboutMenu.Credit.of("Preva1l").description("Code"))
|
AboutMenu.Credit.of("Preva1l").description("Code"),
|
||||||
|
AboutMenu.Credit.of("hanbings").description("Code (Fabric porting)"),
|
||||||
|
AboutMenu.Credit.of("Stampede2011").description("Code (Fabric mixins)"),
|
||||||
|
AboutMenu.Credit.of("VinerDream").description("Code"))
|
||||||
.credits("Translators",
|
.credits("Translators",
|
||||||
AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"),
|
AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"),
|
||||||
AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"),
|
AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"),
|
||||||
@@ -93,189 +95,165 @@ public class HuskSyncCommand extends Command implements TabProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
|
public void provide(@NotNull BaseCommand<?> command) {
|
||||||
final String subCommand = parseStringArg(args, 0).orElse("about").toLowerCase(Locale.ENGLISH);
|
command.setDefaultExecutor((ctx) -> about(command, ctx));
|
||||||
if (SUB_COMMANDS.containsKey(subCommand) && !executor.hasPermission(getPermission(subCommand))) {
|
command.addSubCommand("about", (sub) -> sub.setDefaultExecutor((ctx) -> about(command, ctx)));
|
||||||
plugin.getLocales().getLocale("error_no_permission")
|
command.addSubCommand("status", needsOp("status"), status());
|
||||||
.ifPresent(executor::sendMessage);
|
command.addSubCommand("dump", needsOp("dump"), dump());
|
||||||
return;
|
command.addSubCommand("reload", needsOp("reload"), reload());
|
||||||
|
command.addSubCommand("update", needsOp("update"), update());
|
||||||
|
command.addSubCommand("forceupgrade", forceUpgrade());
|
||||||
|
command.addSubCommand("migrate", migrate());
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (subCommand) {
|
private void about(@NotNull BaseCommand<?> c, @NotNull CommandContext<?> ctx) {
|
||||||
case "about" -> executor.sendMessage(aboutMenu.toComponent());
|
user(c, ctx).getAudience().sendMessage(aboutMenu.toComponent());
|
||||||
case "status" -> {
|
}
|
||||||
getPlugin().getLocales().getLocale("system_status_header").ifPresent(executor::sendMessage);
|
|
||||||
executor.sendMessage(Component.join(
|
@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(),
|
JoinConfiguration.newlines(),
|
||||||
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList()
|
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList()
|
||||||
));
|
));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
case "reload" -> {
|
|
||||||
|
@NotNull
|
||||||
|
private CommandProvider dump() {
|
||||||
|
return (sub) -> {
|
||||||
|
sub.setDefaultExecutor((ctx) -> {
|
||||||
|
final CommandUser user = user(sub, ctx);
|
||||||
|
plugin.getLocales().getLocale("system_dump_confirm").ifPresent(user::sendMessage);
|
||||||
|
});
|
||||||
|
sub.addSubCommand("confirm", (con) -> con.setDefaultExecutor((ctx) -> {
|
||||||
|
final CommandUser user = user(sub, ctx);
|
||||||
|
plugin.getLocales().getLocale("system_dump_started").ifPresent(user::sendMessage);
|
||||||
|
plugin.runAsync(() -> {
|
||||||
|
final String url = plugin.createDump(user);
|
||||||
|
plugin.getLocales().getLocale("system_dump_ready").ifPresent(user::sendMessage);
|
||||||
|
user.sendMessage(Component.text(url).clickEvent(ClickEvent.openUrl(url))
|
||||||
|
.decorate(TextDecoration.UNDERLINED).color(NamedTextColor.GRAY));
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private CommandProvider reload() {
|
||||||
|
return (sub) -> sub.setDefaultExecutor((ctx) -> {
|
||||||
|
final CommandUser user = user(sub, ctx);
|
||||||
try {
|
try {
|
||||||
plugin.loadSettings();
|
plugin.loadSettings();
|
||||||
plugin.loadLocales();
|
plugin.loadLocales();
|
||||||
plugin.loadServer();
|
plugin.loadServer();
|
||||||
plugin.getLocales().getLocale("reload_complete").ifPresent(executor::sendMessage);
|
plugin.getLocales().getLocale("reload_complete").ifPresent(user::sendMessage);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
executor.sendMessage(new MineDown(
|
user.sendMessage(new MineDown(
|
||||||
"[Error:](#ff3300) [Failed to reload the plugin. Check console for errors.](#ff7e5e)"
|
"[Error:](#ff3300) [Failed to reload the plugin. Check console for errors.](#ff7e5e)"
|
||||||
));
|
));
|
||||||
plugin.log(Level.SEVERE, "Failed to reload the plugin", e);
|
plugin.log(Level.SEVERE, "Failed to reload the plugin", e);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
case "migrate" -> {
|
|
||||||
if (executor instanceof OnlineUser) {
|
@NotNull
|
||||||
plugin.getLocales().getLocale("error_console_command_only")
|
private CommandProvider update() {
|
||||||
.ifPresent(executor::sendMessage);
|
return (sub) -> sub.setDefaultExecutor((ctx) -> updateChecker.check().thenAccept(checked -> {
|
||||||
return;
|
final CommandUser user = user(sub, ctx);
|
||||||
}
|
|
||||||
this.handleMigrationCommand(args);
|
|
||||||
}
|
|
||||||
case "update" -> updateChecker.check().thenAccept(checked -> {
|
|
||||||
if (checked.isUpToDate()) {
|
if (checked.isUpToDate()) {
|
||||||
plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString())
|
plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString())
|
||||||
.ifPresent(executor::sendMessage);
|
.ifPresent(user::sendMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
plugin.getLocales().getLocale("update_available", checked.getLatestVersion().toString(),
|
plugin.getLocales().getLocale("update_available", checked.getLatestVersion().toString(),
|
||||||
plugin.getPluginVersion().toString()).ifPresent(executor::sendMessage);
|
plugin.getPluginVersion().toString()).ifPresent(user::sendMessage);
|
||||||
});
|
}));
|
||||||
default -> plugin.getLocales().getLocale("error_invalid_syntax", getUsage())
|
|
||||||
.ifPresent(executor::sendMessage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle a migration console command input
|
@NotNull
|
||||||
private void handleMigrationCommand(@NotNull String[] args) {
|
private CommandProvider migrate() {
|
||||||
if (args.length < 2) {
|
return (sub) -> {
|
||||||
plugin.log(Level.INFO,
|
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
|
||||||
"Please choose a migrator, then run \"husksync migrate <migrator>\"");
|
sub.setDefaultExecutor((ctx) -> {
|
||||||
this.logMigratorList();
|
plugin.log(Level.INFO, "Please choose a migrator, then run \"husksync migrate start <migrator>\"");
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Optional<Migrator> selectedMigrator = plugin.getAvailableMigrators().stream()
|
|
||||||
.filter(available -> available.getIdentifier().equalsIgnoreCase(args[1]))
|
|
||||||
.findFirst();
|
|
||||||
selectedMigrator.ifPresentOrElse(migrator -> {
|
|
||||||
if (args.length < 3) {
|
|
||||||
plugin.log(Level.INFO, migrator.getHelpMenu());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (args[2]) {
|
|
||||||
case "start" -> migrator.start().thenAccept(succeeded -> {
|
|
||||||
if (succeeded) {
|
|
||||||
plugin.log(Level.INFO, "Migration completed successfully!");
|
|
||||||
} else {
|
|
||||||
plugin.log(Level.WARNING, "Migration failed!");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
case "set" -> migrator.handleConfigurationCommand(Arrays.copyOfRange(args, 3, args.length));
|
|
||||||
default -> plugin.log(Level.INFO, String.format(
|
|
||||||
"Invalid syntax. Console usage: \"husksync migrate %s <start/set>", args[1]
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}, () -> {
|
|
||||||
plugin.log(Level.INFO,
|
|
||||||
"Please specify a valid migrator.\n" +
|
|
||||||
"If a migrator is not available, please verify that you meet the prerequisites to use it.");
|
|
||||||
this.logMigratorList();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the list of available migrators
|
|
||||||
private void logMigratorList() {
|
|
||||||
plugin.log(Level.INFO, String.format(
|
plugin.log(Level.INFO, String.format(
|
||||||
"List of available migrators:\nMigrator ID / Migrator Name:\n%s",
|
"List of available migrators:\nMigrator ID / Migrator Name:\n%s",
|
||||||
plugin.getAvailableMigrators().stream()
|
plugin.getAvailableMigrators().stream()
|
||||||
.map(migrator -> String.format("%s - %s", migrator.getIdentifier(), migrator.getName()))
|
.map(migrator -> String.format("%s - %s", migrator.getIdentifier(), migrator.getName()))
|
||||||
.collect(Collectors.joining("\n"))
|
.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!");
|
||||||
}
|
}
|
||||||
|
});
|
||||||
@Nullable
|
}, migrator()));
|
||||||
@Override
|
sub.addSubCommand("set", (set) -> set.addSyntax((cmd) -> {
|
||||||
public List<String> suggest(@NotNull CommandUser user, @NotNull String[] args) {
|
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
|
||||||
return switch (args.length) {
|
final String[] args = cmd.getArgument("args", String.class).split(" ");
|
||||||
case 0, 1 -> SUB_COMMANDS.keySet().stream().sorted().toList();
|
migrator.handleConfigurationCommand(args);
|
||||||
default -> null;
|
}, migrator(), BaseCommand.greedyString("args")));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum StatusLine {
|
@NotNull
|
||||||
PLUGIN_VERSION(plugin -> Component.text("v" + plugin.getPluginVersion().toStringWithoutMetadata())
|
private CommandProvider forceUpgrade() {
|
||||||
.appendSpace().append(plugin.getPluginVersion().getMetadata().isBlank() ? Component.empty()
|
return (sub) -> {
|
||||||
: Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))),
|
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
|
||||||
PLATFORM_TYPE(plugin -> Component.text(WordUtils.capitalizeFully(plugin.getPlatformType()))),
|
sub.setDefaultExecutor((ctx) -> {
|
||||||
LANGUAGE(plugin -> Component.text(plugin.getSettings().getLanguage())),
|
final LegacyConverter converter = plugin.getLegacyConverter().orElse(null);
|
||||||
MINECRAFT_VERSION(plugin -> Component.text(plugin.getMinecraftVersion().toString())),
|
if (converter == null) {
|
||||||
JAVA_VERSION(plugin -> Component.text(System.getProperty("java.version"))),
|
return;
|
||||||
JAVA_VENDOR(plugin -> Component.text(System.getProperty("java.vendor"))),
|
}
|
||||||
SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully(
|
|
||||||
plugin.getSettings().getSynchronization().getMode().toString()
|
|
||||||
))),
|
|
||||||
DELAY_LATENCY(plugin -> Component.text(
|
|
||||||
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms"
|
|
||||||
)),
|
|
||||||
SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
|
|
||||||
CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())),
|
|
||||||
DATABASE_TYPE(plugin ->
|
|
||||||
Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() +
|
|
||||||
(plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ?
|
|
||||||
(plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : ""))
|
|
||||||
),
|
|
||||||
IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())),
|
|
||||||
USING_REDIS_SENTINEL(plugin -> getBoolean(
|
|
||||||
!plugin.getSettings().getRedis().getSentinel().getMaster().isBlank()
|
|
||||||
)),
|
|
||||||
USING_REDIS_PASSWORD(plugin -> getBoolean(
|
|
||||||
!plugin.getSettings().getRedis().getCredentials().getPassword().isBlank()
|
|
||||||
)),
|
|
||||||
REDIS_USING_SSL(plugin -> getBoolean(
|
|
||||||
plugin.getSettings().getRedis().getCredentials().isUseSsl()
|
|
||||||
)),
|
|
||||||
IS_REDIS_LOCAL(plugin -> getLocalhostBoolean(
|
|
||||||
plugin.getSettings().getRedis().getCredentials().getHost()
|
|
||||||
)),
|
|
||||||
DATA_TYPES(plugin -> Component.join(
|
|
||||||
JoinConfiguration.commas(true),
|
|
||||||
plugin.getRegisteredDataTypes().stream().map(i -> {
|
|
||||||
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()
|
|
||||||
));
|
|
||||||
|
|
||||||
private final Function<HuskSync, Component> supplier;
|
plugin.runAsync(() -> {
|
||||||
|
final Database database = plugin.getDatabase();
|
||||||
StatusLine(@NotNull Function<HuskSync, Component> supplier) {
|
plugin.log(Level.INFO, "Beginning forced legacy data upgrade for all users...");
|
||||||
this.supplier = supplier;
|
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
|
@NotNull
|
||||||
private Component get(@NotNull HuskSync plugin) {
|
private <S> ArgumentElement<S, Migrator> migrator() {
|
||||||
return Component
|
return new ArgumentElement<>("migrator", reader -> {
|
||||||
.text("•").appendSpace()
|
final String id = reader.readString();
|
||||||
.append(Component.text(
|
final Migrator migrator = plugin.getAvailableMigrators().stream()
|
||||||
WordUtils.capitalizeFully(name().replaceAll("_", " ")),
|
.filter(m -> m.getIdentifier().equalsIgnoreCase(id)).findFirst().orElse(null);
|
||||||
TextColor.color(0x848484)
|
if (migrator == null) {
|
||||||
))
|
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
|
||||||
.append(Component.text(':')).append(Component.space().color(NamedTextColor.WHITE))
|
|
||||||
.append(supplier.apply(plugin));
|
|
||||||
}
|
}
|
||||||
|
return migrator;
|
||||||
@NotNull
|
}, (context, builder) -> {
|
||||||
private static Component getBoolean(boolean value) {
|
for (Migrator material : plugin.getAvailableMigrators()) {
|
||||||
return Component.text(value ? "Yes" : "No", value ? NamedTextColor.GREEN : NamedTextColor.RED);
|
builder.suggest(material.getIdentifier());
|
||||||
}
|
|
||||||
|
|
||||||
@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"));
|
|
||||||
}
|
}
|
||||||
|
return builder.buildFuture();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import de.themoep.minedown.adventure.MineDown;
|
|||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
import net.william278.husksync.data.Data;
|
import net.william278.husksync.data.Data;
|
||||||
import net.william278.husksync.data.DataSnapshot;
|
import net.william278.husksync.data.DataSnapshot;
|
||||||
import net.william278.husksync.redis.RedisKeyType;
|
|
||||||
import net.william278.husksync.redis.RedisManager;
|
import net.william278.husksync.redis.RedisManager;
|
||||||
import net.william278.husksync.user.OnlineUser;
|
import net.william278.husksync.user.OnlineUser;
|
||||||
import net.william278.husksync.user.User;
|
import net.william278.husksync.user.User;
|
||||||
@@ -37,7 +36,7 @@ import java.util.Optional;
|
|||||||
public class InventoryCommand extends ItemsCommand {
|
public class InventoryCommand extends ItemsCommand {
|
||||||
|
|
||||||
public InventoryCommand(@NotNull HuskSync plugin) {
|
public InventoryCommand(@NotNull HuskSync plugin) {
|
||||||
super(plugin, List.of("inventory", "invsee", "openinv"));
|
super("inventory", List.of("invsee", "openinv"), DataSnapshot.SaveCause.INVENTORY_COMMAND, plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -45,13 +44,14 @@ public class InventoryCommand extends ItemsCommand {
|
|||||||
@NotNull User user, boolean allowEdit) {
|
@NotNull User user, boolean allowEdit) {
|
||||||
final Optional<Data.Items.Inventory> optionalInventory = snapshot.getInventory();
|
final Optional<Data.Items.Inventory> optionalInventory = snapshot.getInventory();
|
||||||
if (optionalInventory.isEmpty()) {
|
if (optionalInventory.isEmpty()) {
|
||||||
|
viewer.sendMessage(new MineDown("what the FUCK is happening"));
|
||||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||||
.ifPresent(viewer::sendMessage);
|
.ifPresent(viewer::sendMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display opening message
|
// Display opening message
|
||||||
plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(),
|
plugin.getLocales().getLocale("inventory_viewer_opened", user.getName(),
|
||||||
snapshot.getTimestamp().format(DateTimeFormatter
|
snapshot.getTimestamp().format(DateTimeFormatter
|
||||||
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
|
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
|
||||||
.ifPresent(viewer::sendMessage);
|
.ifPresent(viewer::sendMessage);
|
||||||
@@ -60,8 +60,8 @@ public class InventoryCommand extends ItemsCommand {
|
|||||||
final Data.Items.Inventory inventory = optionalInventory.get();
|
final Data.Items.Inventory inventory = optionalInventory.get();
|
||||||
viewer.showGui(
|
viewer.showGui(
|
||||||
inventory,
|
inventory,
|
||||||
plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getUsername())
|
plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getName())
|
||||||
.orElse(new MineDown(String.format("%s's Inventory", user.getUsername()))),
|
.orElse(new MineDown(String.format("%s's Inventory", user.getName()))),
|
||||||
allowEdit,
|
allowEdit,
|
||||||
inventory.getSlotCount(),
|
inventory.getSlotCount(),
|
||||||
(itemsOnClose) -> {
|
(itemsOnClose) -> {
|
||||||
@@ -84,18 +84,17 @@ public class InventoryCommand extends ItemsCommand {
|
|||||||
|
|
||||||
// Create and pack the snapshot with the updated inventory
|
// Create and pack the snapshot with the updated inventory
|
||||||
final DataSnapshot.Packed snapshot = latestData.get().copy();
|
final DataSnapshot.Packed snapshot = latestData.get().copy();
|
||||||
|
boolean pin = plugin.getSettings().getSynchronization().doAutoPin(saveCause);
|
||||||
snapshot.edit(plugin, (data) -> {
|
snapshot.edit(plugin, (data) -> {
|
||||||
data.getInventory().ifPresent(inventory -> inventory.setContents(items));
|
data.getInventory().ifPresent(inventory -> inventory.setContents(items));
|
||||||
data.setSaveCause(DataSnapshot.SaveCause.INVENTORY_COMMAND);
|
data.setSaveCause(saveCause);
|
||||||
data.setPinned(
|
data.setPinned(pin);
|
||||||
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save data
|
// Save data
|
||||||
final RedisManager redis = plugin.getRedisManager();
|
final RedisManager redis = plugin.getRedisManager();
|
||||||
plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
|
plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
|
||||||
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
|
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot));
|
||||||
redis.sendUserDataUpdate(user, data);
|
redis.sendUserDataUpdate(user, data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,52 +24,52 @@ import net.william278.husksync.data.DataSnapshot;
|
|||||||
import net.william278.husksync.user.CommandUser;
|
import net.william278.husksync.user.CommandUser;
|
||||||
import net.william278.husksync.user.OnlineUser;
|
import net.william278.husksync.user.OnlineUser;
|
||||||
import net.william278.husksync.user.User;
|
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.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
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) {
|
protected final DataSnapshot.SaveCause saveCause;
|
||||||
super(aliases.get(0), aliases.subList(1, aliases.size()), "<player> [version_uuid]", plugin);
|
|
||||||
setOperatorCommand(true);
|
protected ItemsCommand(@NotNull String name, @NotNull List<String> aliases,
|
||||||
addAdditionalPermissions(Map.of("edit", true));
|
@NotNull DataSnapshot.SaveCause saveCause, @NotNull HuskSync plugin) {
|
||||||
|
super(name, aliases, Permission.Default.IF_OP, ExecutionScope.IN_GAME, plugin);
|
||||||
|
this.saveCause = saveCause;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
|
public void provide(@NotNull BaseCommand<?> command) {
|
||||||
if (!(executor instanceof OnlineUser player)) {
|
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")
|
plugin.getLocales().getLocale("error_in_game_command_only")
|
||||||
.ifPresent(executor::sendMessage);
|
.ifPresent(executor::sendMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.showSnapshotItems(online, user, version);
|
||||||
// Find the user to view the items for
|
}, user("username"), versionUuid());
|
||||||
final Optional<User> optionalUser = parseStringArg(args, 0)
|
command.addSyntax((ctx) -> {
|
||||||
.flatMap(name -> plugin.getDatabase().getUserByName(name));
|
final User user = ctx.getArgument("username", User.class);
|
||||||
if (optionalUser.isEmpty()) {
|
final CommandUser executor = user(command, ctx);
|
||||||
plugin.getLocales().getLocale(
|
if (!(executor instanceof OnlineUser online)) {
|
||||||
args.length >= 1 ? "error_invalid_player" : "error_invalid_syntax", getUsage()
|
plugin.getLocales().getLocale("error_in_game_command_only")
|
||||||
).ifPresent(player::sendMessage);
|
.ifPresent(executor::sendMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.showLatestItems(online, user);
|
||||||
// Show the user data
|
}, user("username"));
|
||||||
final User user = optionalUser.get();
|
|
||||||
parseUUIDArg(args, 1).ifPresentOrElse(
|
|
||||||
version -> this.showSnapshotItems(player, user, version),
|
|
||||||
() -> this.showLatestItems(player, user)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// View (and edit) the latest user data
|
// View (and edit) the latest user data
|
||||||
private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) {
|
private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) {
|
||||||
plugin.getRedisManager().getUserData(user.getUuid(), user).thenAccept(data -> data
|
plugin.getRedisManager().getOnlineUserData(user.getUuid(), user, saveCause).thenAccept(d -> d
|
||||||
.or(() -> plugin.getDatabase().getLatestSnapshot(user))
|
.or(() -> plugin.getDatabase().getLatestSnapshot(user))
|
||||||
.or(() -> {
|
.or(() -> {
|
||||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||||
@@ -114,12 +114,4 @@ public abstract class ItemsCommand extends Command implements TabProvider {
|
|||||||
protected abstract void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
|
protected abstract void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
|
||||||
@NotNull User user, boolean allowEdit);
|
@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,154 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||||
|
*
|
||||||
|
* Copyright (c) William278 <will27528@gmail.com>
|
||||||
|
* Copyright (c) contributors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.william278.husksync.command;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.context.CommandContext;
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
import net.william278.husksync.HuskSync;
|
||||||
|
import net.william278.husksync.user.CommandUser;
|
||||||
|
import net.william278.husksync.user.OnlineUser;
|
||||||
|
import net.william278.husksync.user.User;
|
||||||
|
import net.william278.uniform.BaseCommand;
|
||||||
|
import net.william278.uniform.Command;
|
||||||
|
import net.william278.uniform.Permission;
|
||||||
|
import net.william278.uniform.element.ArgumentElement;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
public abstract class PluginCommand extends Command {
|
||||||
|
|
||||||
|
protected final HuskSync plugin;
|
||||||
|
|
||||||
|
protected PluginCommand(@NotNull String name, @NotNull List<String> aliases, @NotNull Permission.Default defPerm,
|
||||||
|
@NotNull ExecutionScope scope, @NotNull HuskSync plugin) {
|
||||||
|
super(name, aliases, getDescription(plugin, name), new Permission(createPermission(name), defPerm), scope);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getDescription(@NotNull HuskSync plugin, @NotNull String name) {
|
||||||
|
return plugin.getLocales().getRawLocale("%s_command_description".formatted(name)).orElse("");
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static String createPermission(@NotNull String name, @NotNull String... sub) {
|
||||||
|
return "husksync.command." + name + (sub.length > 0 ? "." + String.join(".", sub) : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
protected String getPermission(@NotNull String... sub) {
|
||||||
|
return createPermission(this.getName(), sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
protected CommandUser user(@NotNull BaseCommand base, @NotNull CommandContext context) {
|
||||||
|
return adapt(base.getUser(context.getSource()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
protected Permission needsOp(@NotNull String... nodes) {
|
||||||
|
return new Permission(getPermission(nodes), Permission.Default.IF_OP);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
protected CommandUser adapt(net.william278.uniform.CommandUser user) {
|
||||||
|
return user.getUuid() == null ? plugin.getConsole() : plugin.getOnlineUser(user.getUuid()).orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@SuppressWarnings("SameParameterValue")
|
||||||
|
protected <S> ArgumentElement<S, OnlineUser> onlineUser(@NotNull String name) {
|
||||||
|
return new ArgumentElement<>(name, reader -> {
|
||||||
|
final String username = reader.readString();
|
||||||
|
return plugin.getOnlineUsers().stream()
|
||||||
|
.filter(user -> username.equals(user.getName()))
|
||||||
|
.findFirst().orElse(null);
|
||||||
|
}, (context, builder) -> {
|
||||||
|
plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getName()));
|
||||||
|
return builder.buildFuture();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
protected <S> ArgumentElement<S, User> user(@NotNull String name) {
|
||||||
|
return new ArgumentElement<>(name, reader -> {
|
||||||
|
final String username = reader.readString();
|
||||||
|
return plugin.getDatabase().getUserByName(username).orElseThrow(
|
||||||
|
() -> CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader)
|
||||||
|
);
|
||||||
|
}, (context, builder) -> {
|
||||||
|
plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getName()));
|
||||||
|
return builder.buildFuture();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
protected <S> ArgumentElement<S, UUID> versionUuid() {
|
||||||
|
return new ArgumentElement<>("version", reader -> {
|
||||||
|
try {
|
||||||
|
return UUID.fromString(reader.readString());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
|
||||||
|
}
|
||||||
|
}, (context, builder) -> {
|
||||||
|
try {
|
||||||
|
plugin.getDatabase().getAllSnapshots(context.getArgument("username", User.class))
|
||||||
|
.stream().sorted(Comparator.comparing(d -> d.getTimestamp().toEpochSecond()))
|
||||||
|
.forEach(id -> builder.suggest(id.getId().toString()));
|
||||||
|
return builder.buildFuture();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return builder.buildFuture();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
|
||||||
|
HUSKSYNC_COMMAND(HuskSyncCommand::new),
|
||||||
|
USERDATA_COMMAND(UserDataCommand::new),
|
||||||
|
INVENTORY_COMMAND(InventoryCommand::new),
|
||||||
|
ENDER_CHEST_COMMAND(EnderChestCommand::new);
|
||||||
|
|
||||||
|
public final Function<HuskSync, PluginCommand> commandSupplier;
|
||||||
|
|
||||||
|
Type(@NotNull Function<HuskSync, PluginCommand> supplier) {
|
||||||
|
this.commandSupplier = supplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public PluginCommand supply(@NotNull HuskSync plugin) {
|
||||||
|
return commandSupplier.apply(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public static PluginCommand[] create(@NotNull HuskSync plugin) {
|
||||||
|
return Arrays.stream(values()).map(type -> type.supply(plugin))
|
||||||
|
.filter(command -> !plugin.getSettings().isCommandDisabled(command))
|
||||||
|
.toArray(PluginCommand[]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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,92 +19,47 @@
|
|||||||
|
|
||||||
package net.william278.husksync.command;
|
package net.william278.husksync.command;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.event.ClickEvent;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.kyori.adventure.text.format.TextDecoration;
|
||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
import net.william278.husksync.data.DataSnapshot;
|
import net.william278.husksync.data.DataSnapshot;
|
||||||
import net.william278.husksync.redis.RedisKeyType;
|
|
||||||
import net.william278.husksync.redis.RedisManager;
|
import net.william278.husksync.redis.RedisManager;
|
||||||
import net.william278.husksync.user.CommandUser;
|
import net.william278.husksync.user.CommandUser;
|
||||||
|
import net.william278.husksync.user.OnlineUser;
|
||||||
import net.william278.husksync.user.User;
|
import net.william278.husksync.user.User;
|
||||||
import net.william278.husksync.util.DataDumper;
|
|
||||||
import net.william278.husksync.util.DataSnapshotList;
|
import net.william278.husksync.util.DataSnapshotList;
|
||||||
import net.william278.husksync.util.DataSnapshotOverview;
|
import net.william278.husksync.util.DataSnapshotOverview;
|
||||||
|
import net.william278.husksync.util.UserDataDumper;
|
||||||
|
import net.william278.uniform.BaseCommand;
|
||||||
|
import net.william278.uniform.CommandProvider;
|
||||||
|
import net.william278.uniform.Permission;
|
||||||
|
import net.william278.uniform.element.ArgumentElement;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import 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;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
public class UserDataCommand extends Command implements TabProvider {
|
public class UserDataCommand extends PluginCommand {
|
||||||
|
|
||||||
private static final Map<String, Boolean> SUB_COMMANDS = Map.of(
|
|
||||||
"view", false,
|
|
||||||
"list", false,
|
|
||||||
"delete", true,
|
|
||||||
"restore", true,
|
|
||||||
"pin", true,
|
|
||||||
"dump", true
|
|
||||||
);
|
|
||||||
|
|
||||||
public UserDataCommand(@NotNull HuskSync plugin) {
|
public UserDataCommand(@NotNull HuskSync plugin) {
|
||||||
super("userdata", List.of("playerdata"), String.format(
|
super("userdata", List.of("playerdata"), Permission.Default.IF_OP, ExecutionScope.ALL, plugin);
|
||||||
"<%s> [username] [version_uuid]", String.join("/", SUB_COMMANDS.keySet())
|
|
||||||
), plugin);
|
|
||||||
setOperatorCommand(true);
|
|
||||||
addAdditionalPermissions(SUB_COMMANDS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
|
public void provide(@NotNull BaseCommand<?> command) {
|
||||||
final String subCommand = parseStringArg(args, 0).orElse("view").toLowerCase(Locale.ENGLISH);
|
command.addSubCommand("view", needsOp("view"), view());
|
||||||
final Optional<User> optionalUser = parseStringArg(args, 1)
|
command.addSubCommand("list", needsOp("list"), list());
|
||||||
.flatMap(name -> plugin.getDatabase().getUserByName(name))
|
command.addSubCommand("delete", needsOp("delete"), delete());
|
||||||
.or(() -> parseStringArg(args, 0).flatMap(name -> plugin.getDatabase().getUserByName(name)))
|
command.addSubCommand("save", needsOp("save"), save());
|
||||||
.or(() -> args.length < 2 && executor instanceof User userExecutor
|
command.addSubCommand("restore", needsOp("restore"), restore());
|
||||||
? Optional.of(userExecutor) : Optional.empty());
|
command.addSubCommand("pin", needsOp("pin"), pin());
|
||||||
final Optional<UUID> uuid = parseUUIDArg(args, 2).or(() -> parseUUIDArg(args, 1));
|
command.addSubCommand("dump", needsOp("dump"), dump());
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show the latest snapshot
|
// Show the latest snapshot
|
||||||
@@ -152,6 +107,13 @@ public class UserDataCommand extends Command implements TabProvider {
|
|||||||
DataSnapshotList.create(dataList, user, plugin).displayPage(executor, page);
|
DataSnapshotList.create(dataList, user, plugin).displayPage(executor, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create and save a snapshot of a user's current data
|
||||||
|
private void createAndSaveSnapshot(@NotNull CommandUser executor, @NotNull OnlineUser onlineUser) {
|
||||||
|
plugin.getDataSyncer().saveCurrentUserData(onlineUser, DataSnapshot.SaveCause.SAVE_COMMAND);
|
||||||
|
plugin.getLocales().getLocale("data_saved", onlineUser.getName())
|
||||||
|
.ifPresent(executor::sendMessage);
|
||||||
|
}
|
||||||
|
|
||||||
// Delete a snapshot
|
// Delete a snapshot
|
||||||
private void deleteSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
private void deleteSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
||||||
if (!plugin.getDatabase().deleteSnapshot(user, version)) {
|
if (!plugin.getDatabase().deleteSnapshot(user, version)) {
|
||||||
@@ -163,7 +125,7 @@ public class UserDataCommand extends Command implements TabProvider {
|
|||||||
plugin.getLocales().getLocale("data_deleted",
|
plugin.getLocales().getLocale("data_deleted",
|
||||||
version.toString().split("-")[0],
|
version.toString().split("-")[0],
|
||||||
version.toString(),
|
version.toString(),
|
||||||
user.getUsername(),
|
user.getName(),
|
||||||
user.getUuid().toString())
|
user.getUuid().toString())
|
||||||
.ifPresent(executor::sendMessage);
|
.ifPresent(executor::sendMessage);
|
||||||
}
|
}
|
||||||
@@ -195,9 +157,9 @@ public class UserDataCommand extends Command implements TabProvider {
|
|||||||
// Save data
|
// Save data
|
||||||
final RedisManager redis = plugin.getRedisManager();
|
final RedisManager redis = plugin.getRedisManager();
|
||||||
plugin.getDataSyncer().saveData(user, data, (u, s) -> {
|
plugin.getDataSyncer().saveData(user, data, (u, s) -> {
|
||||||
redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s, RedisKeyType.TTL_1_YEAR));
|
redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s));
|
||||||
redis.sendUserDataUpdate(u, s);
|
redis.sendUserDataUpdate(u, s);
|
||||||
plugin.getLocales().getLocale("data_restored", u.getUsername(), u.getUuid().toString(),
|
plugin.getLocales().getLocale("data_restored", u.getName(), u.getUuid().toString(),
|
||||||
s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage);
|
s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -219,40 +181,141 @@ public class UserDataCommand extends Command implements TabProvider {
|
|||||||
plugin.getDatabase().pinSnapshot(user, data.getId());
|
plugin.getDatabase().pinSnapshot(user, data.getId());
|
||||||
}
|
}
|
||||||
plugin.getLocales().getLocale(data.isPinned() ? "data_unpinned" : "data_pinned", data.getShortId(),
|
plugin.getLocales().getLocale(data.isPinned() ? "data_unpinned" : "data_pinned", data.getShortId(),
|
||||||
data.getId().toString(), user.getUsername(), user.getUuid().toString())
|
data.getId().toString(), user.getName(), user.getUuid().toString())
|
||||||
.ifPresent(executor::sendMessage);
|
.ifPresent(executor::sendMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dump a snapshot
|
// Lookup a snapshot by UUID and dump
|
||||||
private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version, 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);
|
final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, version);
|
||||||
if (data.isEmpty()) {
|
if (data.isEmpty()) {
|
||||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||||
.ifPresent(executor::sendMessage);
|
.ifPresent(executor::sendMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.dumpSnapshot(executor, user, data.get(), type);
|
||||||
|
}
|
||||||
|
|
||||||
// Dump the data
|
// Dump a snapshot
|
||||||
final DataSnapshot.Packed userData = data.get();
|
private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user,
|
||||||
final DataDumper dumper = DataDumper.create(userData, user, plugin);
|
@NotNull DataSnapshot.Packed userData, @NotNull DumpType type) {
|
||||||
|
final UserDataDumper dumper = UserDataDumper.create(userData, user, plugin);
|
||||||
try {
|
try {
|
||||||
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getUsername(),
|
final String url = type == DumpType.WEB ? dumper.toWeb() : dumper.toFile();
|
||||||
(webDump ? dumper.toWeb() : dumper.toFile())).ifPresent(executor::sendMessage);
|
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getName())
|
||||||
|
.ifPresent(executor::sendMessage);
|
||||||
|
executor.sendMessage(Component.text(url)
|
||||||
|
.clickEvent(type == DumpType.WEB ? ClickEvent.openUrl(url) : ClickEvent.copyToClipboard(url))
|
||||||
|
.decorate(TextDecoration.UNDERLINED).color(NamedTextColor.GRAY));
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
plugin.log(Level.SEVERE, "Failed to dump user data", e);
|
plugin.log(Level.SEVERE, "Failed to dump user data", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@NotNull
|
||||||
@Override
|
private CommandProvider view() {
|
||||||
public List<String> suggest(@NotNull CommandUser executor, @NotNull String[] args) {
|
return (sub) -> sub.addSyntax((ctx) -> {
|
||||||
return switch (args.length) {
|
final User user = ctx.getArgument("username", User.class);
|
||||||
case 0, 1 -> SUB_COMMANDS.keySet().stream().sorted().toList();
|
final UUID version = ctx.getArgument("version", UUID.class);
|
||||||
case 2 -> plugin.getOnlineUsers().stream().map(User::getUsername).toList();
|
viewSnapshot(user(sub, ctx), user, version);
|
||||||
case 4 -> parseStringArg(args, 0)
|
}, user("username"), versionUuid());
|
||||||
.map(arg -> arg.equalsIgnoreCase("dump") ? List.of("web", "file") : null)
|
}
|
||||||
.orElse(null);
|
|
||||||
default -> null;
|
@NotNull
|
||||||
|
private CommandProvider list() {
|
||||||
|
return (sub) -> {
|
||||||
|
sub.addSyntax((ctx) -> {
|
||||||
|
final User user = ctx.getArgument("username", User.class);
|
||||||
|
listSnapshots(user(sub, ctx), user, 1);
|
||||||
|
}, user("username"));
|
||||||
|
sub.addSyntax((ctx) -> {
|
||||||
|
final User user = ctx.getArgument("username", User.class);
|
||||||
|
final int page = ctx.getArgument("page", Integer.class);
|
||||||
|
listSnapshots(user(sub, ctx), user, page);
|
||||||
|
}, user("username"), BaseCommand.intNum("page", 1));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private CommandProvider delete() {
|
||||||
|
return (sub) -> sub.addSyntax((ctx) -> {
|
||||||
|
final User user = ctx.getArgument("username", User.class);
|
||||||
|
final UUID version = ctx.getArgument("version", UUID.class);
|
||||||
|
deleteSnapshot(user(sub, ctx), user, version);
|
||||||
|
}, user("username"), versionUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private CommandProvider save() {
|
||||||
|
return (sub) -> sub.addSyntax((ctx) -> {
|
||||||
|
final OnlineUser user = ctx.getArgument("username", OnlineUser.class);
|
||||||
|
createAndSaveSnapshot(user(sub, ctx), user);
|
||||||
|
}, onlineUser("username"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private CommandProvider restore() {
|
||||||
|
return (sub) -> sub.addSyntax((ctx) -> {
|
||||||
|
final User user = ctx.getArgument("username", User.class);
|
||||||
|
final UUID version = ctx.getArgument("version", UUID.class);
|
||||||
|
restoreSnapshot(user(sub, ctx), user, version);
|
||||||
|
}, user("username"), versionUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private CommandProvider pin() {
|
||||||
|
return (sub) -> sub.addSyntax((ctx) -> {
|
||||||
|
final User user = ctx.getArgument("username", User.class);
|
||||||
|
final UUID version = ctx.getArgument("version", UUID.class);
|
||||||
|
pinSnapshot(user(sub, ctx), user, version);
|
||||||
|
}, user("username"), versionUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private CommandProvider dump() {
|
||||||
|
return (sub) -> {
|
||||||
|
sub.addSyntax((ctx) -> {
|
||||||
|
final User user = ctx.getArgument("username", User.class);
|
||||||
|
final CommandUser executor = user(sub, ctx);
|
||||||
|
plugin.getRedisManager()
|
||||||
|
.getOnlineUserData(UUID.randomUUID(), user, DataSnapshot.SaveCause.DUMP_COMMAND)
|
||||||
|
.thenAccept((data) -> data
|
||||||
|
.or(() -> plugin.getDatabase().getLatestSnapshot(user))
|
||||||
|
.ifPresentOrElse(
|
||||||
|
(s) -> dumpSnapshot(executor, user, s, DumpType.WEB),
|
||||||
|
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||||
|
.ifPresent(executor::sendMessage)
|
||||||
|
));
|
||||||
|
}, user("username"));
|
||||||
|
sub.addSyntax((ctx) -> {
|
||||||
|
final User user = ctx.getArgument("username", User.class);
|
||||||
|
final UUID version = ctx.getArgument("version", UUID.class);
|
||||||
|
final DumpType type = ctx.getArgument("type", DumpType.class);
|
||||||
|
dumpSnapshot(user(sub, ctx), user, version, type);
|
||||||
|
}, user("username"), versionUuid(), dumpType());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private <S> ArgumentElement<S, DumpType> dumpType() {
|
||||||
|
return new ArgumentElement<>("type", reader -> {
|
||||||
|
final String type = reader.readString();
|
||||||
|
return switch (type.toLowerCase(Locale.ENGLISH)) {
|
||||||
|
case "web" -> DumpType.WEB;
|
||||||
|
case "file" -> DumpType.FILE;
|
||||||
|
default -> throw CommandSyntaxException.BUILT_IN_EXCEPTIONS
|
||||||
|
.dispatcherUnknownArgument().createWithContext(reader);
|
||||||
|
};
|
||||||
|
}, (context, builder) -> {
|
||||||
|
builder.suggest("web");
|
||||||
|
builder.suggest("file");
|
||||||
|
return builder.buildFuture();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DumpType {
|
||||||
|
WEB,
|
||||||
|
FILE
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* Get a plugin resource
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -193,14 +193,20 @@ public class Locales {
|
|||||||
* Displays the notification in the action bar
|
* Displays the notification in the action bar
|
||||||
*/
|
*/
|
||||||
ACTION_BAR,
|
ACTION_BAR,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the notification in the chat
|
* Displays the notification in the chat
|
||||||
*/
|
*/
|
||||||
CHAT,
|
CHAT,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the notification in an Advancement Toast
|
* Displays the notification in an Advancement Toast
|
||||||
|
*
|
||||||
|
* @deprecated No longer supported
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(since = "3.6.7")
|
||||||
TOAST,
|
TOAST,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does not display the notification
|
* Does not display the notification
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import de.exlll.configlib.Configuration;
|
|||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import net.william278.husksync.command.PluginCommand;
|
||||||
import net.william278.husksync.data.DataSnapshot;
|
import net.william278.husksync.data.DataSnapshot;
|
||||||
import net.william278.husksync.data.Identifier;
|
import net.william278.husksync.data.Identifier;
|
||||||
import net.william278.husksync.database.Database;
|
import net.william278.husksync.database.Database;
|
||||||
@@ -69,15 +70,15 @@ public class Settings {
|
|||||||
@Comment("Enable development debug logging")
|
@Comment("Enable development debug logging")
|
||||||
private boolean debugLogging = false;
|
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"})
|
@Comment({"Whether to enable the Player Analytics hook.", "Docs: https://william278.net/docs/husksync/plan-hook"})
|
||||||
private boolean enablePlanHook = true;
|
private boolean enablePlanHook = true;
|
||||||
|
|
||||||
@Comment("Whether to cancel game event packets directly when handling locked players if ProtocolLib or PacketEvents is installed")
|
@Comment("Whether to cancel game event packets directly when handling locked players if ProtocolLib or PacketEvents is installed")
|
||||||
private boolean cancelPackets = true;
|
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
|
// Database settings
|
||||||
@Comment("Database settings")
|
@Comment("Database settings")
|
||||||
@@ -140,6 +141,9 @@ public class Settings {
|
|||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
private Map<String, String> tableNames = Database.TableName.getDefaults();
|
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
|
@NotNull
|
||||||
public String getTableName(@NotNull Database.TableName tableName) {
|
public String getTableName(@NotNull Database.TableName tableName) {
|
||||||
return tableNames.getOrDefault(tableName.name().toLowerCase(Locale.ENGLISH), tableName.getDefaultName());
|
return tableNames.getOrDefault(tableName.name().toLowerCase(Locale.ENGLISH), tableName.getDefaultName());
|
||||||
@@ -155,7 +159,7 @@ public class Settings {
|
|||||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
public static class RedisSettings {
|
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();
|
private RedisCredentials credentials = new RedisCredentials();
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@@ -185,7 +189,7 @@ public class Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Synchronization settings
|
// Synchronization settings
|
||||||
@Comment("Redis settings")
|
@Comment("Data syncing settings")
|
||||||
private SynchronizationSettings synchronization = new SynchronizationSettings();
|
private SynchronizationSettings synchronization = new SynchronizationSettings();
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@@ -249,7 +253,7 @@ public class Settings {
|
|||||||
@Comment("Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing")
|
@Comment("Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing")
|
||||||
private boolean compressData = true;
|
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;
|
private Locales.NotificationSlot notificationDisplaySlot = Locales.NotificationSlot.ACTION_BAR;
|
||||||
|
|
||||||
@Comment("Persist maps locked in a Cartography Table to let them be viewed on any server")
|
@Comment("Persist maps locked in a Cartography Table to let them be viewed on any server")
|
||||||
@@ -266,10 +270,52 @@ public class Settings {
|
|||||||
@Comment("Commands which should be blocked before a player has finished syncing (Use * to block all commands)")
|
@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("*"));
|
private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*"));
|
||||||
|
|
||||||
@Comment({"For attribute syncing, which attributes should be ignored/skipped when syncing",
|
@Comment("Configuration for how to sync attributes")
|
||||||
"(e.g. ['minecraft:generic.max_health', 'minecraft:generic.attack_damage'])"})
|
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)
|
@Getter(AccessLevel.NONE)
|
||||||
private List<String> ignoredAttributes = new ArrayList<>(List.of(""));
|
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")
|
@Comment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts")
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
@@ -283,10 +329,6 @@ public class Settings {
|
|||||||
return id.isCustom() || features.getOrDefault(id.getKeyValue(), id.isEnabledByDefault());
|
return id.isCustom() || features.getOrDefault(id.getKeyValue(), id.isEnabledByDefault());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isIgnoredAttribute(@NotNull String attribute) {
|
|
||||||
return ignoredAttributes.contains(attribute);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) {
|
public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) {
|
||||||
try {
|
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.common.collect.Sets;
|
||||||
import com.google.gson.annotations.SerializedName;
|
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.kyori.adventure.key.Key;
|
||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
import net.william278.husksync.user.OnlineUser;
|
import net.william278.husksync.user.OnlineUser;
|
||||||
@@ -78,6 +82,7 @@ public interface Data {
|
|||||||
*/
|
*/
|
||||||
interface Inventory extends Items {
|
interface Inventory extends Items {
|
||||||
|
|
||||||
|
int INVENTORY_SLOT_COUNT = 41;
|
||||||
String ITEMS_TAG = "items";
|
String ITEMS_TAG = "items";
|
||||||
String HELD_ITEM_SLOT_TAG = "held_item_slot";
|
String HELD_ITEM_SLOT_TAG = "held_item_slot";
|
||||||
|
|
||||||
@@ -110,7 +115,7 @@ public interface Data {
|
|||||||
* Data container holding data for ender chests
|
* Data container holding data for ender chests
|
||||||
*/
|
*/
|
||||||
interface EnderChest extends Items {
|
interface EnderChest extends Items {
|
||||||
|
int ENDER_CHEST_SLOT_COUNT = 27;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -126,7 +131,7 @@ public interface Data {
|
|||||||
/**
|
/**
|
||||||
* Represents a potion effect
|
* 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 amplifier the amplifier of the potion effect
|
||||||
* @param duration the duration of the potion effect
|
* @param duration the duration of the potion effect
|
||||||
* @param isAmbient whether the potion effect is ambient
|
* @param isAmbient whether the potion effect is ambient
|
||||||
@@ -149,14 +154,14 @@ public interface Data {
|
|||||||
*/
|
*/
|
||||||
interface Advancements extends Data {
|
interface Advancements extends Data {
|
||||||
|
|
||||||
|
String RECIPE_ADVANCEMENT = "minecraft:recipe";
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
List<Advancement> getCompleted();
|
List<Advancement> getCompleted();
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
default List<Advancement> getCompletedExcludingRecipes() {
|
default List<Advancement> getCompletedExcludingRecipes() {
|
||||||
return getCompleted().stream()
|
return getCompleted().stream().filter(adv -> !adv.getKey().startsWith(RECIPE_ADVANCEMENT)).toList();
|
||||||
.filter(advancement -> !advancement.getKey().startsWith("minecraft:recipe"))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setCompleted(@NotNull List<Advancement> completed);
|
void setCompleted(@NotNull List<Advancement> completed);
|
||||||
@@ -333,27 +338,78 @@ public interface Data {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
record Modifier(
|
@Getter
|
||||||
@NotNull UUID uuid,
|
@Accessors(fluent = true)
|
||||||
@NotNull String name,
|
@RequiredArgsConstructor
|
||||||
double amount,
|
final class Modifier {
|
||||||
@SerializedName("operation") int operationType,
|
final static String ANY_EQUIPMENT_SLOT_GROUP = "any";
|
||||||
@SerializedName("equipment_slot") int equipmentSlot
|
|
||||||
) {
|
@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
|
@Override
|
||||||
public boolean equals(Object obj) {
|
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) {
|
public double modify(double value) {
|
||||||
return switch (operationType) {
|
return switch (operation) {
|
||||||
case 0 -> value + amount;
|
case 0 -> value + amount;
|
||||||
case 1 -> value * amount;
|
case 1 -> value * amount;
|
||||||
case 2 -> value * (1 + amount);
|
case 2 -> value * (1 + amount);
|
||||||
default -> value;
|
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) {
|
default Optional<Attribute> getAttribute(@NotNull Key key) {
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ public interface DataHolder {
|
|||||||
@NotNull
|
@NotNull
|
||||||
Map<Identifier, Data> getData();
|
Map<Identifier, Data> getData();
|
||||||
|
|
||||||
default Optional<? extends Data> getData(@NotNull Identifier identifier) {
|
default Optional<? extends Data> getData(@NotNull Identifier id) {
|
||||||
return Optional.ofNullable(getData().get(identifier));
|
return getData().entrySet().stream().filter(e -> e.getKey().equals(id)).map(Map.Entry::getValue).findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
default void setData(@NotNull Identifier identifier, @NotNull Data data) {
|
default void setData(@NotNull Identifier identifier, @NotNull Data data) {
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ public class DataSnapshot {
|
|||||||
public static class Unpacked extends DataSnapshot implements DataHolder {
|
public static class Unpacked extends DataSnapshot implements DataHolder {
|
||||||
|
|
||||||
@Expose(serialize = false, deserialize = false)
|
@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,
|
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||||
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
@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,
|
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) {
|
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||||
super(id, pinned, timestamp, saveCause, serverName, Map.of(), minecraftVersion, platformType, formatVersion);
|
super(id, pinned, timestamp, saveCause, serverName, Map.of(), minecraftVersion, platformType, formatVersion);
|
||||||
this.deserialized = data;
|
this.deserialized = data;
|
||||||
@@ -389,25 +389,25 @@ public class DataSnapshot {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
private Map<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
|
private TreeMap<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
|
||||||
return data.entrySet().stream()
|
return data.entrySet().stream()
|
||||||
.map((entry) -> plugin.getIdentifier(entry.getKey()).map(id -> Map.entry(
|
.filter(e -> plugin.getIdentifier(e.getKey()).isPresent())
|
||||||
id, plugin.getSerializers().get(id).deserialize(entry.getValue(), getMinecraftVersion())
|
.map(entry -> Map.entry(plugin.getIdentifier(entry.getKey()).orElseThrow(), entry.getValue()))
|
||||||
)).orElse(null))
|
.collect(Collectors.toMap(
|
||||||
.filter(Objects::nonNull)
|
Map.Entry::getKey,
|
||||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
entry -> plugin.deserializeData(entry.getKey(), entry.getValue(), getMinecraftVersion()),
|
||||||
|
(a, b) -> b, () -> Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
private Map<String, String> serializeData(@NotNull HuskSync plugin) {
|
private Map<String, String> serializeData(@NotNull HuskSync plugin) {
|
||||||
return deserialized.entrySet().stream()
|
return deserialized.entrySet().stream()
|
||||||
.map((entry) -> Map.entry(entry.getKey().toString(),
|
.collect(Collectors.toMap(
|
||||||
Objects.requireNonNull(
|
entry -> entry.getKey().toString(),
|
||||||
plugin.getSerializers().get(entry.getKey()),
|
entry -> plugin.serializeData(entry.getKey(), entry.getValue())
|
||||||
String.format("No serializer found for %s", entry.getKey())
|
));
|
||||||
).serialize(entry.getValue())))
|
|
||||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -453,12 +453,12 @@ public class DataSnapshot {
|
|||||||
private String serverName;
|
private String serverName;
|
||||||
private boolean pinned;
|
private boolean pinned;
|
||||||
private OffsetDateTime timestamp;
|
private OffsetDateTime timestamp;
|
||||||
private final Map<Identifier, Data> data;
|
private final TreeMap<Identifier, Data> data;
|
||||||
|
|
||||||
private Builder(@NotNull HuskSync plugin) {
|
private Builder(@NotNull HuskSync plugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.pinned = false;
|
this.pinned = false;
|
||||||
this.data = Maps.newHashMap();
|
this.data = Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR);
|
||||||
this.timestamp = OffsetDateTime.now();
|
this.timestamp = OffsetDateTime.now();
|
||||||
this.id = UUID.randomUUID();
|
this.id = UUID.randomUUID();
|
||||||
this.serverName = plugin.getServerName();
|
this.serverName = plugin.getServerName();
|
||||||
@@ -880,6 +880,20 @@ public class DataSnapshot {
|
|||||||
*/
|
*/
|
||||||
public static final SaveCause BACKUP_RESTORE = of("BACKUP_RESTORE");
|
public static final SaveCause BACKUP_RESTORE = of("BACKUP_RESTORE");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates data was saved from executing the {@code /userdata save} command
|
||||||
|
*
|
||||||
|
* @since 3.8
|
||||||
|
*/
|
||||||
|
public static final SaveCause SAVE_COMMAND = of("SAVE_COMMAND", true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates data was saved from executing the {@code /userdata dump} command
|
||||||
|
*
|
||||||
|
* @since 3.8
|
||||||
|
*/
|
||||||
|
public static final SaveCause DUMP_COMMAND = of("DUMP_COMMAND", true);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates data was saved by an API call
|
* Indicates data was saved by an API call
|
||||||
*
|
*
|
||||||
@@ -913,6 +927,8 @@ public class DataSnapshot {
|
|||||||
|
|
||||||
private final boolean fireDataSaveEvent;
|
private final boolean fireDataSaveEvent;
|
||||||
|
|
||||||
|
private static Map<String, SaveCause> registry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or create a {@link SaveCause} from a name
|
* Get or create a {@link SaveCause} from a name
|
||||||
*
|
*
|
||||||
@@ -921,7 +937,7 @@ public class DataSnapshot {
|
|||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
public static SaveCause of(@NotNull String name) {
|
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 +949,14 @@ public class DataSnapshot {
|
|||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
public static SaveCause of(@NotNull String name, boolean firesSaveEvent) {
|
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
|
@NotNull
|
||||||
@@ -944,11 +967,10 @@ public class DataSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@ApiStatus.Obsolete
|
||||||
public static SaveCause[] values() {
|
public static SaveCause[] values() {
|
||||||
return new SaveCause[]{
|
if (registry == null) registry = new HashMap<>();
|
||||||
DISCONNECT, WORLD_SAVE, DEATH, SERVER_SHUTDOWN, INVENTORY_COMMAND, ENDERCHEST_COMMAND,
|
return registry.values().toArray(new SaveCause[0]);
|
||||||
BACKUP_RESTORE, API, MPDB_MIGRATION, LEGACY_MIGRATION, CONVERTED_FROM_V2
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,40 +19,84 @@
|
|||||||
|
|
||||||
package net.william278.husksync.data;
|
package net.william278.husksync.data;
|
||||||
|
|
||||||
|
import lombok.*;
|
||||||
import net.kyori.adventure.key.InvalidKeyException;
|
import net.kyori.adventure.key.InvalidKeyException;
|
||||||
import net.kyori.adventure.key.Key;
|
import net.kyori.adventure.key.Key;
|
||||||
import org.intellij.lang.annotations.Subst;
|
import org.intellij.lang.annotations.Subst;
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
import org.jetbrains.annotations.NotNull;
|
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.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifiers of different types of {@link Data}s
|
* Identifiers of different types of {@link Data}s
|
||||||
*/
|
*/
|
||||||
|
@Getter
|
||||||
public class Identifier {
|
public class Identifier {
|
||||||
|
|
||||||
public static Identifier INVENTORY = huskSync("inventory", true);
|
// Built-in identifiers
|
||||||
public static Identifier ENDER_CHEST = huskSync("ender_chest", true);
|
public static final Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
|
||||||
public static Identifier POTION_EFFECTS = huskSync("potion_effects", true);
|
public static final Identifier INVENTORY = huskSync("inventory", true);
|
||||||
public static Identifier ADVANCEMENTS = huskSync("advancements", true);
|
public static final Identifier ENDER_CHEST = huskSync("ender_chest", true);
|
||||||
public static Identifier LOCATION = huskSync("location", false);
|
public static final Identifier ADVANCEMENTS = huskSync("advancements", true);
|
||||||
public static Identifier STATISTICS = huskSync("statistics", true);
|
public static final Identifier STATISTICS = huskSync("statistics", true);
|
||||||
public static Identifier HEALTH = huskSync("health", true);
|
public static final Identifier POTION_EFFECTS = huskSync("potion_effects", true);
|
||||||
public static Identifier HUNGER = huskSync("hunger", true);
|
public static final Identifier GAME_MODE = huskSync("game_mode", true);
|
||||||
public static Identifier ATTRIBUTES = huskSync("attributes", true);
|
public static final Identifier FLIGHT_STATUS = huskSync("flight_status", true,
|
||||||
public static Identifier EXPERIENCE = huskSync("experience", true);
|
Dependency.optional("game_mode")
|
||||||
public static Identifier GAME_MODE = huskSync("game_mode", true);
|
);
|
||||||
public static Identifier FLIGHT_STATUS = huskSync("flight_status", true);
|
public static final Identifier ATTRIBUTES = huskSync("attributes", true,
|
||||||
public static Identifier PERSISTENT_DATA = huskSync("persistent_data", 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 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.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
|
@NotNull
|
||||||
public static Identifier from(@NotNull Key key) {
|
public static Identifier from(@NotNull Key key) {
|
||||||
if (key.namespace().equals("husksync")) {
|
return from(key, Collections.emptySet());
|
||||||
throw new IllegalArgumentException("You cannot register a key with \"husksync\" as the namespace!");
|
|
||||||
}
|
|
||||||
return new Identifier(key, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,25 +124,34 @@ public class Identifier {
|
|||||||
return from(Key.key(plugin, name));
|
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
|
@NotNull
|
||||||
private static Identifier huskSync(@Subst("null") @NotNull String name,
|
private static Identifier huskSync(@Subst("null") @NotNull String name,
|
||||||
boolean configDefault) throws InvalidKeyException {
|
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
|
@NotNull
|
||||||
@SuppressWarnings("unused")
|
private static Identifier huskSync(@Subst("null") @NotNull String name,
|
||||||
private static Identifier parse(@NotNull String key) throws InvalidKeyException {
|
@SuppressWarnings("SameParameterValue") boolean configDefault,
|
||||||
return huskSync(key, true);
|
@NotNull Dependency... dependents) throws InvalidKeyException {
|
||||||
}
|
return new Identifier(Key.key("husksync", name), configDefault, Set.of(dependents));
|
||||||
|
|
||||||
public boolean isEnabledByDefault() {
|
|
||||||
return configDefault;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private Map.Entry<String, Boolean> getConfigEntry() {
|
|
||||||
return Map.entry(getKeyValue(), configDefault);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,6 +172,17 @@ public class Identifier {
|
|||||||
.toArray(Map.Entry[]::new));
|
.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
|
* Get the namespace of the identifier
|
||||||
*
|
*
|
||||||
@@ -168,12 +229,100 @@ public class Identifier {
|
|||||||
* @param obj the object to compare
|
* @param obj the object to compare
|
||||||
* @return {@code true} if the given object is an identifier with the same key as this identifier
|
* @return {@code true} if the given object is an identifier with the same key as this identifier
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object obj) {
|
||||||
|
return obj instanceof Identifier other ? toString().equals(other.toString()) : super.equals(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return key.toString().hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
@Override
|
||||||
public boolean equals(Object obj) {
|
public boolean equals(Object obj) {
|
||||||
if (obj instanceof Identifier other) {
|
if (obj instanceof Dependency other) {
|
||||||
return key.equals(other.key);
|
return key.equals(other.key);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return key.toString().hashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
package net.william278.husksync.data;
|
package net.william278.husksync.data;
|
||||||
|
|
||||||
import net.william278.desertwell.util.Version;
|
import net.william278.desertwell.util.Version;
|
||||||
|
import net.william278.husksync.HuskSync;
|
||||||
|
import net.william278.husksync.adapter.Adaptable;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public interface Serializer<T extends Data> {
|
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 {
|
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
|
* @return the data that is enabled for syncing
|
||||||
* @since 3.0
|
* @since 3.0
|
||||||
@@ -43,7 +43,7 @@ public interface UserDataHolder extends DataHolder {
|
|||||||
@NotNull
|
@NotNull
|
||||||
default Map<Identifier, Data> getData() {
|
default Map<Identifier, Data> getData() {
|
||||||
return getPlugin().getRegisteredDataTypes().stream()
|
return getPlugin().getRegisteredDataTypes().stream()
|
||||||
.filter(type -> type.isCustom() || getPlugin().getSettings().getSynchronization().isFeatureEnabled(type))
|
.filter(Identifier::isEnabled)
|
||||||
.map(id -> Map.entry(id, getData(id)))
|
.map(id -> Map.entry(id, getData(id)))
|
||||||
.filter(data -> data.getValue().isPresent())
|
.filter(data -> data.getValue().isPresent())
|
||||||
.collect(HashMap::new, (map, data) -> map.put(data.getKey(), data.getValue().get()), HashMap::putAll);
|
.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
|
* Deserialize and apply a data snapshot to this data owner
|
||||||
* <p>
|
* <p>
|
||||||
* This method will deserialize the data on the current thread, then synchronously apply it on
|
* 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>
|
* </p>
|
||||||
* The {@code runAfter} callback function will be run after the snapshot has been applied.
|
* The {@code runAfter} callback function will be run after the snapshot has been applied.
|
||||||
*
|
*
|
||||||
@@ -106,13 +107,16 @@ public interface UserDataHolder extends DataHolder {
|
|||||||
try {
|
try {
|
||||||
for (Map.Entry<Identifier, Data> entry : unpacked.getData().entrySet()) {
|
for (Map.Entry<Identifier, Data> entry : unpacked.getData().entrySet()) {
|
||||||
final Identifier identifier = entry.getKey();
|
final Identifier identifier = entry.getKey();
|
||||||
if (plugin.getSettings().getSynchronization().isFeatureEnabled(identifier)) {
|
if (!identifier.isEnabled()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the identified data
|
||||||
if (identifier.isCustom()) {
|
if (identifier.isCustom()) {
|
||||||
getCustomDataStore().put(identifier, entry.getValue());
|
getCustomDataStore().put(identifier, entry.getValue());
|
||||||
}
|
}
|
||||||
entry.getValue().apply(this, plugin);
|
entry.getValue().apply(this, plugin);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
plugin.log(Level.SEVERE, String.format("Failed to apply data snapshot to %s", getUsername()), e);
|
plugin.log(Level.SEVERE, String.format("Failed to apply data snapshot to %s", getUsername()), e);
|
||||||
plugin.runAsync(() -> runAfter.accept(false));
|
plugin.runAsync(() -> runAfter.accept(false));
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import net.william278.husksync.data.DataSnapshot;
|
|||||||
import net.william278.husksync.user.User;
|
import net.william278.husksync.user.User;
|
||||||
import org.jetbrains.annotations.Blocking;
|
import org.jetbrains.annotations.Blocking;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@@ -56,8 +57,8 @@ public abstract class Database {
|
|||||||
@SuppressWarnings("SameParameterValue")
|
@SuppressWarnings("SameParameterValue")
|
||||||
@NotNull
|
@NotNull
|
||||||
protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException {
|
protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException {
|
||||||
return formatStatementTables(new String(Objects.requireNonNull(plugin.getResource(schemaFileName))
|
return Arrays.stream(formatStatementTables(new String(Objects.requireNonNull(plugin.getResource(schemaFileName))
|
||||||
.readAllBytes(), StandardCharsets.UTF_8)).split(";");
|
.readAllBytes(), StandardCharsets.UTF_8)).split(";")).filter(s -> !s.isBlank()).toArray(String[]::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,7 +71,9 @@ public abstract class Database {
|
|||||||
protected final String formatStatementTables(@NotNull String sql) {
|
protected final String formatStatementTables(@NotNull String sql) {
|
||||||
final Settings.DatabaseSettings settings = plugin.getSettings().getDatabase();
|
final Settings.DatabaseSettings settings = plugin.getSettings().getDatabase();
|
||||||
return sql.replaceAll("%users_table%", settings.getTableName(TableName.USERS))
|
return sql.replaceAll("%users_table%", settings.getTableName(TableName.USERS))
|
||||||
.replaceAll("%user_data_table%", settings.getTableName(TableName.USER_DATA));
|
.replaceAll("%user_data_table%", settings.getTableName(TableName.USER_DATA))
|
||||||
|
.replaceAll("%map_data_table%", settings.getTableName(TableName.MAP_DATA))
|
||||||
|
.replaceAll("%map_ids_table%", settings.getTableName(TableName.MAP_IDS));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,6 +110,14 @@ public abstract class Database {
|
|||||||
@Blocking
|
@Blocking
|
||||||
public abstract Optional<User> getUserByName(@NotNull String username);
|
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.
|
* Get the latest data snapshot for a user.
|
||||||
@@ -238,6 +249,58 @@ public abstract class Database {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write map data to a database
|
||||||
|
*
|
||||||
|
* @param serverName Name of the server the map originates from
|
||||||
|
* @param mapId Original map ID
|
||||||
|
* @param data Map data
|
||||||
|
*/
|
||||||
|
@Blocking
|
||||||
|
public abstract void saveMapData(@NotNull String serverName, int mapId, byte @NotNull [] data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read map data from a database
|
||||||
|
*
|
||||||
|
* @param serverName Name of the server the map originates from
|
||||||
|
* @param mapId Original map ID
|
||||||
|
* @return Map.Entry (key: map data, value: is from current world)
|
||||||
|
*/
|
||||||
|
@Blocking
|
||||||
|
public abstract @Nullable Map.Entry<byte[], Boolean> getMapData(@NotNull String serverName, int mapId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a map server -> ID binding in the database
|
||||||
|
*
|
||||||
|
* @param serverName Name of the server the map originates from
|
||||||
|
* @param mapId Original map ID
|
||||||
|
* @return Map.Entry (key: server name, value: map ID)
|
||||||
|
*/
|
||||||
|
@Blocking
|
||||||
|
public abstract @Nullable Map.Entry<String, Integer> getMapBinding(@NotNull String serverName, int mapId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind map IDs across different servers
|
||||||
|
*
|
||||||
|
* @param fromServerName Name of the server the map originates from
|
||||||
|
* @param fromMapId Original map ID
|
||||||
|
* @param toServerName Name of the new server
|
||||||
|
* @param toMapId New map ID
|
||||||
|
*/
|
||||||
|
@Blocking
|
||||||
|
public abstract void setMapBinding(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName, int toMapId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get map ID for the new server
|
||||||
|
*
|
||||||
|
* @param fromServerName Name of the server the map originates from
|
||||||
|
* @param fromMapId Original map ID
|
||||||
|
* @param toServerName Name of the new server
|
||||||
|
* @return New map ID or -1 if not found
|
||||||
|
*/
|
||||||
|
@Blocking
|
||||||
|
public abstract int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wipes <b>all</b> {@link User} entries from the database.
|
* Wipes <b>all</b> {@link User} entries from the database.
|
||||||
* <b>This should only be used when preparing tables for a data migration.</b>
|
* <b>This should only be used when preparing tables for a data migration.</b>
|
||||||
@@ -275,7 +338,9 @@ public abstract class Database {
|
|||||||
@Getter
|
@Getter
|
||||||
public enum TableName {
|
public enum TableName {
|
||||||
USERS("husksync_users"),
|
USERS("husksync_users"),
|
||||||
USER_DATA("husksync_user_data");
|
USER_DATA("husksync_user_data"),
|
||||||
|
MAP_DATA("husksync_map_data"),
|
||||||
|
MAP_IDS("husksync_map_ids");
|
||||||
|
|
||||||
private final String defaultName;
|
private final String defaultName;
|
||||||
|
|
||||||
|
|||||||
@@ -35,13 +35,11 @@ import org.bson.conversions.Bson;
|
|||||||
import org.bson.types.Binary;
|
import org.bson.types.Binary;
|
||||||
import org.jetbrains.annotations.Blocking;
|
import org.jetbrains.annotations.Blocking;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
public class MongoDbDatabase extends Database {
|
public class MongoDbDatabase extends Database {
|
||||||
@@ -50,17 +48,17 @@ public class MongoDbDatabase extends Database {
|
|||||||
|
|
||||||
private final String usersTable;
|
private final String usersTable;
|
||||||
private final String userDataTable;
|
private final String userDataTable;
|
||||||
|
private final String mapDataTable;
|
||||||
|
private final String mapIdsTable;
|
||||||
|
|
||||||
public MongoDbDatabase(@NotNull HuskSync plugin) {
|
public MongoDbDatabase(@NotNull HuskSync plugin) {
|
||||||
super(plugin);
|
super(plugin);
|
||||||
this.usersTable = plugin.getSettings().getDatabase().getTableName(TableName.USERS);
|
this.usersTable = plugin.getSettings().getDatabase().getTableName(TableName.USERS);
|
||||||
this.userDataTable = plugin.getSettings().getDatabase().getTableName(TableName.USER_DATA);
|
this.userDataTable = plugin.getSettings().getDatabase().getTableName(TableName.USER_DATA);
|
||||||
|
this.mapDataTable = plugin.getSettings().getDatabase().getTableName(TableName.MAP_DATA);
|
||||||
|
this.mapIdsTable = plugin.getSettings().getDatabase().getTableName(TableName.MAP_IDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the database and ensure tables are present; create tables if they do not exist.
|
|
||||||
*
|
|
||||||
* @throws IllegalStateException if the database could not be initialized
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize() throws IllegalStateException {
|
public void initialize() throws IllegalStateException {
|
||||||
final Settings.DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
|
final Settings.DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
|
||||||
@@ -68,12 +66,22 @@ public class MongoDbDatabase extends Database {
|
|||||||
ConnectionString URI = createConnectionURI(credentials);
|
ConnectionString URI = createConnectionURI(credentials);
|
||||||
mongoConnectionHandler = new MongoConnectionHandler(URI, credentials.getDatabase());
|
mongoConnectionHandler = new MongoConnectionHandler(URI, credentials.getDatabase());
|
||||||
mongoCollectionHelper = new MongoCollectionHelper(mongoConnectionHandler);
|
mongoCollectionHelper = new MongoCollectionHelper(mongoConnectionHandler);
|
||||||
|
|
||||||
|
// Check config for if tables should be created
|
||||||
|
if (!plugin.getSettings().getDatabase().isCreateTables()) return;
|
||||||
|
|
||||||
if (mongoCollectionHelper.getCollection(usersTable) == null) {
|
if (mongoCollectionHelper.getCollection(usersTable) == null) {
|
||||||
mongoCollectionHelper.createCollection(usersTable);
|
mongoCollectionHelper.createCollection(usersTable);
|
||||||
}
|
}
|
||||||
if (mongoCollectionHelper.getCollection(userDataTable) == null) {
|
if (mongoCollectionHelper.getCollection(userDataTable) == null) {
|
||||||
mongoCollectionHelper.createCollection(userDataTable);
|
mongoCollectionHelper.createCollection(userDataTable);
|
||||||
}
|
}
|
||||||
|
if (mongoCollectionHelper.getCollection(mapDataTable) == null) {
|
||||||
|
mongoCollectionHelper.createCollection(mapDataTable);
|
||||||
|
}
|
||||||
|
if (mongoCollectionHelper.getCollection(mapIdsTable) == null) {
|
||||||
|
mongoCollectionHelper.createCollection(mapIdsTable);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " +
|
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,27 +101,22 @@ public class MongoDbDatabase extends Database {
|
|||||||
return new ConnectionString(baseURI);
|
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
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public void ensureUser(@NotNull User user) {
|
public void ensureUser(@NotNull User user) {
|
||||||
try {
|
try {
|
||||||
getUser(user.getUuid()).ifPresentOrElse(
|
getUser(user.getUuid()).ifPresentOrElse(
|
||||||
existingUser -> {
|
existingUser -> {
|
||||||
if (!existingUser.getUsername().equals(user.getUsername())) {
|
if (!existingUser.getName().equals(user.getName())) {
|
||||||
// Update a user's name if it has changed in the database
|
// Update a user's name if it has changed in the database
|
||||||
try {
|
try {
|
||||||
Document filter = new Document("uuid", existingUser.getUuid().toString());
|
Document filter = new Document("uuid", existingUser.getUuid());
|
||||||
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
||||||
if (doc == null) {
|
if (doc == null) {
|
||||||
throw new MongoException("User document returned null!");
|
throw new MongoException("User document returned null!");
|
||||||
}
|
}
|
||||||
|
|
||||||
Bson updates = Updates.set("username", user.getUsername());
|
Bson updates = Updates.set("username", user.getName());
|
||||||
mongoCollectionHelper.updateDocument(usersTable, doc, updates);
|
mongoCollectionHelper.updateDocument(usersTable, doc, updates);
|
||||||
} catch (MongoException e) {
|
} catch (MongoException e) {
|
||||||
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||||
@@ -123,7 +126,7 @@ public class MongoDbDatabase extends Database {
|
|||||||
() -> {
|
() -> {
|
||||||
// Insert new player data into the database
|
// Insert new player data into the database
|
||||||
try {
|
try {
|
||||||
Document doc = new Document("uuid", user.getUuid().toString()).append("username", user.getUsername());
|
Document doc = new Document("uuid", user.getUuid()).append("username", user.getName());
|
||||||
mongoCollectionHelper.insertDocument(usersTable, doc);
|
mongoCollectionHelper.insertDocument(usersTable, doc);
|
||||||
} catch (MongoException e) {
|
} catch (MongoException e) {
|
||||||
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||||
@@ -135,12 +138,6 @@ public class MongoDbDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a player by their Minecraft account {@link UUID}
|
|
||||||
*
|
|
||||||
* @param uuid Minecraft account {@link UUID} of the {@link User} to get
|
|
||||||
* @return An optional with the {@link User} present if they exist
|
|
||||||
*/
|
|
||||||
@Blocking
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public Optional<User> getUser(@NotNull UUID uuid) {
|
public Optional<User> getUser(@NotNull UUID uuid) {
|
||||||
@@ -148,8 +145,7 @@ public class MongoDbDatabase extends Database {
|
|||||||
Document filter = new Document("uuid", uuid);
|
Document filter = new Document("uuid", uuid);
|
||||||
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
||||||
if (doc != null) {
|
if (doc != null) {
|
||||||
return Optional.of(new User(UUID.fromString(doc.getString("uuid")),
|
return Optional.of(new User(uuid, doc.getString("username")));
|
||||||
doc.getString("username")));
|
|
||||||
}
|
}
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
} catch (MongoException e) {
|
} catch (MongoException e) {
|
||||||
@@ -158,12 +154,6 @@ public class MongoDbDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a user by their username (<i>case-insensitive</i>)
|
|
||||||
*
|
|
||||||
* @param username Username of the {@link User} to get (<i>case-insensitive</i>)
|
|
||||||
* @return An optional with the {@link User} present if they exist
|
|
||||||
*/
|
|
||||||
@Blocking
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public Optional<User> getUserByName(@NotNull String username) {
|
public Optional<User> getUserByName(@NotNull String username) {
|
||||||
@@ -171,7 +161,7 @@ public class MongoDbDatabase extends Database {
|
|||||||
Document filter = new Document("username", username);
|
Document filter = new Document("username", username);
|
||||||
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
||||||
if (doc != null) {
|
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")));
|
doc.getString("username")));
|
||||||
}
|
}
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
@@ -181,22 +171,34 @@ public class MongoDbDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Get the latest data snapshot for a user.
|
@NotNull
|
||||||
*
|
public List<User> getAllUsers() {
|
||||||
* @param user The user to get data for
|
final List<User> users = Lists.newArrayList();
|
||||||
* @return an optional containing the {@link DataSnapshot}, if it exists, or an empty optional if it does not
|
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
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
||||||
try {
|
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
|
Document sort = new Document("timestamp", -1); // -1 = Descending
|
||||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
||||||
Document doc = iterable.first();
|
Document doc = iterable.first();
|
||||||
if (doc != null) {
|
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 OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
|
||||||
final Binary bin = doc.get("data", Binary.class);
|
final Binary bin = doc.get("data", Binary.class);
|
||||||
final byte[] dataByteArray = bin.getData();
|
final byte[] dataByteArray = bin.getData();
|
||||||
@@ -209,23 +211,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
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
@NotNull
|
@NotNull
|
||||||
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
|
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
|
||||||
try {
|
try {
|
||||||
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
|
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
|
Document sort = new Document("timestamp", -1); // -1 = Descending
|
||||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
||||||
for (Document doc : iterable) {
|
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 OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
|
||||||
final Binary bin = doc.get("data", Binary.class);
|
final Binary bin = doc.get("data", Binary.class);
|
||||||
final byte[] dataByteArray = bin.getData();
|
final byte[] dataByteArray = bin.getData();
|
||||||
@@ -238,18 +234,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
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||||
try {
|
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
|
Document sort = new Document("timestamp", -1); // -1 = Descending
|
||||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
||||||
Document doc = iterable.first();
|
Document doc = iterable.first();
|
||||||
@@ -266,12 +255,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
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
protected void rotateSnapshots(@NotNull User user) {
|
protected void rotateSnapshots(@NotNull User user) {
|
||||||
@@ -281,7 +264,7 @@ public class MongoDbDatabase extends Database {
|
|||||||
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
|
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
|
||||||
if (unpinnedUserData.size() > maxSnapshots) {
|
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
|
Document sort = new Document("timestamp", 1); // 1 = Ascending
|
||||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable)
|
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable)
|
||||||
.find(filter)
|
.find(filter)
|
||||||
@@ -297,17 +280,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
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||||
try {
|
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();
|
Document doc = mongoCollectionHelper.getCollection(userDataTable).find(filter).first();
|
||||||
if (doc == null) {
|
if (doc == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -320,19 +297,11 @@ public class MongoDbDatabase extends Database {
|
|||||||
return false;
|
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
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
|
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
|
||||||
try {
|
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
|
Document sort = new Document("timestamp", 1); // 1 = Ascending
|
||||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable)
|
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable)
|
||||||
.find(filter)
|
.find(filter)
|
||||||
@@ -352,18 +321,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
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||||
try {
|
try {
|
||||||
Document doc = new Document("player_uuid", user.getUuid().toString())
|
Document doc = new Document("player_uuid", user.getUuid())
|
||||||
.append("version_uuid", data.getId().toString())
|
.append("version_uuid", data.getId())
|
||||||
.append("timestamp", data.getTimestamp().toInstant().toEpochMilli())
|
.append("timestamp", data.getTimestamp().toInstant().toEpochMilli())
|
||||||
.append("save_cause", data.getSaveCause().name())
|
.append("save_cause", data.getSaveCause().name())
|
||||||
.append("pinned", data.isPinned())
|
.append("pinned", data.isPinned())
|
||||||
@@ -374,17 +337,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
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||||
try {
|
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(
|
Bson updates = Updates.combine(
|
||||||
Updates.set("save_cause", data.getSaveCause().name()),
|
Updates.set("save_cause", data.getSaveCause().name()),
|
||||||
Updates.set("pinned", data.isPinned()),
|
Updates.set("pinned", data.isPinned()),
|
||||||
@@ -396,10 +353,85 @@ public class MongoDbDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Blocking
|
||||||
* Wipes <b>all</b> {@link User} entries from the database.
|
@Override
|
||||||
* <b>This should only be used when preparing tables for a data migration.</b>
|
public void saveMapData(@NotNull String serverName, int mapId, byte @NotNull [] data) {
|
||||||
*/
|
try {
|
||||||
|
Document doc = new Document("server_name", serverName)
|
||||||
|
.append("map_id", mapId)
|
||||||
|
.append("data", new Binary(data));
|
||||||
|
mongoCollectionHelper.insertDocument(mapDataTable, doc);
|
||||||
|
} catch (MongoException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to write map data to the database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public @Nullable Map.Entry<byte[], Boolean> getMapData(@NotNull String serverName, int mapId) {
|
||||||
|
try {
|
||||||
|
Document filter = new Document("server_name", serverName).append("map_id", mapId);
|
||||||
|
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(mapDataTable).find(filter);
|
||||||
|
Document doc = iterable.first();
|
||||||
|
if (doc != null) {
|
||||||
|
final Binary bin = doc.get("data", Binary.class);
|
||||||
|
return Map.entry(bin.getData(), true);
|
||||||
|
}
|
||||||
|
} catch (MongoException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public @Nullable Map.Entry<String, Integer> getMapBinding(@NotNull String serverName, int mapId) {
|
||||||
|
final Document filter = new Document("to_server_name", serverName).append("to_id", mapId);
|
||||||
|
final FindIterable<Document> iterable = mongoCollectionHelper.getCollection(mapIdsTable).find(filter);
|
||||||
|
final Document doc = iterable.first();
|
||||||
|
if (doc != null) {
|
||||||
|
return new AbstractMap.SimpleImmutableEntry<>(
|
||||||
|
doc.getString("server_name"),
|
||||||
|
doc.getInteger("to_id")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public void setMapBinding(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName, int toMapId) {
|
||||||
|
try {
|
||||||
|
final Document doc = new Document("from_server_name", fromServerName)
|
||||||
|
.append("from_id", fromMapId)
|
||||||
|
.append("to_server_name", toServerName)
|
||||||
|
.append("to_id", toMapId);
|
||||||
|
mongoCollectionHelper.insertDocument(mapIdsTable, doc);
|
||||||
|
} catch (MongoException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to connect map IDs in the database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName) {
|
||||||
|
try {
|
||||||
|
final Document filter = new Document("from_server_name", fromServerName)
|
||||||
|
.append("from_id", fromMapId)
|
||||||
|
.append("to_server_name", toServerName);
|
||||||
|
final FindIterable<Document> iterable = mongoCollectionHelper.getCollection(mapIdsTable).find(filter);
|
||||||
|
|
||||||
|
final Document doc = iterable.first();
|
||||||
|
if (doc != null) {
|
||||||
|
return doc.getInteger("to_id");
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
} catch (MongoException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to get new map id from the database", e);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Blocking
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public void wipeDatabase() {
|
public void wipeDatabase() {
|
||||||
@@ -410,9 +442,6 @@ public class MongoDbDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the database connection
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public void terminate() {
|
public void terminate() {
|
||||||
if (mongoConnectionHandler != null) {
|
if (mongoConnectionHandler != null) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import net.william278.husksync.data.DataSnapshot;
|
|||||||
import net.william278.husksync.user.User;
|
import net.william278.husksync.user.User;
|
||||||
import org.jetbrains.annotations.Blocking;
|
import org.jetbrains.annotations.Blocking;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -115,6 +116,9 @@ public class MySqlDatabase extends Database {
|
|||||||
);
|
);
|
||||||
dataSource.setDataSourceProperties(properties);
|
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
|
// Prepare database schema; make tables if they don't exist
|
||||||
try (Connection connection = dataSource.getConnection()) {
|
try (Connection connection = dataSource.getConnection()) {
|
||||||
final String[] databaseSchema = getSchemaStatements(String.format("database/%s_schema.sql", flavor));
|
final String[] databaseSchema = getSchemaStatements(String.format("database/%s_schema.sql", flavor));
|
||||||
@@ -137,7 +141,7 @@ public class MySqlDatabase extends Database {
|
|||||||
public void ensureUser(@NotNull User user) {
|
public void ensureUser(@NotNull User user) {
|
||||||
getUser(user.getUuid()).ifPresentOrElse(
|
getUser(user.getUuid()).ifPresentOrElse(
|
||||||
existingUser -> {
|
existingUser -> {
|
||||||
if (!existingUser.getUsername().equals(user.getUsername())) {
|
if (!existingUser.getName().equals(user.getName())) {
|
||||||
// Update a user's name if it has changed in the database
|
// Update a user's name if it has changed in the database
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
@@ -145,11 +149,12 @@ public class MySqlDatabase extends Database {
|
|||||||
SET `username`=?
|
SET `username`=?
|
||||||
WHERE `uuid`=?"""))) {
|
WHERE `uuid`=?"""))) {
|
||||||
|
|
||||||
statement.setString(1, user.getUsername());
|
statement.setString(1, user.getName());
|
||||||
statement.setString(2, existingUser.getUuid().toString());
|
statement.setString(2, existingUser.getUuid().toString());
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
plugin.log(Level.INFO, "Updated " + user.getUsername() + "'s name in the database (" + existingUser.getUsername() + " -> " + user.getUsername() + ")");
|
plugin.log(Level.INFO, "Updated " + user.getName() + "'s name in the database ("
|
||||||
|
+ existingUser.getName() + " -> " + user.getName() + ")");
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e);
|
plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e);
|
||||||
}
|
}
|
||||||
@@ -163,7 +168,7 @@ public class MySqlDatabase extends Database {
|
|||||||
VALUES (?,?);"""))) {
|
VALUES (?,?);"""))) {
|
||||||
|
|
||||||
statement.setString(1, user.getUuid().toString());
|
statement.setString(1, user.getUuid().toString());
|
||||||
statement.setString(2, user.getUsername());
|
statement.setString(2, user.getName());
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
@@ -218,6 +223,27 @@ public class MySqlDatabase extends Database {
|
|||||||
return Optional.empty();
|
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
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
||||||
@@ -409,6 +435,120 @@ public class MySqlDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public void saveMapData(@NotNull String serverName, int mapId, byte @NotNull [] data) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
INSERT INTO `%map_data_table%`
|
||||||
|
(`server_name`,`map_id`,`data`)
|
||||||
|
VALUES (?,?,?);"""))) {
|
||||||
|
statement.setString(1, serverName);
|
||||||
|
statement.setInt(2, mapId);
|
||||||
|
statement.setBlob(3, new ByteArrayInputStream(data));
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to write map data to the database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public @Nullable Map.Entry<byte[], Boolean> getMapData(@NotNull String serverName, int mapId) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
SELECT `data`
|
||||||
|
FROM `%map_data_table%`
|
||||||
|
WHERE `server_name`=? AND `map_id`=?
|
||||||
|
LIMIT 1;"""))) {
|
||||||
|
statement.setString(1, serverName);
|
||||||
|
statement.setInt(2, mapId);
|
||||||
|
|
||||||
|
final ResultSet resultSet = statement.executeQuery();
|
||||||
|
if (resultSet.next()) {
|
||||||
|
final Blob blob = resultSet.getBlob("data");
|
||||||
|
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||||
|
blob.free();
|
||||||
|
return Map.entry(dataByteArray, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public @Nullable Map.Entry<String, Integer> getMapBinding(@NotNull String serverName, int mapId) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
SELECT `from_server_name`, `from_id`
|
||||||
|
FROM `%map_ids_table%`
|
||||||
|
WHERE `to_server_name`=? AND `to_id`=?
|
||||||
|
LIMIT 1;
|
||||||
|
"""))) {
|
||||||
|
statement.setString(1, serverName);
|
||||||
|
statement.setInt(2, mapId);
|
||||||
|
|
||||||
|
final ResultSet resultSet = statement.executeQuery();
|
||||||
|
if (resultSet.next()) {
|
||||||
|
return new AbstractMap.SimpleImmutableEntry<>(
|
||||||
|
resultSet.getString("from_server_name"),
|
||||||
|
resultSet.getInt("from_id")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public void setMapBinding(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName, int toMapId) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
INSERT INTO `%map_ids_table%`
|
||||||
|
(`from_server_name`,`from_id`,`to_server_name`,`to_id`)
|
||||||
|
VALUES (?,?,?,?);"""))) {
|
||||||
|
statement.setString(1, fromServerName);
|
||||||
|
statement.setInt(2, fromMapId);
|
||||||
|
statement.setString(3, toServerName);
|
||||||
|
statement.setInt(4, toMapId);
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to connect map IDs in the database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
SELECT `to_id`
|
||||||
|
FROM `%map_ids_table%`
|
||||||
|
WHERE `from_server_name`=? AND `from_id`=? AND `to_server_name`=?
|
||||||
|
LIMIT 1;"""))) {
|
||||||
|
statement.setString(1, fromServerName);
|
||||||
|
statement.setInt(2, fromMapId);
|
||||||
|
statement.setString(3, toServerName);
|
||||||
|
|
||||||
|
final ResultSet resultSet = statement.executeQuery();
|
||||||
|
if (resultSet.next()) {
|
||||||
|
return resultSet.getInt("to_id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to get new map id from the database", e);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void wipeDatabase() {
|
public void wipeDatabase() {
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import net.william278.husksync.data.DataSnapshot;
|
|||||||
import net.william278.husksync.user.User;
|
import net.william278.husksync.user.User;
|
||||||
import org.jetbrains.annotations.Blocking;
|
import org.jetbrains.annotations.Blocking;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
@@ -51,12 +52,6 @@ public class PostgresDatabase extends Database {
|
|||||||
this.driverClass = "org.postgresql.Driver";
|
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
|
@Blocking
|
||||||
@NotNull
|
@NotNull
|
||||||
private Connection getConnection() throws SQLException {
|
private Connection getConnection() throws SQLException {
|
||||||
@@ -114,6 +109,9 @@ public class PostgresDatabase extends Database {
|
|||||||
);
|
);
|
||||||
dataSource.setDataSourceProperties(properties);
|
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
|
// Prepare database schema; make tables if they don't exist
|
||||||
try (Connection connection = dataSource.getConnection()) {
|
try (Connection connection = dataSource.getConnection()) {
|
||||||
final String[] databaseSchema = getSchemaStatements(String.format("database/%s_schema.sql", flavor));
|
final String[] databaseSchema = getSchemaStatements(String.format("database/%s_schema.sql", flavor));
|
||||||
@@ -136,19 +134,19 @@ public class PostgresDatabase extends Database {
|
|||||||
public void ensureUser(@NotNull User user) {
|
public void ensureUser(@NotNull User user) {
|
||||||
getUser(user.getUuid()).ifPresentOrElse(
|
getUser(user.getUuid()).ifPresentOrElse(
|
||||||
existingUser -> {
|
existingUser -> {
|
||||||
if (!existingUser.getUsername().equals(user.getUsername())) {
|
if (!existingUser.getName().equals(user.getName())) {
|
||||||
// Update a user's name if it has changed in the database
|
// Update a user's name if it has changed in the database
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
UPDATE "%users_table%"
|
UPDATE %users_table%
|
||||||
SET "username"=?
|
SET username=?
|
||||||
WHERE "uuid"=?"""))) {
|
WHERE uuid=?;"""))) {
|
||||||
|
|
||||||
statement.setString(1, user.getUsername());
|
statement.setString(1, user.getName());
|
||||||
statement.setObject(2, existingUser.getUuid());
|
statement.setObject(2, existingUser.getUuid());
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
plugin.log(Level.INFO, "Updated " + user.getUsername() + "'s name in the database (" + existingUser.getUsername() + " -> " + user.getUsername() + ")");
|
plugin.log(Level.INFO, "Updated " + user.getName() + "'s name in the database (" + existingUser.getName() + " -> " + user.getName() + ")");
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e);
|
plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e);
|
||||||
}
|
}
|
||||||
@@ -158,11 +156,11 @@ public class PostgresDatabase extends Database {
|
|||||||
// Insert new player data into the database
|
// Insert new player data into the database
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
INSERT INTO "%users_table%" ("uuid","username")
|
INSERT INTO %users_table% (uuid,username)
|
||||||
VALUES (?,?);"""))) {
|
VALUES (?,?);"""))) {
|
||||||
|
|
||||||
statement.setObject(1, user.getUuid());
|
statement.setObject(1, user.getUuid());
|
||||||
statement.setString(2, user.getUsername());
|
statement.setString(2, user.getName());
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
@@ -177,9 +175,9 @@ public class PostgresDatabase extends Database {
|
|||||||
public Optional<User> getUser(@NotNull UUID uuid) {
|
public Optional<User> getUser(@NotNull UUID uuid) {
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
SELECT "uuid", "username"
|
SELECT uuid, username
|
||||||
FROM "%users_table%"
|
FROM %users_table%
|
||||||
WHERE "uuid"=?"""))) {
|
WHERE uuid=?;"""))) {
|
||||||
|
|
||||||
statement.setObject(1, uuid);
|
statement.setObject(1, uuid);
|
||||||
|
|
||||||
@@ -200,9 +198,9 @@ public class PostgresDatabase extends Database {
|
|||||||
public Optional<User> getUserByName(@NotNull String username) {
|
public Optional<User> getUserByName(@NotNull String username) {
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
SELECT "uuid", "username"
|
SELECT uuid, username
|
||||||
FROM "%users_table%"
|
FROM %users_table%
|
||||||
WHERE "username"=?"""))) {
|
WHERE username=?;"""))) {
|
||||||
statement.setString(1, username);
|
statement.setString(1, username);
|
||||||
|
|
||||||
final ResultSet resultSet = statement.executeQuery();
|
final ResultSet resultSet = statement.executeQuery();
|
||||||
@@ -217,15 +215,37 @@ public class PostgresDatabase extends Database {
|
|||||||
return Optional.empty();
|
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
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
SELECT "version_uuid", "timestamp", "data"
|
SELECT version_uuid, timestamp, data
|
||||||
FROM "%user_data_table%"
|
FROM %user_data_table%
|
||||||
WHERE "player_uuid"=?
|
WHERE player_uuid=?
|
||||||
ORDER BY "timestamp" DESC
|
ORDER BY timestamp DESC
|
||||||
LIMIT 1;"""))) {
|
LIMIT 1;"""))) {
|
||||||
statement.setObject(1, user.getUuid());
|
statement.setObject(1, user.getUuid());
|
||||||
final ResultSet resultSet = statement.executeQuery();
|
final ResultSet resultSet = statement.executeQuery();
|
||||||
@@ -251,10 +271,10 @@ public class PostgresDatabase extends Database {
|
|||||||
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
|
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
SELECT "version_uuid", "timestamp", "data"
|
SELECT version_uuid, timestamp, data
|
||||||
FROM "%user_data_table%"
|
FROM %user_data_table%
|
||||||
WHERE "player_uuid"=?
|
WHERE player_uuid=?
|
||||||
ORDER BY "timestamp" DESC;"""))) {
|
ORDER BY timestamp DESC;"""))) {
|
||||||
statement.setObject(1, user.getUuid());
|
statement.setObject(1, user.getUuid());
|
||||||
final ResultSet resultSet = statement.executeQuery();
|
final ResultSet resultSet = statement.executeQuery();
|
||||||
while (resultSet.next()) {
|
while (resultSet.next()) {
|
||||||
@@ -278,10 +298,10 @@ public class PostgresDatabase extends Database {
|
|||||||
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
SELECT "version_uuid", "timestamp", "data"
|
SELECT version_uuid, timestamp, data
|
||||||
FROM "%user_data_table%"
|
FROM %user_data_table%
|
||||||
WHERE "player_uuid"=? AND "version_uuid"=?
|
WHERE player_uuid=? AND version_uuid=?
|
||||||
ORDER BY "timestamp" DESC
|
ORDER BY timestamp DESC
|
||||||
LIMIT 1;"""))) {
|
LIMIT 1;"""))) {
|
||||||
statement.setObject(1, user.getUuid());
|
statement.setObject(1, user.getUuid());
|
||||||
statement.setObject(2, versionUuid);
|
statement.setObject(2, versionUuid);
|
||||||
@@ -309,11 +329,16 @@ public class PostgresDatabase extends Database {
|
|||||||
if (unpinnedUserData.size() > maxSnapshots) {
|
if (unpinnedUserData.size() > maxSnapshots) {
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
DELETE FROM "%user_data_table%"
|
WITH cte AS (
|
||||||
WHERE "player_uuid"=?
|
SELECT version_uuid
|
||||||
AND "pinned" = FALSE
|
FROM %user_data_table%
|
||||||
ORDER BY "timestamp" ASC
|
WHERE player_uuid=?
|
||||||
LIMIT %entry_count%;""".replace("%entry_count%",
|
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))))) {
|
Integer.toString(unpinnedUserData.size() - maxSnapshots))))) {
|
||||||
statement.setObject(1, user.getUuid());
|
statement.setObject(1, user.getUuid());
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
@@ -329,11 +354,10 @@ public class PostgresDatabase extends Database {
|
|||||||
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
DELETE FROM "%user_data_table%"
|
DELETE FROM %user_data_table%
|
||||||
WHERE "player_uuid"=? AND "version_uuid"=?
|
WHERE player_uuid=? AND version_uuid=?;"""))) {
|
||||||
LIMIT 1;"""))) {
|
|
||||||
statement.setObject(1, user.getUuid());
|
statement.setObject(1, user.getUuid());
|
||||||
statement.setString(2, versionUuid.toString());
|
statement.setObject(2, versionUuid);
|
||||||
return statement.executeUpdate() > 0;
|
return statement.executeUpdate() > 0;
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
@@ -347,12 +371,12 @@ public class PostgresDatabase extends Database {
|
|||||||
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
|
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
DELETE FROM "%user_data_table%"
|
DELETE FROM %user_data_table%
|
||||||
WHERE "player_uuid"=? AND "timestamp" = (
|
WHERE player_uuid=? AND timestamp = (
|
||||||
SELECT "timestamp"
|
SELECT timestamp
|
||||||
FROM "%user_data_table%"
|
FROM %user_data_table%
|
||||||
WHERE "player_uuid"=? AND "timestamp" > ? AND "pinned" = FALSE
|
WHERE player_uuid=? AND timestamp > ? AND pinned=FALSE
|
||||||
ORDER BY "timestamp" ASC
|
ORDER BY timestamp ASC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
);"""))) {
|
);"""))) {
|
||||||
statement.setObject(1, user.getUuid());
|
statement.setObject(1, user.getUuid());
|
||||||
@@ -370,8 +394,8 @@ public class PostgresDatabase extends Database {
|
|||||||
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
INSERT INTO "%user_data_table%"
|
INSERT INTO %user_data_table%
|
||||||
("player_uuid","version_uuid","timestamp","save_cause","pinned","data")
|
(player_uuid,version_uuid,timestamp,save_cause,pinned,data)
|
||||||
VALUES (?,?,?,?,?,?);"""))) {
|
VALUES (?,?,?,?,?,?);"""))) {
|
||||||
statement.setObject(1, user.getUuid());
|
statement.setObject(1, user.getUuid());
|
||||||
statement.setObject(2, data.getId());
|
statement.setObject(2, data.getId());
|
||||||
@@ -391,10 +415,10 @@ public class PostgresDatabase extends Database {
|
|||||||
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
UPDATE "%user_data_table%"
|
UPDATE %user_data_table%
|
||||||
SET "save_cause"=?,"pinned"=?,"data"=?
|
SET save_cause=?,pinned=?,data=?
|
||||||
WHERE "player_uuid"=? AND "version_uuid"=?
|
WHERE player_uuid=? AND version_uuid=?;
|
||||||
LIMIT 1;"""))) {
|
"""))) {
|
||||||
statement.setString(1, data.getSaveCause().name());
|
statement.setString(1, data.getSaveCause().name());
|
||||||
statement.setBoolean(2, data.isPinned());
|
statement.setBoolean(2, data.isPinned());
|
||||||
statement.setBytes(3, data.asBytes(plugin));
|
statement.setBytes(3, data.asBytes(plugin));
|
||||||
@@ -407,11 +431,123 @@ public class PostgresDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public void saveMapData(@NotNull String serverName, int mapId, byte @NotNull [] data) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
INSERT INTO %map_data_table%
|
||||||
|
(server_name,map_id,data)
|
||||||
|
VALUES (?,?,?);"""))) {
|
||||||
|
statement.setString(1, serverName);
|
||||||
|
statement.setInt(2, mapId);
|
||||||
|
statement.setBytes(3, data);
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to write map data to the database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public @Nullable Map.Entry<byte[], Boolean> getMapData(@NotNull String serverName, int mapId) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
SELECT data
|
||||||
|
FROM %map_data_table%
|
||||||
|
WHERE server_name=? AND map_id=?
|
||||||
|
LIMIT 1;"""))) {
|
||||||
|
statement.setString(1, serverName);
|
||||||
|
statement.setInt(2, mapId);
|
||||||
|
final ResultSet resultSet = statement.executeQuery();
|
||||||
|
if (resultSet.next()) {
|
||||||
|
final byte[] data = resultSet.getBytes("data");
|
||||||
|
return new AbstractMap.SimpleImmutableEntry<>(data, true);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public @Nullable Map.Entry<String, Integer> getMapBinding(@NotNull String serverName, int mapId) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
SELECT from_server_name, from_id
|
||||||
|
FROM %map_ids_table%
|
||||||
|
WHERE to_server_name=? AND to_id=?
|
||||||
|
LIMIT 1;
|
||||||
|
"""))) {
|
||||||
|
statement.setString(1, serverName);
|
||||||
|
statement.setInt(2, mapId);
|
||||||
|
|
||||||
|
final ResultSet resultSet = statement.executeQuery();
|
||||||
|
if (resultSet.next()) {
|
||||||
|
return new AbstractMap.SimpleImmutableEntry<>(
|
||||||
|
resultSet.getString("from_server_name"),
|
||||||
|
resultSet.getInt("from_id")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public void setMapBinding(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName, int toMapId) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
INSERT INTO %map_ids_table%
|
||||||
|
(from_server_name,from_id,to_server_name,to_id)
|
||||||
|
VALUES (?,?,?,?);"""))) {
|
||||||
|
statement.setString(1, fromServerName);
|
||||||
|
statement.setInt(2, fromMapId);
|
||||||
|
statement.setString(3, toServerName);
|
||||||
|
statement.setInt(4, toMapId);
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to connect map IDs in the database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
SELECT to_id
|
||||||
|
FROM %map_ids_table%
|
||||||
|
WHERE from_server_name=? AND from_id=? AND to_server_name=?
|
||||||
|
LIMIT 1;"""))) {
|
||||||
|
statement.setString(1, fromServerName);
|
||||||
|
statement.setInt(2, fromMapId);
|
||||||
|
statement.setString(3, toServerName);
|
||||||
|
|
||||||
|
final ResultSet resultSet = statement.executeQuery();
|
||||||
|
if (resultSet.next()) {
|
||||||
|
return resultSet.getInt("to_id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to get new map id from the database", e);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void wipeDatabase() {
|
public void wipeDatabase() {
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (Statement statement = connection.createStatement()) {
|
try (Statement statement = connection.createStatement()) {
|
||||||
statement.executeUpdate(formatStatementTables("DELETE FROM \"%user_data_table%\";"));
|
statement.executeUpdate(formatStatementTables("DELETE FROM %user_data_table%;"));
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
plugin.log(Level.SEVERE, "Failed to wipe the database", e);
|
plugin.log(Level.SEVERE, "Failed to wipe the database", e);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public class MongoCollectionHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the collection helper
|
* Initialize the collection helper
|
||||||
|
*
|
||||||
* @param database Instance of {@link MongoConnectionHandler}
|
* @param database Instance of {@link MongoConnectionHandler}
|
||||||
*/
|
*/
|
||||||
public MongoCollectionHelper(@NotNull MongoConnectionHandler database) {
|
public MongoCollectionHelper(@NotNull MongoConnectionHandler database) {
|
||||||
@@ -37,6 +38,7 @@ public class MongoCollectionHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a collection
|
* Create a collection
|
||||||
|
*
|
||||||
* @param collectionName the collection name
|
* @param collectionName the collection name
|
||||||
*/
|
*/
|
||||||
public void createCollection(@NotNull String collectionName) {
|
public void createCollection(@NotNull String collectionName) {
|
||||||
@@ -45,6 +47,7 @@ public class MongoCollectionHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a collection
|
* Delete a collection
|
||||||
|
*
|
||||||
* @param collectionName the collection name
|
* @param collectionName the collection name
|
||||||
*/
|
*/
|
||||||
public void deleteCollection(@NotNull String collectionName) {
|
public void deleteCollection(@NotNull String collectionName) {
|
||||||
@@ -53,6 +56,7 @@ public class MongoCollectionHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a collection
|
* Get a collection
|
||||||
|
*
|
||||||
* @param collectionName the collection name
|
* @param collectionName the collection name
|
||||||
* @return MongoCollection<Document>
|
* @return MongoCollection<Document>
|
||||||
*/
|
*/
|
||||||
@@ -62,6 +66,7 @@ public class MongoCollectionHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a document to a collection
|
* Add a document to a collection
|
||||||
|
*
|
||||||
* @param collectionName collection to add to
|
* @param collectionName collection to add to
|
||||||
* @param document Document to add
|
* @param document Document to add
|
||||||
*/
|
*/
|
||||||
@@ -72,6 +77,7 @@ public class MongoCollectionHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a document
|
* Update a document
|
||||||
|
*
|
||||||
* @param collectionName collection the document is in
|
* @param collectionName collection the document is in
|
||||||
* @param document filter of document
|
* @param document filter of document
|
||||||
* @param updates Bson of updates
|
* @param updates Bson of updates
|
||||||
@@ -83,6 +89,7 @@ public class MongoCollectionHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a document
|
* Delete a document
|
||||||
|
*
|
||||||
* @param collectionName collection the document is in
|
* @param collectionName collection the document is in
|
||||||
* @param document filter to remove
|
* @param document filter to remove
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public class MongoConnectionHandler {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate a connection to a Mongo Server
|
* Initiate a connection to a Mongo Server
|
||||||
|
*
|
||||||
* @param uri The connection string
|
* @param uri The connection string
|
||||||
*/
|
*/
|
||||||
public MongoConnectionHandler(@NotNull ConnectionString uri, @NotNull String databaseName) {
|
public MongoConnectionHandler(@NotNull ConnectionString uri, @NotNull String databaseName) {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
package net.william278.husksync.event;
|
package net.william278.husksync.event;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public interface Cancellable extends Event {
|
public interface Cancellable extends Event {
|
||||||
|
|
||||||
default boolean isCancelled() {
|
default boolean isCancelled() {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public interface PreSyncEvent extends PlayerEvent {
|
public interface PreSyncEvent extends PlayerEvent, Cancellable {
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
DataSnapshot.Packed getData();
|
DataSnapshot.Packed getData();
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public abstract class EventListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
plugin.lockPlayer(user.getUuid());
|
plugin.lockPlayer(user.getUuid());
|
||||||
plugin.getDataSyncer().setUserData(user);
|
plugin.getDataSyncer().syncApplyUserData(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,7 +66,7 @@ public abstract class EventListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
plugin.lockPlayer(user.getUuid());
|
plugin.lockPlayer(user.getUuid());
|
||||||
plugin.getDataSyncer().saveUserData(user);
|
plugin.getDataSyncer().syncSaveUserData(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,8 +80,8 @@ public abstract class EventListener {
|
|||||||
}
|
}
|
||||||
usersInWorld.stream()
|
usersInWorld.stream()
|
||||||
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
|
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
|
||||||
.forEach(user -> plugin.getDataSyncer().saveData(
|
.forEach(user -> plugin.getDataSyncer().saveCurrentUserData(
|
||||||
user, user.createSnapshot(DataSnapshot.SaveCause.WORLD_SAVE)
|
user, DataSnapshot.SaveCause.WORLD_SAVE
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,8 +98,9 @@ public abstract class EventListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We don't persist this to Redis for syncing, as this snapshot is from a state they won't be in post-respawn
|
||||||
final DataSnapshot.Packed snapshot = user.createSnapshot(DataSnapshot.SaveCause.DEATH);
|
final DataSnapshot.Packed snapshot = user.createSnapshot(DataSnapshot.SaveCause.DEATH);
|
||||||
snapshot.edit(plugin, (data -> data.getInventory().ifPresent(inventory -> inventory.setContents(items))));
|
snapshot.edit(plugin, (data -> data.getInventory().ifPresent(inv -> inv.setContents(items))));
|
||||||
plugin.getDataSyncer().saveData(user, snapshot);
|
plugin.getDataSyncer().saveData(user, snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,16 +109,12 @@ public abstract class EventListener {
|
|||||||
* Handle the plugin disabling
|
* Handle the plugin disabling
|
||||||
*/
|
*/
|
||||||
public void handlePluginDisable() {
|
public void handlePluginDisable() {
|
||||||
// Save for all online players
|
// Save for all online players.
|
||||||
plugin.getOnlineUsers().stream()
|
plugin.getOnlineUsers().stream()
|
||||||
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
|
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
|
||||||
.forEach(user -> {
|
.forEach(user -> {
|
||||||
plugin.lockPlayer(user.getUuid());
|
plugin.lockPlayer(user.getUuid());
|
||||||
plugin.getDataSyncer().saveData(
|
plugin.getDataSyncer().saveCurrentUserData(user, DataSnapshot.SaveCause.SERVER_SHUTDOWN);
|
||||||
user,
|
|
||||||
user.createSnapshot(DataSnapshot.SaveCause.SERVER_SHUTDOWN),
|
|
||||||
(saved, data) -> plugin.getRedisManager().clearUserData(saved)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close outstanding connections
|
// Close outstanding connections
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||||
|
*
|
||||||
|
* Copyright (c) William278 <will27528@gmail.com>
|
||||||
|
* Copyright (c) contributors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.william278.husksync.maps;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import net.william278.husksync.adapter.Adaptable;
|
||||||
|
import net.william278.mapdataapi.MapData;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class AdaptableMapData implements Adaptable {
|
||||||
|
|
||||||
|
@SerializedName("data")
|
||||||
|
private final byte[] data;
|
||||||
|
|
||||||
|
public AdaptableMapData(@NotNull MapData data) {
|
||||||
|
this(data.toBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public MapData getData(int dataVersion) throws IOException {
|
||||||
|
return MapData.fromByteArray(dataVersion, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -27,7 +27,10 @@ public enum RedisKeyType {
|
|||||||
|
|
||||||
LATEST_SNAPSHOT,
|
LATEST_SNAPSHOT,
|
||||||
SERVER_SWITCH,
|
SERVER_SWITCH,
|
||||||
DATA_CHECKOUT;
|
DATA_CHECKOUT,
|
||||||
|
MAP_ID,
|
||||||
|
MAP_ID_REVERSED,
|
||||||
|
MAP_DATA;
|
||||||
|
|
||||||
public static final int TTL_1_YEAR = 60 * 60 * 24 * 7 * 52; // 1 year
|
public static final int TTL_1_YEAR = 60 * 60 * 24 * 7 * 52; // 1 year
|
||||||
public static final int TTL_10_SECONDS = 10; // 10 seconds
|
public static final int TTL_10_SECONDS = 10; // 10 seconds
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import net.william278.husksync.data.DataSnapshot;
|
|||||||
import net.william278.husksync.user.User;
|
import net.william278.husksync.user.User;
|
||||||
import org.jetbrains.annotations.Blocking;
|
import org.jetbrains.annotations.Blocking;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
import redis.clients.jedis.*;
|
import redis.clients.jedis.*;
|
||||||
import redis.clients.jedis.exceptions.JedisException;
|
import redis.clients.jedis.exceptions.JedisException;
|
||||||
import redis.clients.jedis.util.Pool;
|
import redis.clients.jedis.util.Pool;
|
||||||
@@ -159,6 +160,7 @@ public class RedisManager extends JedisPubSub {
|
|||||||
switch (messageType) {
|
switch (messageType) {
|
||||||
case UPDATE_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
|
case UPDATE_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
|
||||||
user -> {
|
user -> {
|
||||||
|
plugin.lockPlayer(user.getUuid());
|
||||||
try {
|
try {
|
||||||
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload());
|
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload());
|
||||||
user.applySnapshot(data, DataSnapshot.UpdateCause.UPDATED);
|
user.applySnapshot(data, DataSnapshot.UpdateCause.UPDATED);
|
||||||
@@ -216,15 +218,17 @@ public class RedisManager extends JedisPubSub {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<Optional<DataSnapshot.Packed>> getUserData(@NotNull UUID requestId, @NotNull User user) {
|
public CompletableFuture<Optional<DataSnapshot.Packed>> getOnlineUserData(@NotNull UUID requestId, @NotNull User user,
|
||||||
|
@NotNull DataSnapshot.SaveCause saveCause) {
|
||||||
return plugin.getOnlineUser(user.getUuid())
|
return plugin.getOnlineUser(user.getUuid())
|
||||||
.map(online -> CompletableFuture.completedFuture(
|
.map(online -> CompletableFuture.completedFuture(
|
||||||
Optional.of(online.createSnapshot(DataSnapshot.SaveCause.API)))
|
Optional.of(online.createSnapshot(saveCause)))
|
||||||
)
|
)
|
||||||
.orElse(this.requestData(requestId, user));
|
.orElse(this.getNetworkedUserData(requestId, user));
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompletableFuture<Optional<DataSnapshot.Packed>> requestData(@NotNull UUID requestId, @NotNull User user) {
|
// Request a user's dat x-server
|
||||||
|
private CompletableFuture<Optional<DataSnapshot.Packed>> getNetworkedUserData(@NotNull UUID requestId, @NotNull User user) {
|
||||||
final CompletableFuture<Optional<DataSnapshot.Packed>> future = new CompletableFuture<>();
|
final CompletableFuture<Optional<DataSnapshot.Packed>> future = new CompletableFuture<>();
|
||||||
pendingRequests.put(requestId, future);
|
pendingRequests.put(requestId, future);
|
||||||
plugin.runAsync(() -> {
|
plugin.runAsync(() -> {
|
||||||
@@ -245,22 +249,16 @@ public class RedisManager extends JedisPubSub {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Set a user's data to Redis
|
||||||
* Set a user's data to Redis
|
|
||||||
*
|
|
||||||
* @param user the user to set data for
|
|
||||||
* @param data the user's data to set
|
|
||||||
* @param timeToLive The time to cache the data for
|
|
||||||
*/
|
|
||||||
@Blocking
|
@Blocking
|
||||||
public void setUserData(@NotNull User user, @NotNull DataSnapshot.Packed data, int timeToLive) {
|
public void setUserData(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||||
try (Jedis jedis = jedisPool.getResource()) {
|
try (Jedis jedis = jedisPool.getResource()) {
|
||||||
jedis.setex(
|
jedis.setex(
|
||||||
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId),
|
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId),
|
||||||
timeToLive,
|
RedisKeyType.TTL_1_YEAR,
|
||||||
data.asBytes(plugin)
|
data.asBytes(plugin)
|
||||||
);
|
);
|
||||||
plugin.debug(String.format("[%s] Set %s key on Redis", user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
|
plugin.debug(String.format("[%s] Set %s key on Redis", user.getName(), RedisKeyType.LATEST_SNAPSHOT));
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
plugin.log(Level.SEVERE, "An exception occurred setting user data on Redis", e);
|
plugin.log(Level.SEVERE, "An exception occurred setting user data on Redis", e);
|
||||||
}
|
}
|
||||||
@@ -272,7 +270,7 @@ public class RedisManager extends JedisPubSub {
|
|||||||
jedis.del(
|
jedis.del(
|
||||||
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId)
|
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId)
|
||||||
);
|
);
|
||||||
plugin.debug(String.format("[%s] Cleared %s on Redis", user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
|
plugin.debug(String.format("[%s] Cleared %s on Redis", user.getName(), RedisKeyType.LATEST_SNAPSHOT));
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
plugin.log(Level.SEVERE, "An exception occurred clearing user data on Redis", e);
|
plugin.log(Level.SEVERE, "An exception occurred clearing user data on Redis", e);
|
||||||
}
|
}
|
||||||
@@ -281,16 +279,21 @@ public class RedisManager extends JedisPubSub {
|
|||||||
@Blocking
|
@Blocking
|
||||||
public void setUserCheckedOut(@NotNull User user, boolean checkedOut) {
|
public void setUserCheckedOut(@NotNull User user, boolean checkedOut) {
|
||||||
try (Jedis jedis = jedisPool.getResource()) {
|
try (Jedis jedis = jedisPool.getResource()) {
|
||||||
|
final String key = getKeyString(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId);
|
||||||
if (checkedOut) {
|
if (checkedOut) {
|
||||||
jedis.set(
|
jedis.set(
|
||||||
getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId),
|
key.getBytes(StandardCharsets.UTF_8),
|
||||||
plugin.getServerName().getBytes(StandardCharsets.UTF_8)
|
plugin.getServerName().getBytes(StandardCharsets.UTF_8)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
jedis.del(getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId));
|
if (jedis.del(key.getBytes(StandardCharsets.UTF_8)) == 0) {
|
||||||
|
plugin.debug(String.format("[%s] %s key not set on Redis when attempting removal (%s)",
|
||||||
|
user.getName(), RedisKeyType.DATA_CHECKOUT, key));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
plugin.debug(String.format("[%s] %s %s key to/from Redis", user.getUsername(),
|
}
|
||||||
checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT));
|
plugin.debug(String.format("[%s] %s %s key %s Redis (%s)", user.getName(),
|
||||||
|
checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT, checkedOut ? "to" : "from", key));
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
plugin.log(Level.SEVERE, "An exception occurred setting checkout to", e);
|
plugin.log(Level.SEVERE, "An exception occurred setting checkout to", e);
|
||||||
}
|
}
|
||||||
@@ -304,13 +307,13 @@ public class RedisManager extends JedisPubSub {
|
|||||||
if (readData != null) {
|
if (readData != null) {
|
||||||
final String checkoutServer = new String(readData, StandardCharsets.UTF_8);
|
final String checkoutServer = new String(readData, StandardCharsets.UTF_8);
|
||||||
plugin.debug(String.format("[%s] Waiting for %s %s key to be unset on Redis",
|
plugin.debug(String.format("[%s] Waiting for %s %s key to be unset on Redis",
|
||||||
user.getUsername(), checkoutServer, RedisKeyType.DATA_CHECKOUT));
|
user.getName(), checkoutServer, RedisKeyType.DATA_CHECKOUT));
|
||||||
return Optional.of(checkoutServer);
|
return Optional.of(checkoutServer);
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
plugin.log(Level.SEVERE, "An exception occurred getting a user's checkout key from Redis", e);
|
plugin.log(Level.SEVERE, "An exception occurred getting a user's checkout key from Redis", e);
|
||||||
}
|
}
|
||||||
plugin.debug(String.format("[%s] %s key not set on Redis", user.getUsername(),
|
plugin.debug(String.format("[%s] %s key not set on Redis", user.getName(),
|
||||||
RedisKeyType.DATA_CHECKOUT));
|
RedisKeyType.DATA_CHECKOUT));
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
@@ -348,7 +351,7 @@ public class RedisManager extends JedisPubSub {
|
|||||||
new byte[0]
|
new byte[0]
|
||||||
);
|
);
|
||||||
plugin.debug(String.format("[%s] Set %s key to Redis",
|
plugin.debug(String.format("[%s] Set %s key to Redis",
|
||||||
user.getUsername(), RedisKeyType.SERVER_SWITCH));
|
user.getName(), RedisKeyType.SERVER_SWITCH));
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
plugin.log(Level.SEVERE, "An exception occurred setting a user's server switch key from Redis", e);
|
plugin.log(Level.SEVERE, "An exception occurred setting a user's server switch key from Redis", e);
|
||||||
}
|
}
|
||||||
@@ -367,11 +370,11 @@ public class RedisManager extends JedisPubSub {
|
|||||||
final byte[] dataByteArray = jedis.get(key);
|
final byte[] dataByteArray = jedis.get(key);
|
||||||
if (dataByteArray == null) {
|
if (dataByteArray == null) {
|
||||||
plugin.debug(String.format("[%s] Waiting for %s key from Redis",
|
plugin.debug(String.format("[%s] Waiting for %s key from Redis",
|
||||||
user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
|
user.getName(), RedisKeyType.LATEST_SNAPSHOT));
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
plugin.debug(String.format("[%s] Read %s key from Redis",
|
plugin.debug(String.format("[%s] Read %s key from Redis",
|
||||||
user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
|
user.getName(), RedisKeyType.LATEST_SNAPSHOT));
|
||||||
|
|
||||||
// Consume the key (delete from redis)
|
// Consume the key (delete from redis)
|
||||||
jedis.del(key);
|
jedis.del(key);
|
||||||
@@ -391,11 +394,11 @@ public class RedisManager extends JedisPubSub {
|
|||||||
final byte[] readData = jedis.get(key);
|
final byte[] readData = jedis.get(key);
|
||||||
if (readData == null) {
|
if (readData == null) {
|
||||||
plugin.debug(String.format("[%s] Waiting for %s key from Redis",
|
plugin.debug(String.format("[%s] Waiting for %s key from Redis",
|
||||||
user.getUsername(), RedisKeyType.SERVER_SWITCH));
|
user.getName(), RedisKeyType.SERVER_SWITCH));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
plugin.debug(String.format("[%s] Read %s key from Redis",
|
plugin.debug(String.format("[%s] Read %s key from Redis",
|
||||||
user.getUsername(), RedisKeyType.SERVER_SWITCH));
|
user.getName(), RedisKeyType.SERVER_SWITCH));
|
||||||
|
|
||||||
// Consume the key (delete from redis)
|
// Consume the key (delete from redis)
|
||||||
jedis.del(key);
|
jedis.del(key);
|
||||||
@@ -406,6 +409,124 @@ public class RedisManager extends JedisPubSub {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
public String getStatusDump() {
|
||||||
|
try (Jedis jedis = jedisPool.getResource()) {
|
||||||
|
return jedis.info();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
public long getLatency() {
|
||||||
|
final long startTime = System.currentTimeMillis();
|
||||||
|
try (Jedis jedis = jedisPool.getResource()) {
|
||||||
|
jedis.ping();
|
||||||
|
return startTime - System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
public String getVersion() {
|
||||||
|
final String info = getStatusDump();
|
||||||
|
for (String line : info.split("\n")) {
|
||||||
|
if (line.startsWith("redis_version:")) {
|
||||||
|
return line.split(":")[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
public void bindMapIds(@NotNull String fromServer, int fromId, @NotNull String toServer, int toId) {
|
||||||
|
try (Jedis jedis = jedisPool.getResource()) {
|
||||||
|
jedis.setex(
|
||||||
|
getMapIdKey(fromServer, fromId, toServer, clusterId),
|
||||||
|
RedisKeyType.TTL_1_YEAR,
|
||||||
|
String.valueOf(toId).getBytes(StandardCharsets.UTF_8)
|
||||||
|
);
|
||||||
|
jedis.setex(
|
||||||
|
getReversedMapIdKey(toServer, toId, clusterId),
|
||||||
|
RedisKeyType.TTL_1_YEAR,
|
||||||
|
String.format("%s:%s", fromServer, fromId).getBytes(StandardCharsets.UTF_8)
|
||||||
|
);
|
||||||
|
plugin.debug(String.format("Bound map %s:%s -> %s:%s on Redis", fromServer, fromId, toServer, toId));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
plugin.log(Level.SEVERE, "An exception occurred binding map ids on Redis", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
public Optional<Integer> getBoundMapId(@NotNull String fromServer, int fromId, @NotNull String toServer) {
|
||||||
|
try (Jedis jedis = jedisPool.getResource()) {
|
||||||
|
final byte[] readData = jedis.get(getMapIdKey(fromServer, fromId, toServer, clusterId));
|
||||||
|
if (readData == null) {
|
||||||
|
plugin.debug(String.format("[%s:%s] No bound map id for server %s Redis",
|
||||||
|
fromServer, fromId, toServer));
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
plugin.debug(String.format("[%s:%s] Read bound map id for server %s from Redis",
|
||||||
|
fromServer, fromId, toServer));
|
||||||
|
|
||||||
|
return Optional.of(Integer.parseInt(new String(readData, StandardCharsets.UTF_8)));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
plugin.log(Level.SEVERE, "An exception occurred getting bound map id from Redis", e);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
public @Nullable Map.Entry<String, Integer> getReversedMapBound(@NotNull String toServer, int toId) {
|
||||||
|
try (Jedis jedis = jedisPool.getResource()) {
|
||||||
|
final byte[] readData = jedis.get(getReversedMapIdKey(toServer, toId, clusterId));
|
||||||
|
if (readData == null) {
|
||||||
|
plugin.debug(String.format("[%s:%s] No reversed map bound on Redis",
|
||||||
|
toServer, toId));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
plugin.debug(String.format("[%s:%s] Read reversed map bound from Redis",
|
||||||
|
toServer, toId));
|
||||||
|
|
||||||
|
String[] parts = new String(readData, StandardCharsets.UTF_8).split(":");
|
||||||
|
return Map.entry(parts[0], Integer.parseInt(parts[1]));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
plugin.log(Level.SEVERE, "An exception occurred reading reversed map bound from Redis", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
public void setMapData(@NotNull String serverName, int mapId, byte[] data) {
|
||||||
|
try (Jedis jedis = jedisPool.getResource()) {
|
||||||
|
jedis.setex(
|
||||||
|
getMapDataKey(serverName, mapId, clusterId),
|
||||||
|
RedisKeyType.TTL_1_YEAR,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
plugin.debug(String.format("Set map data %s:%s on Redis", serverName, mapId));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
plugin.log(Level.SEVERE, "An exception occurred setting map data on Redis", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
public byte @Nullable [] getMapData(@NotNull String serverName, int mapId) {
|
||||||
|
try (Jedis jedis = jedisPool.getResource()) {
|
||||||
|
final byte[] readData = jedis.get(getMapDataKey(serverName, mapId, clusterId));
|
||||||
|
if (readData == null) {
|
||||||
|
plugin.debug(String.format("[%s:%s] No map data on Redis",
|
||||||
|
serverName, mapId));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
plugin.debug(String.format("[%s:%s] Read map data from Redis",
|
||||||
|
serverName, mapId));
|
||||||
|
|
||||||
|
return readData;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
plugin.log(Level.SEVERE, "An exception occurred reading map data from Redis", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Blocking
|
@Blocking
|
||||||
public void terminate() {
|
public void terminate() {
|
||||||
enabled = false;
|
enabled = false;
|
||||||
@@ -418,7 +539,24 @@ public class RedisManager extends JedisPubSub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] getKey(@NotNull RedisKeyType keyType, @NotNull UUID uuid, @NotNull String clusterId) {
|
private static byte[] getKey(@NotNull RedisKeyType keyType, @NotNull UUID uuid, @NotNull String clusterId) {
|
||||||
return String.format("%s:%s", keyType.getKeyPrefix(clusterId), uuid).getBytes(StandardCharsets.UTF_8);
|
return getKeyString(keyType, uuid, clusterId).getBytes(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static String getKeyString(@NotNull RedisKeyType keyType, @NotNull UUID uuid, @NotNull String clusterId) {
|
||||||
|
return String.format("%s:%s", keyType.getKeyPrefix(clusterId), uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] getMapIdKey(@NotNull String fromServer, int fromId, @NotNull String toServer, @NotNull String clusterId) {
|
||||||
|
return String.format("%s:%s:%s:%s", RedisKeyType.MAP_ID.getKeyPrefix(clusterId), fromServer, fromId, toServer).getBytes(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] getReversedMapIdKey(@NotNull String toServer, int toId, @NotNull String clusterId) {
|
||||||
|
return String.format("%s:%s:%s", RedisKeyType.MAP_ID_REVERSED.getKeyPrefix(clusterId), toServer, toId).getBytes(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] getMapDataKey(@NotNull String serverName, int mapId, @NotNull String clusterId) {
|
||||||
|
return String.format("%s:%s:%s", RedisKeyType.MAP_DATA.getKeyPrefix(clusterId), serverName, mapId).getBytes(StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ package net.william278.husksync.redis;
|
|||||||
|
|
||||||
import com.google.gson.JsonSyntaxException;
|
import com.google.gson.JsonSyntaxException;
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
import net.william278.husksync.adapter.Adaptable;
|
import net.william278.husksync.adapter.Adaptable;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -34,6 +36,8 @@ public class RedisMessage implements Adaptable {
|
|||||||
|
|
||||||
@SerializedName("target_uuid")
|
@SerializedName("target_uuid")
|
||||||
private UUID targetUuid;
|
private UUID targetUuid;
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
@SerializedName("payload")
|
@SerializedName("payload")
|
||||||
private byte[] payload;
|
private byte[] payload;
|
||||||
|
|
||||||
@@ -72,14 +76,6 @@ public class RedisMessage implements Adaptable {
|
|||||||
this.targetUuid = targetUuid;
|
this.targetUuid = targetUuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getPayload() {
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPayload(byte[] payload) {
|
|
||||||
this.payload = payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Type {
|
public enum Type {
|
||||||
|
|
||||||
UPDATE_USER_DATA,
|
UPDATE_USER_DATA,
|
||||||
|
|||||||
@@ -81,18 +81,28 @@ public abstract class DataSyncer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a user's data should be fetched and applied to them
|
* Called when a user's data should be fetched and applied to them as part of a synchronization process
|
||||||
*
|
*
|
||||||
* @param user the user to fetch data for
|
* @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
|
* @param user the user to save
|
||||||
*/
|
*/
|
||||||
public abstract void saveUserData(@NotNull OnlineUser user);
|
public abstract void syncSaveUserData(@NotNull OnlineUser user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a user's current data
|
||||||
|
*
|
||||||
|
* @param onlineUser the user to save data of
|
||||||
|
* @param cause the save cause
|
||||||
|
*/
|
||||||
|
public void saveCurrentUserData(@NotNull OnlineUser onlineUser, @NotNull DataSnapshot.SaveCause cause) {
|
||||||
|
this.saveData(onlineUser, onlineUser.createSnapshot(cause), getRedis()::setUserData);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save a {@link DataSnapshot.Packed user's data snapshot} to the database,
|
* Save a {@link DataSnapshot.Packed user's data snapshot} to the database,
|
||||||
@@ -163,7 +173,7 @@ public abstract class DataSyncer {
|
|||||||
() -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin)
|
() -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin)
|
||||||
);
|
);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
plugin.log(Level.WARNING, "Failed to set %s's data from the database".formatted(user.getUsername()), e);
|
plugin.log(Level.WARNING, "Failed to set %s's data from the database".formatted(user.getName()), e);
|
||||||
user.completeSync(false, DataSnapshot.UpdateCause.SYNCHRONIZED, plugin);
|
user.completeSync(false, DataSnapshot.UpdateCause.SYNCHRONIZED, plugin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +198,7 @@ public abstract class DataSyncer {
|
|||||||
if (plugin.isDisabling() || timesRun.getAndIncrement() > maxListenAttempts) {
|
if (plugin.isDisabling() || timesRun.getAndIncrement() > maxListenAttempts) {
|
||||||
task.get().cancel();
|
task.get().cancel();
|
||||||
plugin.debug(String.format("[%s] Redis timed out after %s attempts; setting from database",
|
plugin.debug(String.format("[%s] Redis timed out after %s attempts; setting from database",
|
||||||
user.getUsername(), timesRun.get()));
|
user.getName(), timesRun.get()));
|
||||||
setUserFromDatabase(user);
|
setUserFromDatabase(user);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ package net.william278.husksync.sync;
|
|||||||
|
|
||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
import net.william278.husksync.data.DataSnapshot;
|
import net.william278.husksync.data.DataSnapshot;
|
||||||
import net.william278.husksync.redis.RedisKeyType;
|
|
||||||
import net.william278.husksync.user.OnlineUser;
|
import net.william278.husksync.user.OnlineUser;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
@@ -35,7 +34,7 @@ public class DelayDataSyncer extends DataSyncer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setUserData(@NotNull OnlineUser user) {
|
public void syncApplyUserData(@NotNull OnlineUser user) {
|
||||||
plugin.runAsyncDelayed(
|
plugin.runAsyncDelayed(
|
||||||
() -> {
|
() -> {
|
||||||
// Fetch from the database if the user isn't changing servers
|
// Fetch from the database if the user isn't changing servers
|
||||||
@@ -58,12 +57,12 @@ public class DelayDataSyncer extends DataSyncer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void saveUserData(@NotNull OnlineUser onlineUser) {
|
public void syncSaveUserData(@NotNull OnlineUser onlineUser) {
|
||||||
plugin.runAsync(() -> {
|
plugin.runAsync(() -> {
|
||||||
getRedis().setUserServerSwitch(onlineUser);
|
getRedis().setUserServerSwitch(onlineUser);
|
||||||
saveData(
|
saveData(
|
||||||
onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
|
onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
|
||||||
(user, data) -> getRedis().setUserData(user, data, RedisKeyType.TTL_10_SECONDS)
|
(user, data) -> getRedis().setUserData(user, data)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ package net.william278.husksync.sync;
|
|||||||
|
|
||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
import net.william278.husksync.data.DataSnapshot;
|
import net.william278.husksync.data.DataSnapshot;
|
||||||
import net.william278.husksync.redis.RedisKeyType;
|
|
||||||
import net.william278.husksync.user.OnlineUser;
|
import net.william278.husksync.user.OnlineUser;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@ public class LockstepDataSyncer extends DataSyncer {
|
|||||||
|
|
||||||
// Consume their data when they are checked in
|
// Consume their data when they are checked in
|
||||||
@Override
|
@Override
|
||||||
public void setUserData(@NotNull OnlineUser user) {
|
public void syncApplyUserData(@NotNull OnlineUser user) {
|
||||||
this.listenForRedisData(user, () -> {
|
this.listenForRedisData(user, () -> {
|
||||||
if (getRedis().getUserCheckedOut(user).isPresent()) {
|
if (getRedis().getUserCheckedOut(user).isPresent()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -58,17 +57,14 @@ public class LockstepDataSyncer extends DataSyncer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void saveUserData(@NotNull OnlineUser onlineUser) {
|
public void syncSaveUserData(@NotNull OnlineUser onlineUser) {
|
||||||
plugin.runAsync(() -> {
|
plugin.runAsync(() -> saveData(
|
||||||
getRedis().setUserServerSwitch(onlineUser);
|
|
||||||
saveData(
|
|
||||||
onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
|
onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
|
||||||
(user, data) -> {
|
(user, data) -> {
|
||||||
getRedis().setUserData(user, data, RedisKeyType.TTL_1_YEAR);
|
getRedis().setUserData(user, data);
|
||||||
getRedis().setUserCheckedOut(user, false);
|
getRedis().setUserCheckedOut(user, false);
|
||||||
}
|
}
|
||||||
);
|
));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,4 +42,6 @@ public final class ConsoleUser implements CommandUser {
|
|||||||
public boolean hasPermission(@NotNull String permission) {
|
public boolean hasPermission(@NotNull String permission) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,9 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
|||||||
* @param description the description of the toast
|
* @param description the description of the toast
|
||||||
* @param iconMaterial the namespace-keyed material to use as an hasIcon 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
|
* @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,
|
public abstract void sendToast(@NotNull MineDown title, @NotNull MineDown description,
|
||||||
@NotNull String iconMaterial, @NotNull String backgroundType);
|
@NotNull String iconMaterial, @NotNull String backgroundType);
|
||||||
|
|
||||||
@@ -125,7 +127,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
|||||||
getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> {
|
getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> {
|
||||||
if (!isOffline()) {
|
if (!isOffline()) {
|
||||||
getPlugin().debug(String.format("Applying snapshot (%s) to %s (cause: %s)",
|
getPlugin().debug(String.format("Applying snapshot (%s) to %s (cause: %s)",
|
||||||
snapshot.getShortId(), getUsername(), cause.getDisplayName()
|
snapshot.getShortId(), getName(), cause.getDisplayName()
|
||||||
));
|
));
|
||||||
UserDataHolder.super.applySnapshot(
|
UserDataHolder.super.applySnapshot(
|
||||||
event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin())
|
event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin())
|
||||||
@@ -145,12 +147,6 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
|||||||
switch (plugin.getSettings().getSynchronization().getNotificationDisplaySlot()) {
|
switch (plugin.getSettings().getSynchronization().getNotificationDisplaySlot()) {
|
||||||
case CHAT -> cause.getCompletedLocale(plugin).ifPresent(this::sendMessage);
|
case CHAT -> cause.getCompletedLocale(plugin).ifPresent(this::sendMessage);
|
||||||
case ACTION_BAR -> cause.getCompletedLocale(plugin).ifPresent(this::sendActionBar);
|
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.fireEvent(
|
||||||
plugin.getSyncCompleteEvent(this),
|
plugin.getSyncCompleteEvent(this),
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
|
|
||||||
package net.william278.husksync.user;
|
package net.william278.husksync.user;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -26,39 +29,18 @@ import java.util.UUID;
|
|||||||
/**
|
/**
|
||||||
* Represents a user who has their data synchronized by HuskSync
|
* Represents a user who has their data synchronized by HuskSync
|
||||||
*/
|
*/
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode(exclude = {"name"})
|
||||||
public class User {
|
public class User {
|
||||||
|
|
||||||
private final UUID uuid;
|
private final UUID uuid;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
private final String username;
|
|
||||||
|
|
||||||
public User(@NotNull UUID uuid, @NotNull String username) {
|
|
||||||
this.username = username;
|
|
||||||
this.uuid = uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the user's unique account ID
|
|
||||||
*/
|
|
||||||
@NotNull
|
|
||||||
public UUID getUuid() {
|
|
||||||
return uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the user's username
|
|
||||||
*/
|
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@Deprecated(since = "3.7.4")
|
||||||
public String getUsername() {
|
public String getUsername() {
|
||||||
return username;
|
return name;
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object object) {
|
|
||||||
if (object instanceof User other) {
|
|
||||||
return this.getUuid().equals(other.getUuid());
|
|
||||||
}
|
|
||||||
return super.equals(object);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
|
||||||
*
|
|
||||||
* Copyright (c) William278 <will27528@gmail.com>
|
|
||||||
* Copyright (c) contributors
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.william278.husksync.util;
|
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.google.gson.JsonParser;
|
|
||||||
import net.william278.husksync.HuskSync;
|
|
||||||
import net.william278.husksync.data.DataSnapshot;
|
|
||||||
import net.william278.husksync.user.User;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.StringJoiner;
|
|
||||||
import java.util.logging.Level;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class for dumping {@link DataSnapshot}s to a file or as a paste on the web
|
|
||||||
*/
|
|
||||||
public class DataDumper {
|
|
||||||
|
|
||||||
private static final String LOGS_SITE_ENDPOINT = "https://api.mclo.gs/1/log";
|
|
||||||
|
|
||||||
private final HuskSync plugin;
|
|
||||||
private final DataSnapshot.Packed snapshot;
|
|
||||||
private final User user;
|
|
||||||
|
|
||||||
private DataDumper(@NotNull DataSnapshot.Packed snapshot, @NotNull User user, @NotNull HuskSync implementor) {
|
|
||||||
this.snapshot = snapshot;
|
|
||||||
this.user = user;
|
|
||||||
this.plugin = implementor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a {@link DataDumper} of the given {@link DataSnapshot}
|
|
||||||
*
|
|
||||||
* @param dataSnapshot The {@link DataSnapshot} to dump
|
|
||||||
* @param user The {@link User} whose data is being dumped
|
|
||||||
* @param plugin The implementing {@link HuskSync} plugin
|
|
||||||
* @return A {@link DataDumper} for the given {@link DataSnapshot}
|
|
||||||
*/
|
|
||||||
public static DataDumper create(@NotNull DataSnapshot.Packed dataSnapshot,
|
|
||||||
@NotNull User user, @NotNull HuskSync plugin) {
|
|
||||||
return new DataDumper(dataSnapshot, user, plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dumps the data snapshot to a string
|
|
||||||
*
|
|
||||||
* @return the data snapshot as a string
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@NotNull
|
|
||||||
public String toString() {
|
|
||||||
return snapshot.asJson(plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public String toWeb() {
|
|
||||||
try {
|
|
||||||
final URL url = new URL(LOGS_SITE_ENDPOINT);
|
|
||||||
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
||||||
connection.setRequestMethod("POST");
|
|
||||||
connection.setDoOutput(true);
|
|
||||||
|
|
||||||
// Dispatch the request
|
|
||||||
final byte[] messageBody = getWebContentField().getBytes(StandardCharsets.UTF_8);
|
|
||||||
final int messageLength = messageBody.length;
|
|
||||||
connection.setFixedLengthStreamingMode(messageLength);
|
|
||||||
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
|
|
||||||
connection.connect();
|
|
||||||
try (OutputStream messageOutputStream = connection.getOutputStream()) {
|
|
||||||
messageOutputStream.write(messageBody);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the response
|
|
||||||
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
|
|
||||||
// Get the body as a json
|
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
|
|
||||||
final StringBuilder response = new StringBuilder();
|
|
||||||
String line;
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
response.append(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the response as json
|
|
||||||
final JsonObject responseJson = JsonParser.parseString(response.toString()).getAsJsonObject();
|
|
||||||
if (responseJson.has("url")) {
|
|
||||||
return responseJson.get("url").getAsString();
|
|
||||||
}
|
|
||||||
return "(Failed to get URL from response)";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return "(Failed to upload to logs site, got: " + connection.getResponseCode() + ")";
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
|
||||||
plugin.log(Level.SEVERE, "Failed to upload data to logs site", e);
|
|
||||||
}
|
|
||||||
return "(Failed to upload to logs site)";
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private String getWebContentField() {
|
|
||||||
return "content=" + URLEncoder.encode(toString(), StandardCharsets.UTF_8);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dump the {@link DataSnapshot} to a file and return the file name
|
|
||||||
*
|
|
||||||
* @return the relative path of the file the data was dumped to
|
|
||||||
*/
|
|
||||||
@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());
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IOException("Failed to write data to file", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return "~/plugins/HuskSync/dumps/" + filePath.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the file path to dump the data to
|
|
||||||
*
|
|
||||||
* @return the file path
|
|
||||||
* @throws IOException if the prerequisite dumps parent folder could not be created
|
|
||||||
*/
|
|
||||||
@NotNull
|
|
||||||
private File getFilePath() throws IOException {
|
|
||||||
return new File(getDumpsFolder(), getFileName());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the folder to dump the data to and create it if it does not exist
|
|
||||||
*
|
|
||||||
* @return the dumps folder
|
|
||||||
* @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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dumpsFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the name of the file to dump the data snapshot to
|
|
||||||
*
|
|
||||||
* @return the file name
|
|
||||||
*/
|
|
||||||
@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";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -49,7 +49,7 @@ public class DataSnapshotList {
|
|||||||
.map(snapshot -> plugin.getLocales()
|
.map(snapshot -> plugin.getLocales()
|
||||||
.getRawLocale(!snapshot.isInvalid() ? "data_list_item" : "data_list_item_invalid",
|
.getRawLocale(!snapshot.isInvalid() ? "data_list_item" : "data_list_item_invalid",
|
||||||
getNumberIcon(snapshotNumber.getAndIncrement()),
|
getNumberIcon(snapshotNumber.getAndIncrement()),
|
||||||
dataOwner.getUsername(),
|
dataOwner.getName(),
|
||||||
snapshot.getId().toString(),
|
snapshot.getId().toString(),
|
||||||
snapshot.getShortId(),
|
snapshot.getShortId(),
|
||||||
snapshot.isPinned() ? "※" : " ",
|
snapshot.isPinned() ? "※" : " ",
|
||||||
@@ -63,10 +63,10 @@ public class DataSnapshotList {
|
|||||||
.orElse("• " + snapshot.getId())).toList(),
|
.orElse("• " + snapshot.getId())).toList(),
|
||||||
plugin.getLocales().getBaseChatList(6)
|
plugin.getLocales().getBaseChatList(6)
|
||||||
.setHeaderFormat(plugin.getLocales()
|
.setHeaderFormat(plugin.getLocales()
|
||||||
.getRawLocale("data_list_title", dataOwner.getUsername(),
|
.getRawLocale("data_list_title", dataOwner.getName(),
|
||||||
"%first_item_on_page_index%", "%last_item_on_page_index%", "%total_items%")
|
"%first_item_on_page_index%", "%last_item_on_page_index%", "%total_items%")
|
||||||
.orElse(""))
|
.orElse(""))
|
||||||
.setCommand("/husksync:userdata list " + dataOwner.getUsername())
|
.setCommand("/husksync:userdata list " + dataOwner.getName())
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class DataSnapshotOverview {
|
|||||||
// Title message, timestamp, owner and cause.
|
// Title message, timestamp, owner and cause.
|
||||||
final Locales locales = plugin.getLocales();
|
final Locales locales = plugin.getLocales();
|
||||||
locales.getLocale("data_manager_title", snapshot.getShortId(), snapshot.getId().toString(),
|
locales.getLocale("data_manager_title", snapshot.getShortId(), snapshot.getId().toString(),
|
||||||
dataOwner.getUsername(), dataOwner.getUuid().toString())
|
dataOwner.getName(), dataOwner.getUuid().toString())
|
||||||
.ifPresent(user::sendMessage);
|
.ifPresent(user::sendMessage);
|
||||||
locales.getLocale("data_manager_timestamp",
|
locales.getLocale("data_manager_timestamp",
|
||||||
snapshot.getTimestamp().format(DateTimeFormatter
|
snapshot.getTimestamp().format(DateTimeFormatter
|
||||||
@@ -107,13 +107,13 @@ public class DataSnapshotOverview {
|
|||||||
|
|
||||||
if (user.hasPermission("husksync.command.inventory.edit")
|
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())
|
locales.getLocale("data_manager_item_buttons", dataOwner.getName(), snapshot.getId().toString())
|
||||||
.ifPresent(user::sendMessage);
|
.ifPresent(user::sendMessage);
|
||||||
}
|
}
|
||||||
locales.getLocale("data_manager_management_buttons", dataOwner.getUsername(), snapshot.getId().toString())
|
locales.getLocale("data_manager_management_buttons", dataOwner.getName(), snapshot.getId().toString())
|
||||||
.ifPresent(user::sendMessage);
|
.ifPresent(user::sendMessage);
|
||||||
if (user.hasPermission("husksync.command.userdata.dump")) {
|
if (user.hasPermission("husksync.command.userdata.dump")) {
|
||||||
locales.getLocale("data_manager_system_buttons", dataOwner.getUsername(), snapshot.getId().toString())
|
locales.getLocale("data_manager_system_buttons", dataOwner.getName(), snapshot.getId().toString())
|
||||||
.ifPresent(user::sendMessage);
|
.ifPresent(user::sendMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||||
|
*
|
||||||
|
* Copyright (c) William278 <will27528@gmail.com>
|
||||||
|
* Copyright (c) contributors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.william278.husksync.util;
|
||||||
|
|
||||||
|
import net.william278.husksync.HuskSync;
|
||||||
|
import net.william278.husksync.user.CommandUser;
|
||||||
|
import net.william278.husksync.user.OnlineUser;
|
||||||
|
import net.william278.toilet.DumpOptions;
|
||||||
|
import net.william278.toilet.Toilet;
|
||||||
|
import net.william278.toilet.dump.*;
|
||||||
|
import org.jetbrains.annotations.Blocking;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import static net.william278.toilet.DumpOptions.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public interface DumpProvider {
|
||||||
|
|
||||||
|
@NotNull String BYTEBIN_URL = "https://bytebin.lucko.me";
|
||||||
|
@NotNull String VIEWER_URL = "https://william278.net/dump";
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
Toilet getToilet();
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Blocking
|
||||||
|
default String createDump(@NotNull CommandUser u) {
|
||||||
|
return getToilet().dump(getPluginStatus(), u instanceof OnlineUser o
|
||||||
|
? new DumpUser(o.getName(), o.getUuid()) : null,
|
||||||
|
getRedisInfo()).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
default DumpOptions getDumpOptions() {
|
||||||
|
return builder()
|
||||||
|
.bytebinUrl(BYTEBIN_URL)
|
||||||
|
.viewerUrl(VIEWER_URL)
|
||||||
|
.projectMeta(ProjectMeta.builder()
|
||||||
|
.id("husksync")
|
||||||
|
.name("HuskSync")
|
||||||
|
.version(getPlugin().getPluginVersion().toString())
|
||||||
|
.md5("unknown")
|
||||||
|
.author("William278")
|
||||||
|
.sourceCode("https://github.com/WiIIiam278/HuskSync")
|
||||||
|
.website("https://william278.net/project/husksync")
|
||||||
|
.support("https://discord.gg/tVYhJfyDWG")
|
||||||
|
.build())
|
||||||
|
.fileInclusionRules(List.of(
|
||||||
|
FileInclusionRule.configFile("config.yml", "Config File"),
|
||||||
|
FileInclusionRule.configFile(getMessagesFile(), "Locales File")
|
||||||
|
))
|
||||||
|
.compatibilityRules(List.of(
|
||||||
|
getCompatibilityWarning("CombatLogX", "Combat loggers require additional" +
|
||||||
|
"configuration for use with HuskSync. Check https://william278.net/docs/husksync/event-priorities"),
|
||||||
|
getIncompatibleNotice("UltimateAutoRestart", "Restart plugins are not" +
|
||||||
|
"compatible with HuskSync as they affect the way the server shuts down, preventing data" +
|
||||||
|
"from saving correctly during a restart. Check https://william278.net/docs/husksync/troubleshooting")
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Blocking
|
||||||
|
private PluginStatus getPluginStatus() {
|
||||||
|
return PluginStatus.builder()
|
||||||
|
.blocks(List.of(getSystemStatus(), getRegisteredDataTypes()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Blocking
|
||||||
|
private PluginStatus.MapStatusBlock getSystemStatus() {
|
||||||
|
return new PluginStatus.MapStatusBlock(
|
||||||
|
Map.ofEntries(
|
||||||
|
Map.entry("Language", StatusLine.LANGUAGE.getValue(getPlugin())),
|
||||||
|
Map.entry("Database Type", StatusLine.DATABASE_TYPE.getValue(getPlugin())),
|
||||||
|
Map.entry("Database Local", StatusLine.IS_DATABASE_LOCAL.getValue(getPlugin())),
|
||||||
|
Map.entry("Locked User Handler", StatusLine.LOCKED_USER_HANDLER.getValue(getPlugin())),
|
||||||
|
Map.entry("Server Name", StatusLine.SERVER_NAME.getValue(getPlugin())),
|
||||||
|
Map.entry("Redis Version", StatusLine.REDIS_VERSION.getValue(getPlugin())),
|
||||||
|
Map.entry("Redis Latency", StatusLine.REDIS_LATENCY.getValue(getPlugin())),
|
||||||
|
Map.entry("Redis Sentinel", StatusLine.USING_REDIS_SENTINEL.getValue(getPlugin())),
|
||||||
|
Map.entry("Redis Password", StatusLine.USING_REDIS_PASSWORD.getValue(getPlugin())),
|
||||||
|
Map.entry("Redis SSL", StatusLine.REDIS_USING_SSL.getValue(getPlugin())),
|
||||||
|
Map.entry("Redis Local", StatusLine.IS_REDIS_LOCAL.getValue(getPlugin()))
|
||||||
|
),
|
||||||
|
"Plugin Status", "fa6-solid:wrench"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Blocking
|
||||||
|
private PluginStatus.MapStatusBlock getRegisteredDataTypes() {
|
||||||
|
return new PluginStatus.MapStatusBlock(
|
||||||
|
getPlugin().getRegisteredDataTypes().stream().collect(Collectors.toMap(
|
||||||
|
i -> i.getKey().asMinimalString(),
|
||||||
|
i -> i.isEnabled() ? "✅ Enabled" : "❌ Disabled",
|
||||||
|
(a, b) -> a)
|
||||||
|
),
|
||||||
|
"Registered Data Types", "carbon:data-blob"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Blocking
|
||||||
|
private ExtraFile getRedisInfo() {
|
||||||
|
return new ExtraFile(
|
||||||
|
"redis-status", "Redis Status", "devicon-plain:redis",
|
||||||
|
getPlugin().getRedisManager().getStatusDump(),
|
||||||
|
"markdown"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@SuppressWarnings("SameParameterValue")
|
||||||
|
private CompatibilityRule getCompatibilityWarning(@NotNull String plugin, @NotNull String description) {
|
||||||
|
return CompatibilityRule.builder()
|
||||||
|
.labelToApply(new PluginInfo.Label("Warning", "#fcba03", description))
|
||||||
|
.resourceName(plugin).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@SuppressWarnings("SameParameterValue")
|
||||||
|
private CompatibilityRule getIncompatibleNotice(@NotNull String plugin, @NotNull String description) {
|
||||||
|
return CompatibilityRule.builder()
|
||||||
|
.labelToApply(new PluginInfo.Label("Incompatible", "#ff3300", description))
|
||||||
|
.resourceName(plugin).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private String getMessagesFile() {
|
||||||
|
return "messages-%s.yml".formatted(getPlugin().getSettings().getLanguage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
HuskSync getPlugin();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||||
|
*
|
||||||
|
* Copyright (c) William278 <will27528@gmail.com>
|
||||||
|
* Copyright (c) contributors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.william278.husksync.util;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.JoinConfiguration;
|
||||||
|
import net.kyori.adventure.text.event.HoverEvent;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.kyori.adventure.text.format.TextColor;
|
||||||
|
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||||
|
import net.william278.husksync.HuskSync;
|
||||||
|
import net.william278.husksync.database.Database;
|
||||||
|
import org.apache.commons.text.WordUtils;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public enum StatusLine {
|
||||||
|
PLUGIN_VERSION(plugin -> Component.text("v" + plugin.getPluginVersion().toStringWithoutMetadata())
|
||||||
|
.appendSpace().append(plugin.getPluginVersion().getMetadata().isBlank() ? Component.empty()
|
||||||
|
: Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))),
|
||||||
|
SERVER_VERSION(plugin -> Component.text(plugin.getServerVersion())),
|
||||||
|
LANGUAGE(plugin -> Component.text(plugin.getSettings().getLanguage())),
|
||||||
|
MINECRAFT_VERSION(plugin -> Component.text(plugin.getMinecraftVersion().toString())),
|
||||||
|
JAVA_VERSION(plugin -> Component.text(System.getProperty("java.version"))),
|
||||||
|
JAVA_VENDOR(plugin -> Component.text(System.getProperty("java.vendor"))),
|
||||||
|
SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
|
||||||
|
CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())),
|
||||||
|
SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully(
|
||||||
|
plugin.getSettings().getSynchronization().getMode().toString()
|
||||||
|
))),
|
||||||
|
DELAY_LATENCY(plugin -> Component.text(
|
||||||
|
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms"
|
||||||
|
)),
|
||||||
|
DATABASE_TYPE(plugin ->
|
||||||
|
Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() +
|
||||||
|
(plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ?
|
||||||
|
(plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : ""))
|
||||||
|
),
|
||||||
|
IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())),
|
||||||
|
REDIS_VERSION(plugin -> Component.text(plugin.getRedisManager().getVersion())),
|
||||||
|
USING_REDIS_SENTINEL(plugin -> getBoolean(
|
||||||
|
!plugin.getSettings().getRedis().getSentinel().getMaster().isBlank()
|
||||||
|
)),
|
||||||
|
USING_REDIS_PASSWORD(plugin -> getBoolean(
|
||||||
|
!plugin.getSettings().getRedis().getCredentials().getPassword().isBlank()
|
||||||
|
)),
|
||||||
|
REDIS_USING_SSL(plugin -> getBoolean(
|
||||||
|
plugin.getSettings().getRedis().getCredentials().isUseSsl()
|
||||||
|
)),
|
||||||
|
REDIS_LATENCY(plugin -> Component.text("%sms".formatted(plugin.getRedisManager().getLatency()))),
|
||||||
|
IS_REDIS_LOCAL(plugin -> getLocalhostBoolean(
|
||||||
|
plugin.getSettings().getRedis().getCredentials().getHost()
|
||||||
|
)),
|
||||||
|
LOCKED_USER_HANDLER(plugin -> Component.text(plugin.getLockedHandler().getClass().getSimpleName())),
|
||||||
|
DATA_TYPES(plugin -> Component.join(
|
||||||
|
JoinConfiguration.commas(true),
|
||||||
|
plugin.getRegisteredDataTypes().stream().map(i -> Component.textOfChildren(Component.text(i.toString())
|
||||||
|
.appendSpace().append(Component.text(i.isEnabled() ? '✔' : '❌')))
|
||||||
|
.color(i.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED)
|
||||||
|
.hoverEvent(HoverEvent.showText(
|
||||||
|
Component.text(i.isEnabled() ? "Enabled" : "Disabled")
|
||||||
|
.append(Component.newline())
|
||||||
|
.append(Component.text("Dependencies: %s".formatted(i.getDependencies()
|
||||||
|
.isEmpty() ? "(None)" : i.getDependencies().stream()
|
||||||
|
.map(d -> "%s (%s)".formatted(
|
||||||
|
d.getKey().value(), d.isRequired() ? "Required" : "Optional"
|
||||||
|
)).collect(Collectors.joining(", ")))
|
||||||
|
).color(NamedTextColor.GRAY))
|
||||||
|
))).toList()
|
||||||
|
));
|
||||||
|
|
||||||
|
private final Function<HuskSync, Component> supplier;
|
||||||
|
|
||||||
|
StatusLine(@NotNull Function<HuskSync, Component> supplier) {
|
||||||
|
this.supplier = supplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public Component get(@NotNull HuskSync plugin) {
|
||||||
|
return Component
|
||||||
|
.text("•").appendSpace()
|
||||||
|
.append(Component.text(
|
||||||
|
WordUtils.capitalizeFully(name().replaceAll("_", " ")),
|
||||||
|
TextColor.color(0x848484)
|
||||||
|
))
|
||||||
|
.append(Component.text(':')).append(Component.space().color(NamedTextColor.WHITE))
|
||||||
|
.append(supplier.apply(plugin));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public String getValue(@NotNull HuskSync plugin) {
|
||||||
|
return PlainTextComponentSerializer.plainText().serialize(supplier.apply(plugin));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static Component getBoolean(boolean value) {
|
||||||
|
return Component.text(value ? "Yes" : "No", value ? NamedTextColor.GREEN : NamedTextColor.RED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static Component getLocalhostBoolean(@NotNull String value) {
|
||||||
|
return getBoolean(value.equals("127.0.0.1") || value.equals("0.0.0.0")
|
||||||
|
|| value.equals("localhost") || value.equals("::1"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
* 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 lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import net.william278.husksync.HuskSync;
|
||||||
|
import net.william278.husksync.data.DataSnapshot;
|
||||||
|
import net.william278.husksync.user.User;
|
||||||
|
import net.william278.toilet.web.Flusher;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.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;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
import static net.william278.husksync.util.DumpProvider.BYTEBIN_URL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for dumping {@link DataSnapshot}s to a file or as a paste on the web
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class UserDataDumper implements Flusher {
|
||||||
|
|
||||||
|
private static final String PASTE_VIEWER_URL = "https://pastes.dev";
|
||||||
|
|
||||||
|
private final DataSnapshot.Packed snapshot;
|
||||||
|
private final User user;
|
||||||
|
private final HuskSync plugin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a {@link UserDataDumper} of the given {@link DataSnapshot}
|
||||||
|
*
|
||||||
|
* @param snapshot The {@link DataSnapshot} to dump
|
||||||
|
* @param user The {@link User} whose data is being dumped
|
||||||
|
* @param plugin The implementing {@link HuskSync} plugin
|
||||||
|
* @return A {@link UserDataDumper} for the given {@link DataSnapshot}
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public static UserDataDumper create(@NotNull DataSnapshot.Packed snapshot, @NotNull User user,
|
||||||
|
@NotNull HuskSync plugin) {
|
||||||
|
return new UserDataDumper(snapshot, user, plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public String toWeb() {
|
||||||
|
try {
|
||||||
|
return "%s/%s".formatted(PASTE_VIEWER_URL, uploadDump(toString(), BYTEBIN_URL, "husksync"));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to upload data.", e);
|
||||||
|
}
|
||||||
|
return "(Failed to upload. Try dumping to a file instead.)";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dump the {@link DataSnapshot} to a file and return the file name
|
||||||
|
*
|
||||||
|
* @return the relative path of the file the data was dumped to
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public String toFile() throws IOException {
|
||||||
|
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 dump to file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the file path to dump the data to
|
||||||
|
*
|
||||||
|
* @return the file path
|
||||||
|
* @throws IOException if the prerequisite dumps parent folder could not be created
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
private Path getFilePath() throws IOException {
|
||||||
|
return getDumpsFolder().resolve(getFileName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the folder to dump the data to and create it if it does not exist
|
||||||
|
*
|
||||||
|
* @return the dumps folder
|
||||||
|
* @throws IOException if the folder could not be created
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
private Path getDumpsFolder() throws IOException {
|
||||||
|
final Path dumps = plugin.getConfigDirectory().resolve("dumps");
|
||||||
|
if (!Files.exists(dumps)) {
|
||||||
|
Files.createDirectory(dumps);
|
||||||
|
}
|
||||||
|
return dumps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the file to dump the data snapshot to
|
||||||
|
*
|
||||||
|
* @return the file name
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
private String getFileName() {
|
||||||
|
return new StringJoiner("_")
|
||||||
|
.add(user.getName())
|
||||||
|
.add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")))
|
||||||
|
.add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH))
|
||||||
|
.add(snapshot.getShortId())
|
||||||
|
+ ".json";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dumps the data snapshot to a string
|
||||||
|
*
|
||||||
|
* @return the data snapshot as a string
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public String toString() {
|
||||||
|
return snapshot.asJson(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -30,3 +30,27 @@ CREATE TABLE IF NOT EXISTS `%user_data_table%`
|
|||||||
) ENGINE = InnoDB
|
) ENGINE = InnoDB
|
||||||
DEFAULT CHARSET = utf8mb4
|
DEFAULT CHARSET = utf8mb4
|
||||||
COLLATE = utf8mb4_unicode_ci;
|
COLLATE = utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Create the map data table if it does not exist
|
||||||
|
CREATE TABLE IF NOT EXISTS `%map_data_table%`
|
||||||
|
(
|
||||||
|
`server_name` varchar(32) NOT NULL,
|
||||||
|
`map_id` int NOT NULL,
|
||||||
|
`data` longblob NOT NULL,
|
||||||
|
PRIMARY KEY (`server_name`, `map_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Create the map ids table if it does not exist
|
||||||
|
CREATE TABLE IF NOT EXISTS `%map_ids_table%`
|
||||||
|
(
|
||||||
|
`from_server_name` varchar(32) NOT NULL,
|
||||||
|
`from_id` int NOT NULL,
|
||||||
|
`to_server_name` varchar(32) NOT NULL,
|
||||||
|
`to_id` int NOT NULL,
|
||||||
|
PRIMARY KEY (`from_server_name`, `from_id`, `to_server_name`),
|
||||||
|
FOREIGN KEY (`from_server_name`, `from_id`) REFERENCES `%map_data_table%` (`server_name`, `map_id`) ON DELETE CASCADE
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_unicode_ci;
|
||||||
|
|||||||
@@ -27,3 +27,25 @@ CREATE TABLE IF NOT EXISTS `%user_data_table%`
|
|||||||
FOREIGN KEY (`player_uuid`) REFERENCES `%users_table%` (`uuid`) ON DELETE CASCADE
|
FOREIGN KEY (`player_uuid`) REFERENCES `%users_table%` (`uuid`) ON DELETE CASCADE
|
||||||
) CHARACTER SET utf8
|
) CHARACTER SET utf8
|
||||||
COLLATE utf8_unicode_ci;
|
COLLATE utf8_unicode_ci;
|
||||||
|
|
||||||
|
# Create the map data table if it does not exist
|
||||||
|
CREATE TABLE IF NOT EXISTS `%map_data_table%`
|
||||||
|
(
|
||||||
|
`server_name` varchar(32) NOT NULL,
|
||||||
|
`map_id` int NOT NULL,
|
||||||
|
`data` longblob NOT NULL,
|
||||||
|
PRIMARY KEY (`server_name`, `map_id`)
|
||||||
|
) CHARACTER SET utf8
|
||||||
|
COLLATE utf8_unicode_ci;
|
||||||
|
|
||||||
|
# Create the map ids table if it does not exist
|
||||||
|
CREATE TABLE IF NOT EXISTS `%map_ids_table%`
|
||||||
|
(
|
||||||
|
`from_server_name` varchar(32) NOT NULL,
|
||||||
|
`from_id` int NOT NULL,
|
||||||
|
`to_server_name` varchar(32) NOT NULL,
|
||||||
|
`to_id` int NOT NULL,
|
||||||
|
PRIMARY KEY (`from_server_name`, `from_id`, `to_server_name`),
|
||||||
|
FOREIGN KEY (`from_server_name`, `from_id`) REFERENCES `%map_data_table%` (`server_name`, `map_id`) ON DELETE CASCADE
|
||||||
|
) CHARACTER SET utf8
|
||||||
|
COLLATE utf8_unicode_ci;
|
||||||
|
|||||||
@@ -20,3 +20,23 @@ CREATE TABLE IF NOT EXISTS "%user_data_table%"
|
|||||||
PRIMARY KEY (version_uuid, player_uuid),
|
PRIMARY KEY (version_uuid, player_uuid),
|
||||||
FOREIGN KEY (player_uuid) REFERENCES "%users_table%" (uuid) ON DELETE CASCADE
|
FOREIGN KEY (player_uuid) REFERENCES "%users_table%" (uuid) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Create the map data table if it does not exist
|
||||||
|
CREATE TABLE IF NOT EXISTS "%map_data_table%"
|
||||||
|
(
|
||||||
|
server_name varchar(32) NOT NULL,
|
||||||
|
map_id int NOT NULL,
|
||||||
|
data bytea NOT NULL,
|
||||||
|
PRIMARY KEY (server_name, map_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create the map ids table if it does not exist
|
||||||
|
CREATE TABLE IF NOT EXISTS "%map_ids_table%"
|
||||||
|
(
|
||||||
|
from_server_name varchar(32) NOT NULL,
|
||||||
|
from_id int NOT NULL,
|
||||||
|
to_server_name varchar(32) NOT NULL,
|
||||||
|
to_id int NOT NULL,
|
||||||
|
PRIMARY KEY (from_server_name, from_id, to_server_name),
|
||||||
|
FOREIGN KEY (from_server_name, from_id) REFERENCES "%map_data_table%" (server_name, map_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ locales:
|
|||||||
data_list_title: '[Лист от](#00fb9a) [снапшоти на данните на потребителя](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)\n'
|
data_list_title: '[Лист от](#00fb9a) [снапшоти на данните на потребителя](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
|
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||||
data_deleted: '[❌ Успешно изтрихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
data_deleted: '[❌ Успешно изтрихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
||||||
data_restored: '[⏪ Успешно възстановихме](#00fb9a) [текущите потребителски данни за](#00fb9a) [%1%](#00fb9a show_text=&7UUID на Играча:\n&8%2%) [от снапшот](#00fb9a) [%3%.](#00fb9a show_text=&7Версия на UUID:\n&8%4%)'
|
data_restored: '[⏪ Успешно възстановихме](#00fb9a) [текущите потребителски данни за](#00fb9a) [%1%](#00fb9a show_text=&7UUID на Играча:\n&8%2%) [от снапшот](#00fb9a) [%3%.](#00fb9a show_text=&7Версия на UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Успешно закачихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
data_pinned: '[※ Успешно закачихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
||||||
data_unpinned: '[※ Успешно откачихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
data_unpinned: '[※ Успешно откачихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
||||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
data_dumped: '[☂ Successfully dumped user data snapshot %1% for %2%. Click to view:](#00fb9a)'
|
||||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||||
@@ -40,6 +41,8 @@ locales:
|
|||||||
save_cause_world_save: 'world save'
|
save_cause_world_save: 'world save'
|
||||||
save_cause_death: 'death'
|
save_cause_death: 'death'
|
||||||
save_cause_server_shutdown: 'server shutdown'
|
save_cause_server_shutdown: 'server shutdown'
|
||||||
|
save_cause_save_command: 'save command'
|
||||||
|
save_cause_dump_command: 'dump command'
|
||||||
save_cause_inventory_command: 'inventory command'
|
save_cause_inventory_command: 'inventory command'
|
||||||
save_cause_enderchest_command: 'enderchest command'
|
save_cause_enderchest_command: 'enderchest command'
|
||||||
save_cause_backup_restore: 'backup restore'
|
save_cause_backup_restore: 'backup restore'
|
||||||
@@ -51,6 +54,9 @@ locales:
|
|||||||
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
|
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
|
||||||
reload_complete: '[HuskSync](#00fb9a bold) [| Презаредихме конфигурацията и файловете със съобщения.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
reload_complete: '[HuskSync](#00fb9a bold) [| Презаредихме конфигурацията и файловете със съобщения.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||||
|
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||||
|
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||||
|
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Грешка:](#ff3300) [Неправилен синтаксис. Използвайте:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
error_invalid_syntax: '[Грешка:](#ff3300) [Неправилен синтаксис. Използвайте:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||||
error_invalid_player: '[Грешка:](#ff3300) [Не можахме да открием играч с това име.](#ff7e5e)'
|
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_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ locales:
|
|||||||
data_list_title: '[Nutzerdaten-Schnappschüsse von %1%:](#00fb9a) [(%2%-%3% von](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[Nutzerdaten-Schnappschüsse von %1%:](#00fb9a) [(%2%-%3% von](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7Nutzerdaten-Schnappschuss für %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Angeheftet:\n&8Angeheftete Schnappschüsse werden nicht automatisch rotiert. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versions-Zeitstempel:&7\n&8Zeitpunkt der Speicherung der Daten\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Speicherungsgrund:\n&8Grund für das Speichern der Daten run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Schnappschuss-Größe:&7\n&8Geschätzte Dateigröße des Schnappschusses (in KiB) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7Nutzerdaten-Schnappschuss für %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Angeheftet:\n&8Angeheftete Schnappschüsse werden nicht automatisch rotiert. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versions-Zeitstempel:&7\n&8Zeitpunkt der Speicherung der Daten\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Speicherungsgrund:\n&8Grund für das Speichern der Daten run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Schnappschuss-Größe:&7\n&8Geschätzte Dateigröße des Schnappschusses (in KiB) run_command=/userdata view %2% %3%)'
|
||||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
|
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||||
data_deleted: '[❌ Nutzerdaten-Schnappschuss erfolgreich gelöscht](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_deleted: '[❌ Nutzerdaten-Schnappschuss erfolgreich gelöscht](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
data_restored: '[⏪ Erfgreich wiederhergestellt](#00fb9a) [Aktuelle Nutzerdaten des Schnappschusses von %1%](#00fb9a show_text=&7Spieler-UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versions-UUID:\n&8%4%)'
|
data_restored: '[⏪ Erfgreich wiederhergestellt](#00fb9a) [Aktuelle Nutzerdaten des Schnappschusses von %1%](#00fb9a show_text=&7Spieler-UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versions-UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Nutzerdaten-Schnappschuss erfolgreich angepinnt](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)'
|
data_pinned: '[※ Nutzerdaten-Schnappschuss erfolgreich angepinnt](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)'
|
||||||
data_unpinned: '[※ Nutzerdaten-Schnappschuss erfolgreich losgelöst](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)'
|
data_unpinned: '[※ Nutzerdaten-Schnappschuss erfolgreich losgelöst](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)'
|
||||||
data_dumped: '[☂ Nutzerdaten-Schnappschuss %1% für %2% erfolgreich gedumpt nach:](#00fb9a) &7%3%'
|
data_dumped: '[☂ Nutzerdaten-Schnappschuss %1% für %2% erfolgreich gedumpt nach:](#00fb9a)'
|
||||||
list_footer: '\n%1%[Seite](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
list_footer: '\n%1%[Seite](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||||
list_previous_page_button: '[◀](white show_text=&7Siehe vorherige Seite run_command=%2% %1%) '
|
list_previous_page_button: '[◀](white show_text=&7Siehe vorherige Seite run_command=%2% %1%) '
|
||||||
list_next_page_button: ' [▶](white show_text=&7Siehe nächste Seite run_command=%2% %1%)'
|
list_next_page_button: ' [▶](white show_text=&7Siehe nächste Seite run_command=%2% %1%)'
|
||||||
@@ -39,18 +40,23 @@ locales:
|
|||||||
save_cause_disconnect: 'Server verlassen'
|
save_cause_disconnect: 'Server verlassen'
|
||||||
save_cause_world_save: 'Welt gespeichert'
|
save_cause_world_save: 'Welt gespeichert'
|
||||||
save_cause_death: 'Tod'
|
save_cause_death: 'Tod'
|
||||||
save_cause_server_shutdown: 'Server gestoppt'
|
save_cause_server_shutdown: 'server gestoppt'
|
||||||
save_cause_inventory_command: 'Inventar Befehl'
|
save_cause_save_command: 'save command'
|
||||||
save_cause_enderchest_command: 'Enderchest Befehl'
|
save_cause_dump_command: 'dump command'
|
||||||
save_cause_backup_restore: 'Backup wiederhergestellt'
|
save_cause_inventory_command: 'inventar Befehl'
|
||||||
|
save_cause_enderchest_command: 'enderchest Befehl'
|
||||||
|
save_cause_backup_restore: 'backup wiederhergestellt'
|
||||||
save_cause_api: 'API'
|
save_cause_api: 'API'
|
||||||
save_cause_mpdb_migration: 'MPDB Migration'
|
save_cause_mpdb_migration: 'MPDB Migration'
|
||||||
save_cause_legacy_migration: 'Legacy Migration'
|
save_cause_legacy_migration: 'legacy Migration'
|
||||||
save_cause_converted_from_v2: 'Import von v2'
|
save_cause_converted_from_v2: 'Import von v2'
|
||||||
reload_complete: '[HuskSync](#00fb9a bold) [| Die Konfigurations- und Sprachdateien wurden neu geladen.](#00fb9a)\n[⚠ Stelle sicher, dass die Konfigurationsdateien auf allen Servern aktuell sind!](#00fb9a)\n[Ein Neustart wird benötigt, damit Konfigurations-Änderungen wirkbar werden.](#00fb9a italic)'
|
reload_complete: '[HuskSync](#00fb9a bold) [| Die Konfigurations- und Sprachdateien wurden neu geladen.](#00fb9a)\n[⚠ Stelle sicher, dass die Konfigurationsdateien auf allen Servern aktuell sind!](#00fb9a)\n[Ein Neustart wird benötigt, damit Konfigurations-Änderungen wirkbar werden.](#00fb9a italic)'
|
||||||
up_to_date: '[HuskSync](#00fb9a bold) [| Du verwendest die neuste Version von HuskSync (v%1%).](#00fb9a)'
|
up_to_date: '[HuskSync](#00fb9a bold) [| Du verwendest die neuste Version von HuskSync (v%1%).](#00fb9a)'
|
||||||
update_available: '[HuskSync](#ff7e5e bold) [| Eine neue Version von HuskSync ist verfügbar: v%1% (Aktuelle Version: v%2%).](#ff7e5e)'
|
update_available: '[HuskSync](#ff7e5e bold) [| Eine neue Version von HuskSync ist verfügbar: v%1% (Aktuelle Version: v%2%).](#ff7e5e)'
|
||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||||
|
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||||
|
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||||
|
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Fehler:](#ff3300) [Falsche Syntax. Nutze:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
error_invalid_syntax: '[Fehler:](#ff3300) [Falsche Syntax. Nutze:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||||
error_invalid_player: '[Fehler:](#ff3300) [Es konnte kein Spieler mit diesem Namen gefunden werden.](#ff7e5e)'
|
error_invalid_player: '[Fehler:](#ff3300) [Es konnte kein Spieler mit diesem Namen gefunden werden.](#ff7e5e)'
|
||||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ locales:
|
|||||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
|
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||||
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
data_unpinned: '[※ Successfully unpinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_unpinned: '[※ Successfully unpinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
data_dumped: '[☂ Successfully dumped user data snapshot %1% for %2%. Click to view:](#00fb9a)'
|
||||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||||
@@ -40,6 +41,8 @@ locales:
|
|||||||
save_cause_world_save: 'world save'
|
save_cause_world_save: 'world save'
|
||||||
save_cause_death: 'death'
|
save_cause_death: 'death'
|
||||||
save_cause_server_shutdown: 'server shutdown'
|
save_cause_server_shutdown: 'server shutdown'
|
||||||
|
save_cause_save_command: 'save command'
|
||||||
|
save_cause_dump_command: 'dump command'
|
||||||
save_cause_inventory_command: 'inventory command'
|
save_cause_inventory_command: 'inventory command'
|
||||||
save_cause_enderchest_command: 'enderchest command'
|
save_cause_enderchest_command: 'enderchest command'
|
||||||
save_cause_backup_restore: 'backup restore'
|
save_cause_backup_restore: 'backup restore'
|
||||||
@@ -51,6 +54,9 @@ locales:
|
|||||||
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
|
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
|
||||||
reload_complete: '[HuskSync](#00fb9a bold) [| Reloaded config and message files.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
reload_complete: '[HuskSync](#00fb9a bold) [| Reloaded config and message files.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||||
|
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||||
|
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||||
|
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Error:](#ff3300) [Incorrect syntax. Usage:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
error_invalid_syntax: '[Error:](#ff3300) [Incorrect syntax. Usage:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||||
error_invalid_player: '[Error:](#ff3300) [Could not find a player by that name.](#ff7e5e)'
|
error_invalid_player: '[Error:](#ff3300) [Could not find a player by that name.](#ff7e5e)'
|
||||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ locales:
|
|||||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
|
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||||
data_deleted: '[❌ Se ha eliminado correctamente la snapshot del usuario](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_deleted: '[❌ Se ha eliminado correctamente la snapshot del usuario](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
data_restored: '[⏪ Restaurado correctamente](#00fb9a) [%1%](#00fb9a show_text=&7UUID del jugador:\n&8%2%)[Informacion actual de la snapshot del jugador](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
data_restored: '[⏪ Restaurado correctamente](#00fb9a) [%1%](#00fb9a show_text=&7UUID del jugador:\n&8%2%)[Informacion actual de la snapshot del jugador](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Se ha anclado perfectamente la snapshot del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7UUID del usuario:\n&8%4%)'
|
data_pinned: '[※ Se ha anclado perfectamente la snapshot del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7UUID del usuario:\n&8%4%)'
|
||||||
data_unpinned: '[※ Se ha desanclado perfectamente la snapshot del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7UUID del usuario:\n&8%4%)'
|
data_unpinned: '[※ Se ha desanclado perfectamente la snapshot del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7UUID del usuario:\n&8%4%)'
|
||||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a)'
|
||||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||||
@@ -40,6 +41,8 @@ locales:
|
|||||||
save_cause_world_save: 'world save'
|
save_cause_world_save: 'world save'
|
||||||
save_cause_death: 'death'
|
save_cause_death: 'death'
|
||||||
save_cause_server_shutdown: 'server shutdown'
|
save_cause_server_shutdown: 'server shutdown'
|
||||||
|
save_cause_save_command: 'save command'
|
||||||
|
save_cause_dump_command: 'dump command'
|
||||||
save_cause_inventory_command: 'inventory command'
|
save_cause_inventory_command: 'inventory command'
|
||||||
save_cause_enderchest_command: 'enderchest command'
|
save_cause_enderchest_command: 'enderchest command'
|
||||||
save_cause_backup_restore: 'backup restore'
|
save_cause_backup_restore: 'backup restore'
|
||||||
@@ -51,6 +54,9 @@ locales:
|
|||||||
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
|
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
|
||||||
reload_complete: '[HuskSync](#00fb9a bold) [| Recargada la configuración y los archivos de lenguaje.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
reload_complete: '[HuskSync](#00fb9a bold) [| Recargada la configuración y los archivos de lenguaje.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||||
|
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||||
|
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||||
|
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Error:](#ff3300) [Sintanxis incorrecta. Usa:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
error_invalid_syntax: '[Error:](#ff3300) [Sintanxis incorrecta. Usa:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||||
error_invalid_player: '[Error:](#ff3300) [No se ha podido encontrar un jugador con ese nombre.](#ff7e5e)'
|
error_invalid_player: '[Error:](#ff3300) [No se ha podido encontrar un jugador con ese nombre.](#ff7e5e)'
|
||||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ locales:
|
|||||||
data_list_title: '[Les instantanés des données utilisateur de %1%:](#00fb9a) [(%2%-%3% sur](#00fb9a)[%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[Les instantanés des données utilisateur de %1%:](#00fb9a) [(%2%-%3% sur](#00fb9a)[%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7Instantané des données utilisateur pour %2%\n&8⚡ %4% run_command=/userdataview %2% %3%) [%5%](#d8ff2b show_text=&7Épinglé:\n&8Les instantanés épinglés ne serontpas automatiquement supprimés. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962show_text=&7Horodatage de la version:&7\n&8Quand les données ont été enregistrées\n&8%7% run_command=/userdataview %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Cause de la sauvegarde:\n&8Ce qui a causél''enregistrement des données run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fashow_text=&7Taille de l''instantané:&7\n&8Taille du fichier estimée de l''instantané (en KiB) run_command=/userdataview %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7Instantané des données utilisateur pour %2%\n&8⚡ %4% run_command=/userdataview %2% %3%) [%5%](#d8ff2b show_text=&7Épinglé:\n&8Les instantanés épinglés ne serontpas automatiquement supprimés. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962show_text=&7Horodatage de la version:&7\n&8Quand les données ont été enregistrées\n&8%7% run_command=/userdataview %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Cause de la sauvegarde:\n&8Ce qui a causél''enregistrement des données run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fashow_text=&7Taille de l''instantané:&7\n&8Taille du fichier estimée de l''instantané (en KiB) run_command=/userdataview %2% %3%)'
|
||||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7Instantané des données utilisateur pour %2%\n&8⚡%4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Épinglé:\n&8Lesinstantanés épinglés ne seront pas automatiquement supprimés. suggest_command=/userdata delete %2%%3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Instantané des donnéesinvalide\n&#ff7e5e&Cliquez pour supprimer\n\n&7⚠ %10% suggest_command=/userdata delete%2% %3%)'
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7Instantané des données utilisateur pour %2%\n&8⚡%4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Épinglé:\n&8Lesinstantanés épinglés ne seront pas automatiquement supprimés. suggest_command=/userdata delete %2%%3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Instantané des donnéesinvalide\n&#ff7e5e&Cliquez pour supprimer\n\n&7⚠ %10% suggest_command=/userdata delete%2% %3%)'
|
||||||
|
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||||
data_deleted: '[❌ Instantané des données utilisateur supprimé avec succès](#00fb9a) [%1%](#00fb9ashow_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%.](#00fb9a show_text=&7UUID du joueur:\n&8%4%)'
|
data_deleted: '[❌ Instantané des données utilisateur supprimé avec succès](#00fb9a) [%1%](#00fb9ashow_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%.](#00fb9a show_text=&7UUID du joueur:\n&8%4%)'
|
||||||
data_restored: '[⏪ Données utilisateur actuelles de %1% restaurées avec succès à partir de l''instantané](#00fb9a) [%3%.](#00fb9a show_text=&7UUID de la version:\n&8%4%)'
|
data_restored: '[⏪ Données utilisateur actuelles de %1% restaurées avec succès à partir de l''instantané](#00fb9a) [%3%.](#00fb9a show_text=&7UUID de la version:\n&8%4%)'
|
||||||
data_pinned: '[※ Instantané des données utilisateur épinglé avec succès](#00fb9a) [%1%](#00fb9ashow_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%.](#00fb9a show_text=&7UUID du joueur:\n&8%4%)'
|
data_pinned: '[※ Instantané des données utilisateur épinglé avec succès](#00fb9a) [%1%](#00fb9ashow_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%.](#00fb9a show_text=&7UUID du joueur:\n&8%4%)'
|
||||||
data_unpinned: '[※ Instantané des données utilisateur détaché avec succès](#00fb9a) [%1%](#00fb9ashow_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%.](#00fb9a show_text=&7UUID du joueur:\n&8%4%)'
|
data_unpinned: '[※ Instantané des données utilisateur détaché avec succès](#00fb9a) [%1%](#00fb9ashow_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%.](#00fb9a show_text=&7UUID du joueur:\n&8%4%)'
|
||||||
data_dumped: '[☂ Dump de l''instantané des données utilisateur %1% pour %2% à:](#00fb9a)&7%3%'
|
data_dumped: '[☂ Dump de l''instantané des données utilisateur %1% pour %2% à:](#00fb9a)'
|
||||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||||
list_previous_page_button: '[◀](white show_text=&7Voir la page précédente run_command=%2%%1%) '
|
list_previous_page_button: '[◀](white show_text=&7Voir la page précédente run_command=%2%%1%) '
|
||||||
list_next_page_button: ' [▶](white show_text=&7Voir la page suivante run_command=%2% %1%)'
|
list_next_page_button: ' [▶](white show_text=&7Voir la page suivante run_command=%2% %1%)'
|
||||||
@@ -40,6 +41,8 @@ locales:
|
|||||||
save_cause_world_save: 'sauvegarde du monde'
|
save_cause_world_save: 'sauvegarde du monde'
|
||||||
save_cause_death: 'mort'
|
save_cause_death: 'mort'
|
||||||
save_cause_server_shutdown: 'arrêt du serveur'
|
save_cause_server_shutdown: 'arrêt du serveur'
|
||||||
|
save_cause_save_command: 'save command'
|
||||||
|
save_cause_dump_command: 'dump command'
|
||||||
save_cause_inventory_command: 'commande d''inventaire'
|
save_cause_inventory_command: 'commande d''inventaire'
|
||||||
save_cause_enderchest_command: 'commande du coffre de l''Ender'
|
save_cause_enderchest_command: 'commande du coffre de l''Ender'
|
||||||
save_cause_backup_restore: 'restauration de sauvegarde'
|
save_cause_backup_restore: 'restauration de sauvegarde'
|
||||||
@@ -51,6 +54,9 @@ locales:
|
|||||||
update_available: '[HuskSync](#ff7e5e bold) [| Une nouvelle version de HuskSync est disponible:v%1% (version actuelle: v%2%).](#ff7e5e)'
|
update_available: '[HuskSync](#ff7e5e bold) [| Une nouvelle version de HuskSync est disponible:v%1% (version actuelle: v%2%).](#ff7e5e)'
|
||||||
reload_complete: '[HuskSync](#00fb9a bold) [| Config et messages rechargés.](#00fb9a)\n[⚠Assurez-vous que les fichiers de configuration sont à jour sur tous les serveurs!](#00fb9a)\n[Un redémarrage est nécessairepour que les modifications de configuration prennent effet.](#00fb9a italic)'
|
reload_complete: '[HuskSync](#00fb9a bold) [| Config et messages rechargés.](#00fb9a)\n[⚠Assurez-vous que les fichiers de configuration sont à jour sur tous les serveurs!](#00fb9a)\n[Un redémarrage est nécessairepour que les modifications de configuration prennent effet.](#00fb9a italic)'
|
||||||
system_status_header: '[HuskSync](#00fb9a bold) [| Rapport d''état du système:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| Rapport d''état du système:](#00fb9a)'
|
||||||
|
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||||
|
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||||
|
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Erreur:](#ff3300) [Syntaxe incorrecte. Utilisation:](#ff7e5e) [%1%](#ff7e5eitalic show_text=&#ff7e5e&Cliquez pour suggérer suggest_command=%1%)'
|
error_invalid_syntax: '[Erreur:](#ff3300) [Syntaxe incorrecte. Utilisation:](#ff7e5e) [%1%](#ff7e5eitalic show_text=&#ff7e5e&Cliquez pour suggérer suggest_command=%1%)'
|
||||||
error_invalid_player: '[Erreur:](#ff3300) [Impossible de trouver un joueur avec ce nom.](#ff7e5e)'
|
error_invalid_player: '[Erreur:](#ff3300) [Impossible de trouver un joueur avec ce nom.](#ff7e5e)'
|
||||||
error_invalid_data: '[Erreur:](#ff3300) [Impossible de déballer les données de l''instantané car elles sont invalides ou corrompues.](#ff7e5e) [(Détails…)](gray show_text=&7⚠ %1%)'
|
error_invalid_data: '[Erreur:](#ff3300) [Impossible de déballer les données de l''instantané car elles sont invalides ou corrompues.](#ff7e5e) [(Détails…)](gray show_text=&7⚠ %1%)'
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ locales:
|
|||||||
data_list_title: '[Cuplikan data %1%:](#00fb9a) [(%2%-%3% dari](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[Cuplikan data %1%:](#00fb9a) [(%2%-%3% dari](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7Cuplikan data pengguna untuk %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Disematkan:\n&8Cuplikan yang disematkan tidak akan dirotasi otomatis. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versi stampel waktu:&7\n&8Saat data disimpan\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Disimpan karena:\n&8Apa yang menyebabkan data disimpan run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Ukuran cuplikan:&7\n&8Perkiraan ukuran file cuplikan (dalam KiB) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7Cuplikan data pengguna untuk %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Disematkan:\n&8Cuplikan yang disematkan tidak akan dirotasi otomatis. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versi stampel waktu:&7\n&8Saat data disimpan\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Disimpan karena:\n&8Apa yang menyebabkan data disimpan run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Ukuran cuplikan:&7\n&8Perkiraan ukuran file cuplikan (dalam KiB) run_command=/userdata view %2% %3%)'
|
||||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
|
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||||
data_deleted: '[❌ Berhasil menghapus cuplikan data pengguna](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
|
data_deleted: '[❌ Berhasil menghapus cuplikan data pengguna](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
|
||||||
data_restored: '[⏪ Berhasil dipulihkan](#00fb9a) [%1%](#00fb9a show_text=&7UUID Pemain:\n&8%2%)[data pengguna saat ini dari cuplikan](#00fb9a) [%3%.](#00fb9a show_text=&7Versi UUID:\n&8%4%)'
|
data_restored: '[⏪ Berhasil dipulihkan](#00fb9a) [%1%](#00fb9a show_text=&7UUID Pemain:\n&8%2%)[data pengguna saat ini dari cuplikan](#00fb9a) [%3%.](#00fb9a show_text=&7Versi UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Berhasil menyematkan cuplikan data pengguna](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
|
data_pinned: '[※ Berhasil menyematkan cuplikan data pengguna](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
|
||||||
data_unpinned: '[※ Berhasil melepaskan cuplikan data pengguna yang disematkan](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
|
data_unpinned: '[※ Berhasil melepaskan cuplikan data pengguna yang disematkan](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
|
||||||
data_dumped: '[☂ Berhasil membuang cuplikan data pengguna %1% untuk %2% ke:](#00fb9a) &7%3%'
|
data_dumped: '[☂ Berhasil membuang cuplikan data pengguna %1% untuk %2% ke:](#00fb9a)'
|
||||||
list_footer: '\n%1%[Halaman](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
list_footer: '\n%1%[Halaman](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||||
list_previous_page_button: '[◀](white show_text=&7Lihat halaman sebelumnya run_command=%2% %1%) '
|
list_previous_page_button: '[◀](white show_text=&7Lihat halaman sebelumnya run_command=%2% %1%) '
|
||||||
list_next_page_button: ' [▶](white show_text=&7Lihat halaman selanjutnya run_command=%2% %1%)'
|
list_next_page_button: ' [▶](white show_text=&7Lihat halaman selanjutnya run_command=%2% %1%)'
|
||||||
@@ -40,6 +41,8 @@ locales:
|
|||||||
save_cause_world_save: 'penyimpanan dunia'
|
save_cause_world_save: 'penyimpanan dunia'
|
||||||
save_cause_death: 'kematian'
|
save_cause_death: 'kematian'
|
||||||
save_cause_server_shutdown: 'pematian server'
|
save_cause_server_shutdown: 'pematian server'
|
||||||
|
save_cause_save_command: 'save command'
|
||||||
|
save_cause_dump_command: 'dump command'
|
||||||
save_cause_inventory_command: 'perintah inventaris'
|
save_cause_inventory_command: 'perintah inventaris'
|
||||||
save_cause_enderchest_command: 'perintah enderchest'
|
save_cause_enderchest_command: 'perintah enderchest'
|
||||||
save_cause_backup_restore: 'pemulihan cadangan'
|
save_cause_backup_restore: 'pemulihan cadangan'
|
||||||
@@ -51,6 +54,9 @@ locales:
|
|||||||
update_available: '[HuskSync](#ff7e5e bold) [| Versi baru HuskSync tersedia: v%1% (menjalankan: v%2%).](#ff7e5e)'
|
update_available: '[HuskSync](#ff7e5e bold) [| Versi baru HuskSync tersedia: v%1% (menjalankan: v%2%).](#ff7e5e)'
|
||||||
reload_complete: '[HuskSync](#00fb9a bold) [| Memuat ulang file konfigurasi dan pesan.](#00fb9a)\n[⚠ Pastikan file konfigurasi sudah diperbarui di semua server!](#00fb9a)\n[Diperlukan pengaktifan ulang agar perubahan konfigurasi dapat diterapkan.](#00fb9a italic)'
|
reload_complete: '[HuskSync](#00fb9a bold) [| Memuat ulang file konfigurasi dan pesan.](#00fb9a)\n[⚠ Pastikan file konfigurasi sudah diperbarui di semua server!](#00fb9a)\n[Diperlukan pengaktifan ulang agar perubahan konfigurasi dapat diterapkan.](#00fb9a italic)'
|
||||||
system_status_header: '[HuskSync](#00fb9a bold) [| Laporan status sistem:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| Laporan status sistem:](#00fb9a)'
|
||||||
|
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||||
|
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||||
|
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Kesalahan:](#ff3300) [Sintaks salah. Penggunaan:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Klik untuk menyarankan suggest_command=%1%)'
|
error_invalid_syntax: '[Kesalahan:](#ff3300) [Sintaks salah. Penggunaan:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Klik untuk menyarankan suggest_command=%1%)'
|
||||||
error_invalid_player: '[Kesalahan:](#ff3300) [Tidak dapat menemukan pemain dengan nama tersebut.](#ff7e5e)'
|
error_invalid_player: '[Kesalahan:](#ff3300) [Tidak dapat menemukan pemain dengan nama tersebut.](#ff7e5e)'
|
||||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ locales:
|
|||||||
data_list_title: '[Lista delle istantanee di %1%:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[Lista delle istantanee di %1%:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7Instantanea di %2%&8⚡ id: %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Fissata:\n&8Se fissata, l''istantanea non viene mai modificata. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Data di salvataggio:&7\n&8Momento preciso in cui è stato salvato il dato\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Causa di salvataggio:\n&8Che cosa ha causato il salvataggio run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Peso dell''istantanea:&7\n&8Peso stimato del file (in KiB) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7Instantanea di %2%&8⚡ id: %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Fissata:\n&8Se fissata, l''istantanea non viene mai modificata. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Data di salvataggio:&7\n&8Momento preciso in cui è stato salvato il dato\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Causa di salvataggio:\n&8Che cosa ha causato il salvataggio run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Peso dell''istantanea:&7\n&8Peso stimato del file (in KiB) run_command=/userdata view %2% %3%)'
|
||||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
|
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||||
data_deleted: '[❌ Istantanea eliminata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_deleted: '[❌ Istantanea eliminata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
data_restored: '[⏪ Ripristato con successo](#00fb9a) [Dati dall''istantanea di](#00fb9a)[%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versione di UUID:\n&8%4%)'
|
data_restored: '[⏪ Ripristato con successo](#00fb9a) [Dati dall''istantanea di](#00fb9a)[%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versione di UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Instantanea fissata](#00fb9a) [%1%](#00fb9a show_text=&7UUID della versione:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_pinned: '[※ Instantanea fissata](#00fb9a) [%1%](#00fb9a show_text=&7UUID della versione:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
data_unpinned: '[※ L''istantanea dei dati utente è stata sbloccata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_unpinned: '[※ L''istantanea dei dati utente è stata sbloccata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
data_dumped: '[☂ Hai ottenuto il dump dell''istantanea %1% di %2% nel formato:](#00fb9a) &7%3%'
|
data_dumped: '[☂ Hai ottenuto il dump dell''istantanea %1% di %2% nel formato:](#00fb9a)'
|
||||||
list_footer: '\n%1%[Pagina](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
list_footer: '\n%1%[Pagina](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||||
list_previous_page_button: '[◀](white show_text=&7Visualizza pagina precedente run_command=%2% %1%) '
|
list_previous_page_button: '[◀](white show_text=&7Visualizza pagina precedente run_command=%2% %1%) '
|
||||||
list_next_page_button: ' [▶](white show_text=&7Visualizza pagina successiva run_command=%2% %1%)'
|
list_next_page_button: ' [▶](white show_text=&7Visualizza pagina successiva run_command=%2% %1%)'
|
||||||
@@ -40,6 +41,8 @@ locales:
|
|||||||
save_cause_world_save: 'world save'
|
save_cause_world_save: 'world save'
|
||||||
save_cause_death: 'death'
|
save_cause_death: 'death'
|
||||||
save_cause_server_shutdown: 'server shutdown'
|
save_cause_server_shutdown: 'server shutdown'
|
||||||
|
save_cause_save_command: 'save command'
|
||||||
|
save_cause_dump_command: 'dump command'
|
||||||
save_cause_inventory_command: 'inventory command'
|
save_cause_inventory_command: 'inventory command'
|
||||||
save_cause_enderchest_command: 'enderchest command'
|
save_cause_enderchest_command: 'enderchest command'
|
||||||
save_cause_backup_restore: 'backup restore'
|
save_cause_backup_restore: 'backup restore'
|
||||||
@@ -51,6 +54,9 @@ locales:
|
|||||||
update_available: '[HuskSync](#ff7e5e bold) [| Disponibile una nuova versione: v%1% (running: v%2%).](#ff7e5e)'
|
update_available: '[HuskSync](#ff7e5e bold) [| Disponibile una nuova versione: v%1% (running: v%2%).](#ff7e5e)'
|
||||||
reload_complete: '[HuskSync](#00fb9a bold) [| Configurazione e messaggi ricaricati.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
reload_complete: '[HuskSync](#00fb9a bold) [| Configurazione e messaggi ricaricati.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||||
|
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||||
|
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||||
|
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Errore:](#ff3300) [Sintassi errata. Usa:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
error_invalid_syntax: '[Errore:](#ff3300) [Sintassi errata. Usa:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||||
error_invalid_player: '[Errore:](#ff3300) [Impossibile trovare un giocatore con questo nome.](#ff7e5e)'
|
error_invalid_player: '[Errore:](#ff3300) [Impossibile trovare un giocatore con questo nome.](#ff7e5e)'
|
||||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user