mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-23 16:49:19 +00:00
Compare commits
185 Commits
3.6.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 |
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
|
|
||||||
29
README.md
29
README.md
@@ -43,8 +43,29 @@
|
|||||||
|
|
||||||
**Ready?** [It's syncing time!](https://william278.net/docs/husksync/setup)
|
**Ready?** [It's syncing time!](https://william278.net/docs/husksync/setup)
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
HuskSync supports the following [compatible versions](https://william278.net/docs/husksync/compatibility) of Minecraft. Since v3.7, you must download the correct version of HuskSync for your server:
|
||||||
|
|
||||||
|
| Minecraft | Latest HuskSync | Java Version | Platforms | Support Status |
|
||||||
|
|:---------------:|:---------------:|:------------:|:--------------|:-----------------------------|
|
||||||
|
| 1.21.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
|
## Setup
|
||||||
Requires a MySQL/Mongo/PostgreSQL database, a Redis (v5.0+) server and a network of Spigot (1.17.1+) or Fabric (1.20.1) Minecraft servers, running Java 17+.
|
Requires a [MySQL/MariaDB/Mongo/PostgreSQL database](https://william278.net/docs/husksync/database), a [Redis (v5.0+) server]((https://william278.net/docs/husksync/redis)) and a network of [compatible Spigot or Fabric Minecraft servers](https://william278.net/docs/husksync/compatibility).
|
||||||
|
|
||||||
1. Place the plugin jar file in the `/plugins` or `/mods` directory of each Spigot/Fabric server. You do not need to install HuskSync as a proxy plugin.
|
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.
|
||||||
@@ -52,12 +73,14 @@ Requires a MySQL/Mongo/PostgreSQL database, a Redis (v5.0+) server and a network
|
|||||||
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 (building requires Java 17). Builds will be output in `/target`:
|
To build HuskSync, simply run the following in the root of the repository (building requires Java 21). Builds will be output in `/target`:
|
||||||
|
|
||||||
```bash
|
```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.
|
||||||
|
|
||||||
@@ -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.
|
||||||
|
|||||||
82
build.gradle
82
build.gradle
@@ -1,10 +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 'fabric-loom' version '1.7-SNAPSHOT' apply false
|
id 'fabric-loom' version "$fabric_loom_version" apply false
|
||||||
id 'org.ajoberstar.grgit' version '5.2.2'
|
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'
|
||||||
}
|
}
|
||||||
@@ -58,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'
|
||||||
|
|
||||||
@@ -73,7 +79,7 @@ allprojects {
|
|||||||
maven { url 'https://repo.william278.net/releases/' }
|
maven { url 'https://repo.william278.net/releases/' }
|
||||||
maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
|
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/' }
|
||||||
@@ -83,13 +89,10 @@ allprojects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -98,21 +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 {
|
||||||
if (['fabric'].contains(project.name)) {
|
// Ignore parent projects (no jars)
|
||||||
apply plugin: 'fabric-loom'
|
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'
|
||||||
@@ -123,13 +150,8 @@ subprojects {
|
|||||||
archiveClassifier.set('')
|
archiveClassifier.set('')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append the Minecraft to the version for Fabric projects
|
|
||||||
if (project.name == 'fabric') {
|
|
||||||
version += "+mc.${fabric_minecraft_version}"
|
|
||||||
}
|
|
||||||
|
|
||||||
// API publishing
|
// API publishing
|
||||||
if (['common', 'bukkit', 'fabric'].contains(project.name)) {
|
if (project.name == 'common' || ['fabric', 'bukkit'].contains(project.parent?.name)) {
|
||||||
java {
|
java {
|
||||||
withSourcesJar()
|
withSourcesJar()
|
||||||
withJavadocJar()
|
withJavadocJar()
|
||||||
@@ -156,12 +178,12 @@ 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
|
||||||
@@ -169,12 +191,12 @@ subprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['fabric'].contains(project.name)) {
|
if (project.parent?.name?.equals('fabric')) {
|
||||||
publications {
|
publications {
|
||||||
mavenJavaFabric(MavenPublication) {
|
"mavenJavaFabric_${project.name.replace('.', '_')}"(MavenPublication) {
|
||||||
groupId = 'net.william278.husksync'
|
groupId = 'net.william278.husksync'
|
||||||
artifactId = 'husksync-fabric'
|
artifactId = 'husksync-fabric'
|
||||||
version = "$rootProject.version+${fabric_minecraft_version}"
|
version = "$rootProject.version+$project.name"
|
||||||
artifact remapJar
|
artifact remapJar
|
||||||
artifact sourcesJar
|
artifact sourcesJar
|
||||||
artifact javadocJar
|
artifact javadocJar
|
||||||
@@ -184,7 +206,7 @@ subprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jar.dependsOn(shadowJar)
|
jar.dependsOn shadowJar
|
||||||
clean.delete "$rootDir/target"
|
clean.delete "$rootDir/target"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +221,7 @@ def versionMetadata() {
|
|||||||
|
|
||||||
// If unclean, return the last commit hash with -indev
|
// If unclean, return the last commit hash with -indev
|
||||||
if (!grgit.status().clean) {
|
if (!grgit.status().clean) {
|
||||||
return '-' + grgit.head().abbreviatedId + '-indev'
|
return '-' + grgit.head().abbreviatedId + '-indev'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise if this matches a tag, return nothing
|
// Otherwise if this matches a tag, return nothing
|
||||||
|
|||||||
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 'net.william278.uniform:uniform-bukkit:1.1.5'
|
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 'org.bstats:bstats-bukkit:3.0.2'
|
implementation 'net.kyori:adventure-platform-bukkit:4.3.4'
|
||||||
implementation 'net.kyori:adventure-platform-bukkit:4.3.3'
|
implementation 'dev.triumphteam:triumph-gui:3.1.11'
|
||||||
implementation 'dev.triumphteam:triumph-gui:3.1.10'
|
|
||||||
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
|
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
|
||||||
implementation 'de.tr7zw:item-nbt-api:2.13.1-SNAPSHOT'
|
implementation 'de.tr7zw:item-nbt-api:2.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'
|
||||||
@@ -43,10 +73,10 @@ shadowJar {
|
|||||||
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.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'
|
||||||
@@ -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;
|
||||||
@@ -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,9 +56,11 @@ 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.Uniform;
|
||||||
import net.william278.uniform.bukkit.BukkitUniform;
|
import net.william278.uniform.bukkit.BukkitUniform;
|
||||||
import org.bstats.bukkit.Metrics;
|
import org.bstats.bukkit.Metrics;
|
||||||
@@ -78,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>.
|
||||||
@@ -98,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;
|
||||||
@@ -127,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();
|
||||||
@@ -136,6 +143,10 @@ 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
|
// Register commands
|
||||||
initialize("commands", (plugin) -> getUniform().register(PluginCommand.Type.create(this)));
|
initialize("commands", (plugin) -> getUniform().register(PluginCommand.Type.create(this)));
|
||||||
@@ -290,7 +301,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
@Override
|
@Override
|
||||||
public boolean isDependencyLoaded(@NotNull String name) {
|
public boolean isDependencyLoaded(@NotNull String name) {
|
||||||
final Plugin plugin = getServer().getPluginManager().getPlugin(name);
|
final Plugin plugin = getServer().getPluginManager().getPlugin(name);
|
||||||
return plugin != null && plugin.isEnabled();
|
return plugin != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register bStats metrics
|
// Register bStats metrics
|
||||||
@@ -327,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();
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
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.Uniform;
|
||||||
@@ -29,7 +30,7 @@ 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
|
||||||
@@ -45,6 +46,12 @@ 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
|
@Override
|
||||||
@NotNull
|
@NotNull
|
||||||
public Uniform getUniform() {
|
public Uniform getUniform() {
|
||||||
@@ -62,7 +62,7 @@ public class BukkitHuskSyncAPI extends HuskSyncAPI {
|
|||||||
public static BukkitHuskSyncAPI getInstance() {
|
public static BukkitHuskSyncAPI getInstance() {
|
||||||
if (!JavaPlugin.getProvidingPlugin(BukkitHuskSyncAPI.class).getName().equals("HuskSync")) {
|
if (!JavaPlugin.getProvidingPlugin(BukkitHuskSyncAPI.class).getName().equals("HuskSync")) {
|
||||||
throw new NotRegisteredException("This is likely because you have shaded HuskSync into your plugin JAR " +
|
throw new NotRegisteredException("This is likely because you have shaded HuskSync into your plugin JAR " +
|
||||||
"and need to fix your maven/gradle/build script so that it *compiles against* HuskSync instead.");
|
"and need to fix your maven/gradle/build script so that it *compiles against* HuskSync instead.");
|
||||||
}
|
}
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
throw new NotRegisteredException();
|
throw new NotRegisteredException();
|
||||||
|
|||||||
@@ -26,21 +26,23 @@ import de.tr7zw.changeme.nbtapi.NBTCompound;
|
|||||||
import de.tr7zw.changeme.nbtapi.NBTPersistentDataContainer;
|
import de.tr7zw.changeme.nbtapi.NBTPersistentDataContainer;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import net.william278.desertwell.util.ThrowingConsumer;
|
import net.william278.desertwell.util.ThrowingConsumer;
|
||||||
import net.william278.desertwell.util.Version;
|
|
||||||
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;
|
||||||
@@ -48,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;
|
||||||
@@ -156,20 +159,17 @@ 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) {
|
||||||
try {
|
final org.bukkit.inventory.Inventory inventory = player.getOpenInventory().getTopInventory();
|
||||||
final org.bukkit.inventory.Inventory inventory = player.getOpenInventory().getTopInventory();
|
if (inventory.getType() == InventoryType.CRAFTING) {
|
||||||
if (inventory.getType() == InventoryType.CRAFTING) {
|
for (int slot = 0; slot < 5; slot++) {
|
||||||
for (int slot = 0; slot < 5; slot++) {
|
inventory.setItem(slot, null);
|
||||||
inventory.setItem(slot, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
|
||||||
// Ignore any exceptions
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,8 +236,9 @@ 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
|
||||||
@@ -261,7 +262,7 @@ public abstract class BukkitData implements Data {
|
|||||||
@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
|
||||||
@@ -277,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(),
|
||||||
@@ -364,7 +366,7 @@ public abstract class BukkitData implements Data {
|
|||||||
|
|
||||||
// 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()
|
if (!toAward.isEmpty()
|
||||||
&& (player.getLevel() != expLevel || player.getExp() != expProgress)) {
|
&& (player.getLevel() != expLevel || player.getExp() != expProgress)) {
|
||||||
player.setLevel(expLevel);
|
player.setLevel(expLevel);
|
||||||
player.setExp(expProgress);
|
player.setExp(expProgress);
|
||||||
}
|
}
|
||||||
@@ -373,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,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);
|
||||||
@@ -478,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 -> {
|
@NotNull Map<String, Map<String, Integer>> map) {
|
||||||
if ((material.isBlock() && !isBlock) || (material.isItem() && isBlock)) {
|
registry.forEach(i -> {
|
||||||
return;
|
try {
|
||||||
}
|
final int stat = i instanceof Material m ? p.getStatistic(id, m) :
|
||||||
final int stat = p.getStatistic(id, material);
|
(i instanceof EntityType e ? p.getStatistic(id, e) : -1);
|
||||||
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(material.getKey().getKey(), stat);
|
.put(i.getKey().getKey(), stat);
|
||||||
}
|
}
|
||||||
});
|
} catch (IllegalStateException ignored) {
|
||||||
}
|
|
||||||
|
|
||||||
private static void addEntityStatistic(@NotNull Player p, @NotNull Statistic id,
|
|
||||||
@NotNull Map<String, Map<String, Integer>> map) {
|
|
||||||
Registry.ENTITY_TYPE.forEach(entity -> {
|
|
||||||
if (!entity.isAlive()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final int stat = p.getStatistic(id, entity);
|
|
||||||
if (stat != 0) {
|
|
||||||
map.computeIfAbsent(id.getKey().getKey(), k -> Maps.newHashMap())
|
|
||||||
.put(entity.getKey().getKey(), stat);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@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);
|
||||||
@@ -528,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,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;
|
||||||
@@ -570,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, plugin.getMinecraftVersion()));
|
attributes.add(adapt(instance, settings));
|
||||||
});
|
});
|
||||||
return new BukkitData.Attributes(attributes);
|
return new BukkitData.Attributes(attributes);
|
||||||
}
|
}
|
||||||
@@ -596,47 +591,87 @@ public abstract class BukkitData implements Data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private static Attribute adapt(@NotNull AttributeInstance instance, @NotNull Version version) {
|
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(m -> adapt(m, version)).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, @NotNull Version version) {
|
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(
|
||||||
version.compareTo(Version.fromString("1.21")) >= 0 ? null : 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() : attribute.baseValue());
|
|
||||||
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
|
||||||
@@ -696,11 +731,12 @@ public abstract class BukkitData implements Data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set health scale
|
// Set health scale
|
||||||
|
double scale = healthScale <= 0 ? player.getMaxHealth() : healthScale;
|
||||||
try {
|
try {
|
||||||
player.setHealthScale(healthScale);
|
player.setHealthScale(scale);
|
||||||
player.setHealthScaled(isHealthScaled);
|
player.setHealthScaled(isHealthScaled);
|
||||||
} 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -153,20 +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;
|
|
||||||
case "1.20.5", "1.20.6" -> DataFixerUtil.VERSION1_20_5;
|
|
||||||
default -> DataFixerUtil.getCurrentVersion();
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -228,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
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -163,7 +163,7 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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
|
||||||
@@ -78,7 +79,20 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPacketReceive(PacketReceiveEvent event) {
|
public void onPacketReceive(PacketReceiveEvent event) {
|
||||||
if(!(event.getPacketType() instanceof PacketType.Play.Client client)) {
|
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!CANCEL_PACKETS.contains(client)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (listener.cancelPlayerEvent(event.getUser().getUUID())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPacketSend(PacketSendEvent event) {
|
||||||
|
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!CANCEL_PACKETS.contains(client)) {
|
if (!CANCEL_PACKETS.contains(client)) {
|
||||||
|
|||||||
@@ -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,45 +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.ShulkerBox;
|
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.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
|
||||||
@@ -96,14 +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 ShulkerBox box) {
|
} else if (item.getItemMeta() instanceof BlockStateMeta b && b.getBlockState() instanceof Container box) {
|
||||||
forEachMap(box.getInventory().getContents(), function);
|
forEachMap(box.getInventory().getContents(), function);
|
||||||
b.setBlockState(box);
|
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());
|
||||||
@@ -122,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()));
|
||||||
@@ -130,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());
|
||||||
@@ -146,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);
|
||||||
}
|
map.setItemMeta(meta);
|
||||||
}
|
getPlugin().debug(String.format("Map ID set to %s", newId));
|
||||||
if (world.isPresent()) {
|
return;
|
||||||
final String uid = world.get();
|
|
||||||
final Optional<MapView> existingView = this.getMapView(mapIds.getInteger(uid));
|
|
||||||
if (existingView.isPresent()) {
|
|
||||||
final MapView view = existingView.get();
|
|
||||||
view.setLocked(true);
|
|
||||||
meta.setMapView(view);
|
|
||||||
map.setItemMeta(meta);
|
|
||||||
getPlugin().debug(String.format("View exists (#%s); updated map (UID: %s)", view.getId(), uid));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the pixel data and generate a map view otherwise
|
// Read the pixel data and generate a map view otherwise
|
||||||
final MapData canvasData;
|
getPlugin().debug("Deserializing map data from NBT and generating view...");
|
||||||
try {
|
final @Nullable Map.Entry<MapData, Boolean> readMapData = readMapData(originServerName, originalMapId);
|
||||||
getPlugin().debug("Deserializing map data from NBT and generating view...");
|
if (readMapData == null) {
|
||||||
canvasData = MapData.fromByteArray(Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY),
|
getPlugin().debug("Read pixel data was not found in database, skipping...");
|
||||||
"Map pixel data is null"));
|
|
||||||
} catch (Throwable e) {
|
|
||||||
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);
|
||||||
@@ -230,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) {
|
||||||
@@ -294,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;
|
||||||
@@ -326,42 +394,47 @@ public interface BukkitMapPersister {
|
|||||||
@NotNull
|
@NotNull
|
||||||
private static MapCursor createBannerCursor(@NotNull MapBanner banner) {
|
private static MapCursor createBannerCursor(@NotNull MapBanner banner) {
|
||||||
return new MapCursor(
|
return new MapCursor(
|
||||||
(byte) banner.getPosition().getX(),
|
(byte) banner.getPosition().getX(),
|
||||||
(byte) banner.getPosition().getZ(),
|
(byte) banner.getPosition().getZ(),
|
||||||
(byte) 8, // Always rotate banners upright
|
(byte) 8, // Always rotate banners upright
|
||||||
switch (banner.getColor().toLowerCase(Locale.ENGLISH)) {
|
switch (banner.getColor().toLowerCase(Locale.ENGLISH)) {
|
||||||
case "white" -> MapCursor.Type.BANNER_WHITE;
|
case "white" -> MapCursor.Type.BANNER_WHITE;
|
||||||
case "orange" -> MapCursor.Type.BANNER_ORANGE;
|
case "orange" -> MapCursor.Type.BANNER_ORANGE;
|
||||||
case "magenta" -> MapCursor.Type.BANNER_MAGENTA;
|
case "magenta" -> MapCursor.Type.BANNER_MAGENTA;
|
||||||
case "light_blue" -> MapCursor.Type.BANNER_LIGHT_BLUE;
|
case "light_blue" -> MapCursor.Type.BANNER_LIGHT_BLUE;
|
||||||
case "yellow" -> MapCursor.Type.BANNER_YELLOW;
|
case "yellow" -> MapCursor.Type.BANNER_YELLOW;
|
||||||
case "lime" -> MapCursor.Type.BANNER_LIME;
|
case "lime" -> MapCursor.Type.BANNER_LIME;
|
||||||
case "pink" -> MapCursor.Type.BANNER_PINK;
|
case "pink" -> MapCursor.Type.BANNER_PINK;
|
||||||
case "gray" -> MapCursor.Type.BANNER_GRAY;
|
case "gray" -> MapCursor.Type.BANNER_GRAY;
|
||||||
case "light_gray" -> MapCursor.Type.BANNER_LIGHT_GRAY;
|
case "light_gray" -> MapCursor.Type.BANNER_LIGHT_GRAY;
|
||||||
case "cyan" -> MapCursor.Type.BANNER_CYAN;
|
case "cyan" -> MapCursor.Type.BANNER_CYAN;
|
||||||
case "purple" -> MapCursor.Type.BANNER_PURPLE;
|
case "purple" -> MapCursor.Type.BANNER_PURPLE;
|
||||||
case "blue" -> MapCursor.Type.BANNER_BLUE;
|
case "blue" -> MapCursor.Type.BANNER_BLUE;
|
||||||
case "brown" -> MapCursor.Type.BANNER_BROWN;
|
case "brown" -> MapCursor.Type.BANNER_BROWN;
|
||||||
case "green" -> MapCursor.Type.BANNER_GREEN;
|
case "green" -> MapCursor.Type.BANNER_GREEN;
|
||||||
case "red" -> MapCursor.Type.BANNER_RED;
|
case "red" -> MapCursor.Type.BANNER_RED;
|
||||||
default -> MapCursor.Type.BANNER_BLACK;
|
default -> MapCursor.Type.BANNER_BLACK;
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
banner.getText().isEmpty() ? null : banner.getText()
|
banner.getText().isEmpty() ? null : banner.getText()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,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
|
||||||
@@ -424,27 +517,34 @@ 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, ""),
|
||||||
cursor.getCaption() == null ? "" : cursor.getCaption(),
|
cursor.getCaption() == null ? "" : cursor.getCaption(),
|
||||||
cursor.getX(),
|
cursor.getX(),
|
||||||
mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128,
|
mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128,
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,10 +204,10 @@ public class LegacyMigrator extends Migrator {
|
|||||||
}) {
|
}) {
|
||||||
plugin.log(Level.INFO, getHelpMenu());
|
plugin.log(Level.INFO, getHelpMenu());
|
||||||
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
|
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
|
||||||
obfuscateDataString(args[1]));
|
obfuscateDataString(args[1]));
|
||||||
} else {
|
} else {
|
||||||
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
|
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
|
||||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
plugin.log(Level.INFO, getHelpMenu());
|
plugin.log(Level.INFO, getHelpMenu());
|
||||||
|
|||||||
@@ -201,10 +201,10 @@ public class MpdbMigrator extends Migrator {
|
|||||||
}) {
|
}) {
|
||||||
plugin.log(Level.INFO, getHelpMenu());
|
plugin.log(Level.INFO, getHelpMenu());
|
||||||
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
|
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
|
||||||
obfuscateDataString(args[1]));
|
obfuscateDataString(args[1]));
|
||||||
} else {
|
} else {
|
||||||
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
|
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
|
||||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
plugin.log(Level.INFO, getHelpMenu());
|
plugin.log(Level.INFO, getHelpMenu());
|
||||||
@@ -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}
|
||||||
*/
|
*/
|
||||||
@@ -68,20 +62,12 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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
|
||||||
@@ -92,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(
|
||||||
|
|||||||
@@ -51,7 +51,11 @@ public final class BukkitKeyedAdapter {
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static PotionEffectType matchEffectType(@NotNull String key) {
|
public static PotionEffectType matchEffectType(@NotNull String key) {
|
||||||
return PotionEffectType.getByName(key); // No registry for this in 1.17 API
|
//#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) {
|
||||||
|
|||||||
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,26 +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 'net.william278.uniform:uniform-common:1.1.5'
|
compileOnlyApi 'net.william278.toilet:toilet-common:1.0.12'
|
||||||
|
|
||||||
|
compileOnly 'net.william278.uniform:uniform-common:1.3.1'
|
||||||
compileOnly 'com.mojang:brigadier:1.1.8'
|
compileOnly 'com.mojang:brigadier:1.1.8'
|
||||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
compileOnly 'org.projectlombok:lombok:1.18.36'
|
||||||
compileOnly 'org.jetbrains:annotations:24.1.0'
|
compileOnly 'org.jetbrains:annotations:26.0.2'
|
||||||
compileOnly 'net.kyori:adventure-api:4.17.0'
|
compileOnly 'net.kyori:adventure-api:4.19.0'
|
||||||
compileOnly 'net.kyori:adventure-platform-api:4.3.3'
|
compileOnly 'net.kyori:adventure-platform-api:4.3.4'
|
||||||
compileOnly 'com.google.guava:guava:33.2.1-jre'
|
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"
|
||||||
@@ -33,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.1-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'
|
||||||
}
|
}
|
||||||
@@ -34,11 +34,14 @@ import net.william278.husksync.data.Identifier;
|
|||||||
import net.william278.husksync.data.SerializerRegistry;
|
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 net.william278.uniform.Uniform;
|
||||||
@@ -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, SerializerRegistry {
|
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider, SerializerRegistry,
|
||||||
|
CompatibilityChecker, DumpProvider {
|
||||||
|
|
||||||
int SPIGOT_RESOURCE_ID = 97144;
|
int SPIGOT_RESOURCE_ID = 97144;
|
||||||
|
|
||||||
@@ -247,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
|
||||||
*
|
*
|
||||||
@@ -255,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
|
||||||
*
|
*
|
||||||
@@ -265,10 +285,10 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
|||||||
@NotNull
|
@NotNull
|
||||||
default UpdateChecker getUpdateChecker() {
|
default UpdateChecker getUpdateChecker() {
|
||||||
return UpdateChecker.builder()
|
return UpdateChecker.builder()
|
||||||
.currentVersion(getPluginVersion())
|
.currentVersion(getPluginVersion())
|
||||||
.endpoint(UpdateChecker.Endpoint.SPIGOT)
|
.endpoint(UpdateChecker.Endpoint.SPIGOT)
|
||||||
.resource(Integer.toString(SPIGOT_RESOURCE_ID))
|
.resource(Integer.toString(SPIGOT_RESOURCE_ID))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
default void checkForUpdates() {
|
default void checkForUpdates() {
|
||||||
@@ -276,14 +296,17 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
|||||||
getUpdateChecker().check().thenAccept(checked -> {
|
getUpdateChecker().check().thenAccept(checked -> {
|
||||||
if (!checked.isUpToDate()) {
|
if (!checked.isUpToDate()) {
|
||||||
log(Level.WARNING, String.format(
|
log(Level.WARNING, String.format(
|
||||||
"A new version of HuskSync is available: v%s (running v%s)",
|
"A new version of HuskSync is available: v%s (running v%s)",
|
||||||
checked.getLatestVersion(), getPluginVersion())
|
checked.getLatestVersion(), getPluginVersion())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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>
|
||||||
@@ -320,17 +343,21 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
|||||||
final class FailedToLoadException extends IllegalStateException {
|
final class FailedToLoadException extends IllegalStateException {
|
||||||
|
|
||||||
private static final String FORMAT = """
|
private static final String FORMAT = """
|
||||||
HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized.
|
HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized.
|
||||||
Please make sure the plugin has been setup correctly (https://william278.net/docs/husksync/setup):
|
Please make sure the plugin has been setup correctly (https://william278.net/docs/husksync/setup):
|
||||||
|
|
||||||
1) Make sure you've entered your MySQL, MariaDB or MongoDB database details correctly in config.yml
|
1) Make sure you've entered your MySQL, MariaDB or MongoDB database details correctly in config.yml
|
||||||
2) Make sure your Redis server details are also correct in config.yml
|
2) Make sure your Redis server details are also correct in config.yml
|
||||||
3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-file)
|
3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-file)
|
||||||
4) Check the error below for more details
|
4) Check the error below for more details
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("enderchest", List.of("echest", "openechest"), plugin);
|
super("enderchest", List.of("echest", "openechest"), DataSnapshot.SaveCause.ENDERCHEST_COMMAND, plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -46,29 +45,29 @@ public class EnderChestCommand extends ItemsCommand {
|
|||||||
final Optional<Data.Items.EnderChest> optionalEnderChest = snapshot.getEnderChest();
|
final Optional<Data.Items.EnderChest> optionalEnderChest = snapshot.getEnderChest();
|
||||||
if (optionalEnderChest.isEmpty()) {
|
if (optionalEnderChest.isEmpty()) {
|
||||||
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("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);
|
||||||
|
|
||||||
// Show GUI
|
// Show GUI
|
||||||
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) -> {
|
||||||
if (allowEdit && !enderChest.equals(itemsOnClose)) {
|
if (allowEdit && !enderChest.equals(itemsOnClose)) {
|
||||||
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
|
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,24 +77,23 @@ public class EnderChestCommand extends ItemsCommand {
|
|||||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
|
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
|
||||||
if (latestData.isEmpty()) {
|
if (latestData.isEmpty()) {
|
||||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||||
.ifPresent(viewer::sendMessage);
|
.ifPresent(viewer::sendMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,25 +24,29 @@ 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.util.LegacyConverter;
|
||||||
|
import net.william278.husksync.util.StatusLine;
|
||||||
import net.william278.uniform.BaseCommand;
|
import net.william278.uniform.BaseCommand;
|
||||||
import net.william278.uniform.CommandProvider;
|
import net.william278.uniform.CommandProvider;
|
||||||
import net.william278.uniform.Permission;
|
import net.william278.uniform.Permission;
|
||||||
import net.william278.uniform.element.ArgumentElement;
|
import net.william278.uniform.element.ArgumentElement;
|
||||||
import org.apache.commons.text.WordUtils;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Function;
|
import java.util.UUID;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -52,41 +56,42 @@ public class HuskSyncCommand extends PluginCommand {
|
|||||||
private final AboutMenu aboutMenu;
|
private final AboutMenu aboutMenu;
|
||||||
|
|
||||||
public HuskSyncCommand(@NotNull HuskSync plugin) {
|
public HuskSyncCommand(@NotNull HuskSync plugin) {
|
||||||
super("husksync", List.of(), Permission.Default.TRUE, plugin);
|
super("husksync", List.of(), Permission.Default.TRUE, ExecutionScope.ALL, plugin);
|
||||||
this.updateChecker = plugin.getUpdateChecker();
|
this.updateChecker = plugin.getUpdateChecker();
|
||||||
this.aboutMenu = AboutMenu.builder()
|
this.aboutMenu = AboutMenu.builder()
|
||||||
.title(Component.text("HuskSync"))
|
.title(Component.text("HuskSync"))
|
||||||
.description(Component.text("A modern, cross-server player data synchronization system"))
|
.description(Component.text("A modern, cross-server player data synchronization system"))
|
||||||
.version(plugin.getPluginVersion())
|
.version(plugin.getPluginVersion())
|
||||||
.credits("Author",
|
.credits("Author",
|
||||||
AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net"))
|
AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net"))
|
||||||
.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("hanbings").description("Code (Fabric porting)"),
|
||||||
AboutMenu.Credit.of("Stampede2011").description("Code (Fabric mixins)"))
|
AboutMenu.Credit.of("Stampede2011").description("Code (Fabric mixins)"),
|
||||||
.credits("Translators",
|
AboutMenu.Credit.of("VinerDream").description("Code"))
|
||||||
AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"),
|
.credits("Translators",
|
||||||
AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"),
|
AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"),
|
||||||
AboutMenu.Credit.of("Melonzio").description("Spanish (es-es)"),
|
AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"),
|
||||||
AboutMenu.Credit.of("Ceddix").description("German (de-de)"),
|
AboutMenu.Credit.of("Melonzio").description("Spanish (es-es)"),
|
||||||
AboutMenu.Credit.of("Pukejoy_1").description("Bulgarian (bg-bg)"),
|
AboutMenu.Credit.of("Ceddix").description("German (de-de)"),
|
||||||
AboutMenu.Credit.of("mateusneresrb").description("Brazilian Portuguese (pt-br)"),
|
AboutMenu.Credit.of("Pukejoy_1").description("Bulgarian (bg-bg)"),
|
||||||
AboutMenu.Credit.of("小蔡").description("Traditional Chinese (zh-tw)"),
|
AboutMenu.Credit.of("mateusneresrb").description("Brazilian Portuguese (pt-br)"),
|
||||||
AboutMenu.Credit.of("Ghost-chu").description("Simplified Chinese (zh-cn)"),
|
AboutMenu.Credit.of("小蔡").description("Traditional Chinese (zh-tw)"),
|
||||||
AboutMenu.Credit.of("DJelly4K").description("Simplified Chinese (zh-cn)"),
|
AboutMenu.Credit.of("Ghost-chu").description("Simplified Chinese (zh-cn)"),
|
||||||
AboutMenu.Credit.of("Thourgard").description("Ukrainian (uk-ua)"),
|
AboutMenu.Credit.of("DJelly4K").description("Simplified Chinese (zh-cn)"),
|
||||||
AboutMenu.Credit.of("xF3d3").description("Italian (it-it)"),
|
AboutMenu.Credit.of("Thourgard").description("Ukrainian (uk-ua)"),
|
||||||
AboutMenu.Credit.of("cada3141").description("Korean (ko-kr)"),
|
AboutMenu.Credit.of("xF3d3").description("Italian (it-it)"),
|
||||||
AboutMenu.Credit.of("Wirayuda5620").description("Indonesian (id-id)"),
|
AboutMenu.Credit.of("cada3141").description("Korean (ko-kr)"),
|
||||||
AboutMenu.Credit.of("WinTone01").description("Turkish (tr-tr)"),
|
AboutMenu.Credit.of("Wirayuda5620").description("Indonesian (id-id)"),
|
||||||
AboutMenu.Credit.of("IbanEtchep").description("French (fr-fr)"))
|
AboutMenu.Credit.of("WinTone01").description("Turkish (tr-tr)"),
|
||||||
.buttons(
|
AboutMenu.Credit.of("IbanEtchep").description("French (fr-fr)"))
|
||||||
AboutMenu.Link.of("https://william278.net/docs/husksync").text("Documentation").icon("⛏"),
|
.buttons(
|
||||||
AboutMenu.Link.of("https://github.com/WiIIiam278/HuskSync/issues").text("Issues").icon("❌").color(TextColor.color(0xff9f0f)),
|
AboutMenu.Link.of("https://william278.net/docs/husksync").text("Documentation").icon("⛏"),
|
||||||
AboutMenu.Link.of("https://discord.gg/tVYhJfyDWG").text("Discord").icon("⭐").color(TextColor.color(0x6773f5)))
|
AboutMenu.Link.of("https://github.com/WiIIiam278/HuskSync/issues").text("Issues").icon("❌").color(TextColor.color(0xff9f0f)),
|
||||||
.build();
|
AboutMenu.Link.of("https://discord.gg/tVYhJfyDWG").text("Discord").icon("⭐").color(TextColor.color(0x6773f5)))
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -94,8 +99,10 @@ public class HuskSyncCommand extends PluginCommand {
|
|||||||
command.setDefaultExecutor((ctx) -> about(command, ctx));
|
command.setDefaultExecutor((ctx) -> about(command, ctx));
|
||||||
command.addSubCommand("about", (sub) -> sub.setDefaultExecutor((ctx) -> about(command, ctx)));
|
command.addSubCommand("about", (sub) -> sub.setDefaultExecutor((ctx) -> about(command, ctx)));
|
||||||
command.addSubCommand("status", needsOp("status"), status());
|
command.addSubCommand("status", needsOp("status"), status());
|
||||||
|
command.addSubCommand("dump", needsOp("dump"), dump());
|
||||||
command.addSubCommand("reload", needsOp("reload"), reload());
|
command.addSubCommand("reload", needsOp("reload"), reload());
|
||||||
command.addSubCommand("update", needsOp("update"), update());
|
command.addSubCommand("update", needsOp("update"), update());
|
||||||
|
command.addSubCommand("forceupgrade", forceUpgrade());
|
||||||
command.addSubCommand("migrate", migrate());
|
command.addSubCommand("migrate", migrate());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,12 +116,32 @@ public class HuskSyncCommand extends PluginCommand {
|
|||||||
final CommandUser user = user(sub, ctx);
|
final CommandUser user = user(sub, ctx);
|
||||||
plugin.getLocales().getLocale("system_status_header").ifPresent(user::sendMessage);
|
plugin.getLocales().getLocale("system_status_header").ifPresent(user::sendMessage);
|
||||||
user.sendMessage(Component.join(
|
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()
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@NotNull
|
||||||
private CommandProvider reload() {
|
private CommandProvider reload() {
|
||||||
return (sub) -> sub.setDefaultExecutor((ctx) -> {
|
return (sub) -> sub.setDefaultExecutor((ctx) -> {
|
||||||
@@ -126,7 +153,7 @@ public class HuskSyncCommand extends PluginCommand {
|
|||||||
plugin.getLocales().getLocale("reload_complete").ifPresent(user::sendMessage);
|
plugin.getLocales().getLocale("reload_complete").ifPresent(user::sendMessage);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
user.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);
|
||||||
}
|
}
|
||||||
@@ -139,11 +166,11 @@ public class HuskSyncCommand extends PluginCommand {
|
|||||||
final CommandUser user = user(sub, ctx);
|
final CommandUser user = user(sub, ctx);
|
||||||
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(user::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(user::sendMessage);
|
plugin.getPluginVersion().toString()).ifPresent(user::sendMessage);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,14 +179,18 @@ public class HuskSyncCommand extends PluginCommand {
|
|||||||
return (sub) -> {
|
return (sub) -> {
|
||||||
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
|
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
|
||||||
sub.setDefaultExecutor((ctx) -> {
|
sub.setDefaultExecutor((ctx) -> {
|
||||||
plugin.log(Level.INFO, "Please choose a migrator, then run \"husksync migrate <migrator>\"");
|
plugin.log(Level.INFO, "Please choose a migrator, then run \"husksync migrate start <migrator>\"");
|
||||||
plugin.log(Level.INFO, String.format(
|
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) -> {
|
sub.addSubCommand("start", (start) -> start.addSyntax((cmd) -> {
|
||||||
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
|
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
|
||||||
migrator.start().thenAccept(succeeded -> {
|
migrator.start().thenAccept(succeeded -> {
|
||||||
@@ -173,17 +204,46 @@ public class HuskSyncCommand extends PluginCommand {
|
|||||||
sub.addSubCommand("set", (set) -> set.addSyntax((cmd) -> {
|
sub.addSubCommand("set", (set) -> set.addSyntax((cmd) -> {
|
||||||
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
|
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
|
||||||
final String[] args = cmd.getArgument("args", String.class).split(" ");
|
final String[] args = cmd.getArgument("args", String.class).split(" ");
|
||||||
migrator.handleConfigurationCommand(Arrays.copyOfRange(args, 3, args.length));
|
migrator.handleConfigurationCommand(args);
|
||||||
}, migrator(), BaseCommand.greedyString("args")));
|
}, migrator(), BaseCommand.greedyString("args")));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private CommandProvider forceUpgrade() {
|
||||||
|
return (sub) -> {
|
||||||
|
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
|
||||||
|
sub.setDefaultExecutor((ctx) -> {
|
||||||
|
final LegacyConverter converter = plugin.getLegacyConverter().orElse(null);
|
||||||
|
if (converter == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.runAsync(() -> {
|
||||||
|
final Database database = plugin.getDatabase();
|
||||||
|
plugin.log(Level.INFO, "Beginning forced legacy data upgrade for all users...");
|
||||||
|
database.getAllUsers().forEach(user -> database.getLatestSnapshot(user).ifPresent(snapshot -> {
|
||||||
|
final DataSnapshot.Packed upgraded = converter.convert(
|
||||||
|
snapshot.asBytes(plugin),
|
||||||
|
UUID.randomUUID(),
|
||||||
|
OffsetDateTime.now()
|
||||||
|
);
|
||||||
|
upgraded.setSaveCause(DataSnapshot.SaveCause.CONVERTED_FROM_V2);
|
||||||
|
plugin.getDatabase().addSnapshot(user, upgraded);
|
||||||
|
plugin.getRedisManager().clearUserData(user);
|
||||||
|
}));
|
||||||
|
plugin.log(Level.INFO, "Legacy data upgrade complete!");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private <S> ArgumentElement<S, Migrator> migrator() {
|
private <S> ArgumentElement<S, Migrator> migrator() {
|
||||||
return new ArgumentElement<>("migrator", reader -> {
|
return new ArgumentElement<>("migrator", reader -> {
|
||||||
final String id = reader.readString();
|
final String id = reader.readString();
|
||||||
final Migrator migrator = plugin.getAvailableMigrators().stream()
|
final Migrator migrator = plugin.getAvailableMigrators().stream()
|
||||||
.filter(m -> m.getIdentifier().equalsIgnoreCase(id)).findFirst().orElse(null);
|
.filter(m -> m.getIdentifier().equalsIgnoreCase(id)).findFirst().orElse(null);
|
||||||
if (migrator == null) {
|
if (migrator == null) {
|
||||||
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
|
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
|
||||||
}
|
}
|
||||||
@@ -196,86 +256,4 @@ public class HuskSyncCommand extends PluginCommand {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum StatusLine {
|
|
||||||
PLUGIN_VERSION(plugin -> Component.text("v" + plugin.getPluginVersion().toStringWithoutMetadata())
|
|
||||||
.appendSpace().append(plugin.getPluginVersion().getMetadata().isBlank() ? Component.empty()
|
|
||||||
: Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))),
|
|
||||||
PLATFORM_TYPE(plugin -> Component.text(WordUtils.capitalizeFully(plugin.getPlatformType()))),
|
|
||||||
LANGUAGE(plugin -> Component.text(plugin.getSettings().getLanguage())),
|
|
||||||
MINECRAFT_VERSION(plugin -> Component.text(plugin.getMinecraftVersion().toString())),
|
|
||||||
JAVA_VERSION(plugin -> Component.text(System.getProperty("java.version"))),
|
|
||||||
JAVA_VENDOR(plugin -> Component.text(System.getProperty("java.vendor"))),
|
|
||||||
SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully(
|
|
||||||
plugin.getSettings().getSynchronization().getMode().toString()
|
|
||||||
))),
|
|
||||||
DELAY_LATENCY(plugin -> Component.text(
|
|
||||||
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms"
|
|
||||||
)),
|
|
||||||
SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
|
|
||||||
CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())),
|
|
||||||
DATABASE_TYPE(plugin ->
|
|
||||||
Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() +
|
|
||||||
(plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ?
|
|
||||||
(plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : ""))
|
|
||||||
),
|
|
||||||
IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())),
|
|
||||||
USING_REDIS_SENTINEL(plugin -> getBoolean(
|
|
||||||
!plugin.getSettings().getRedis().getSentinel().getMaster().isBlank()
|
|
||||||
)),
|
|
||||||
USING_REDIS_PASSWORD(plugin -> getBoolean(
|
|
||||||
!plugin.getSettings().getRedis().getCredentials().getPassword().isBlank()
|
|
||||||
)),
|
|
||||||
REDIS_USING_SSL(plugin -> getBoolean(
|
|
||||||
plugin.getSettings().getRedis().getCredentials().isUseSsl()
|
|
||||||
)),
|
|
||||||
IS_REDIS_LOCAL(plugin -> getLocalhostBoolean(
|
|
||||||
plugin.getSettings().getRedis().getCredentials().getHost()
|
|
||||||
)),
|
|
||||||
DATA_TYPES(plugin -> Component.join(
|
|
||||||
JoinConfiguration.commas(true),
|
|
||||||
plugin.getRegisteredDataTypes().stream().map(i -> Component.textOfChildren(Component.text(i.toString())
|
|
||||||
.appendSpace().append(Component.text(i.isEnabled() ? '✔' : '❌')))
|
|
||||||
.color(i.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED)
|
|
||||||
.hoverEvent(HoverEvent.showText(
|
|
||||||
Component.text(i.isEnabled() ? "Enabled" : "Disabled")
|
|
||||||
.append(Component.newline())
|
|
||||||
.append(Component.text("Dependencies: %s".formatted(i.getDependencies()
|
|
||||||
.isEmpty() ? "(None)" : i.getDependencies().stream()
|
|
||||||
.map(d -> "%s (%s)".formatted(
|
|
||||||
d.getKey().value(), d.isRequired() ? "Required" : "Optional"
|
|
||||||
)).collect(Collectors.joining(", ")))
|
|
||||||
).color(NamedTextColor.GRAY))
|
|
||||||
))).toList()
|
|
||||||
));
|
|
||||||
|
|
||||||
private final Function<HuskSync, Component> supplier;
|
|
||||||
|
|
||||||
StatusLine(@NotNull Function<HuskSync, Component> supplier) {
|
|
||||||
this.supplier = supplier;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private Component get(@NotNull HuskSync plugin) {
|
|
||||||
return Component
|
|
||||||
.text("•").appendSpace()
|
|
||||||
.append(Component.text(
|
|
||||||
WordUtils.capitalizeFully(name().replaceAll("_", " ")),
|
|
||||||
TextColor.color(0x848484)
|
|
||||||
))
|
|
||||||
.append(Component.text(':')).append(Component.space().color(NamedTextColor.WHITE))
|
|
||||||
.append(supplier.apply(plugin));
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private static Component getBoolean(boolean value) {
|
|
||||||
return Component.text(value ? "Yes" : "No", value ? NamedTextColor.GREEN : NamedTextColor.RED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private static Component getLocalhostBoolean(@NotNull String value) {
|
|
||||||
return getBoolean(value.equals("127.0.0.1") || value.equals("0.0.0.0")
|
|
||||||
|| value.equals("localhost") || value.equals("::1"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import de.themoep.minedown.adventure.MineDown;
|
|||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.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("inventory", List.of("invsee", "openinv"), plugin);
|
super("inventory", List.of("invsee", "openinv"), DataSnapshot.SaveCause.INVENTORY_COMMAND, plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -47,29 +46,29 @@ public class InventoryCommand extends ItemsCommand {
|
|||||||
if (optionalInventory.isEmpty()) {
|
if (optionalInventory.isEmpty()) {
|
||||||
viewer.sendMessage(new MineDown("what the FUCK is happening"));
|
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);
|
||||||
|
|
||||||
// Show GUI
|
// Show GUI
|
||||||
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) -> {
|
||||||
if (allowEdit && !inventory.equals(itemsOnClose)) {
|
if (allowEdit && !inventory.equals(itemsOnClose)) {
|
||||||
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
|
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,24 +78,23 @@ public class InventoryCommand extends ItemsCommand {
|
|||||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
|
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
|
||||||
if (latestData.isEmpty()) {
|
if (latestData.isEmpty()) {
|
||||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||||
.ifPresent(viewer::sendMessage);
|
.ifPresent(viewer::sendMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,12 @@ import java.util.UUID;
|
|||||||
|
|
||||||
public abstract class ItemsCommand extends PluginCommand {
|
public abstract class ItemsCommand extends PluginCommand {
|
||||||
|
|
||||||
protected ItemsCommand(@NotNull String name, @NotNull List<String> aliases, @NotNull HuskSync plugin) {
|
protected final DataSnapshot.SaveCause saveCause;
|
||||||
super(name, aliases, Permission.Default.IF_OP, plugin);
|
|
||||||
|
protected ItemsCommand(@NotNull String name, @NotNull List<String> aliases,
|
||||||
|
@NotNull DataSnapshot.SaveCause saveCause, @NotNull HuskSync plugin) {
|
||||||
|
super(name, aliases, Permission.Default.IF_OP, ExecutionScope.IN_GAME, plugin);
|
||||||
|
this.saveCause = saveCause;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -46,17 +50,17 @@ public abstract class ItemsCommand extends PluginCommand {
|
|||||||
final CommandUser executor = user(command, ctx);
|
final CommandUser executor = user(command, ctx);
|
||||||
if (!(executor instanceof OnlineUser online)) {
|
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);
|
this.showSnapshotItems(online, user, version);
|
||||||
}, user("username"), uuid("version"));
|
}, user("username"), versionUuid());
|
||||||
command.addSyntax((ctx) -> {
|
command.addSyntax((ctx) -> {
|
||||||
final User user = ctx.getArgument("username", User.class);
|
final User user = ctx.getArgument("username", User.class);
|
||||||
final CommandUser executor = user(command, ctx);
|
final CommandUser executor = user(command, ctx);
|
||||||
if (!(executor instanceof OnlineUser online)) {
|
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.showLatestItems(online, user);
|
this.showLatestItems(online, user);
|
||||||
@@ -65,45 +69,45 @@ public abstract class ItemsCommand extends PluginCommand {
|
|||||||
|
|
||||||
// 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")
|
||||||
.ifPresent(viewer::sendMessage);
|
.ifPresent(viewer::sendMessage);
|
||||||
return Optional.empty();
|
|
||||||
})
|
|
||||||
.flatMap(packed -> {
|
|
||||||
if (packed.isInvalid()) {
|
|
||||||
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
|
|
||||||
.ifPresent(viewer::sendMessage);
|
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
})
|
||||||
return Optional.of(packed.unpack(plugin));
|
.flatMap(packed -> {
|
||||||
})
|
if (packed.isInvalid()) {
|
||||||
.ifPresent(snapshot -> this.showItems(
|
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
|
||||||
viewer, snapshot, user, viewer.hasPermission(getPermission("edit"))
|
.ifPresent(viewer::sendMessage);
|
||||||
)));
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(packed.unpack(plugin));
|
||||||
|
})
|
||||||
|
.ifPresent(snapshot -> this.showItems(
|
||||||
|
viewer, snapshot, user, viewer.hasPermission(getPermission("edit"))
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// View a specific version of the user data
|
// View a specific version of the user data
|
||||||
private void showSnapshotItems(@NotNull OnlineUser viewer, @NotNull User user, @NotNull UUID version) {
|
private void showSnapshotItems(@NotNull OnlineUser viewer, @NotNull User user, @NotNull UUID version) {
|
||||||
plugin.getDatabase().getSnapshot(user, version)
|
plugin.getDatabase().getSnapshot(user, version)
|
||||||
.or(() -> {
|
.or(() -> {
|
||||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||||
.ifPresent(viewer::sendMessage);
|
.ifPresent(viewer::sendMessage);
|
||||||
return Optional.empty();
|
|
||||||
})
|
|
||||||
.flatMap(packed -> {
|
|
||||||
if (packed.isInvalid()) {
|
|
||||||
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
|
|
||||||
.ifPresent(viewer::sendMessage);
|
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
})
|
||||||
return Optional.of(packed.unpack(plugin));
|
.flatMap(packed -> {
|
||||||
})
|
if (packed.isInvalid()) {
|
||||||
.ifPresent(snapshot -> this.showItems(
|
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
|
||||||
viewer, snapshot, user, false
|
.ifPresent(viewer::sendMessage);
|
||||||
));
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(packed.unpack(plugin));
|
||||||
|
})
|
||||||
|
.ifPresent(snapshot -> this.showItems(
|
||||||
|
viewer, snapshot, user, false
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show a GUI menu with the correct item data from the snapshot
|
// Show a GUI menu with the correct item data from the snapshot
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import com.mojang.brigadier.context.CommandContext;
|
|||||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
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.uniform.BaseCommand;
|
import net.william278.uniform.BaseCommand;
|
||||||
import net.william278.uniform.Command;
|
import net.william278.uniform.Command;
|
||||||
@@ -31,6 +32,7 @@ import net.william278.uniform.element.ArgumentElement;
|
|||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
@@ -39,9 +41,9 @@ public abstract class PluginCommand extends Command {
|
|||||||
|
|
||||||
protected final HuskSync plugin;
|
protected final HuskSync plugin;
|
||||||
|
|
||||||
protected PluginCommand(@NotNull String name, @NotNull List<String> aliases,
|
protected PluginCommand(@NotNull String name, @NotNull List<String> aliases, @NotNull Permission.Default defPerm,
|
||||||
@NotNull Permission.Default permissionDefault, @NotNull HuskSync plugin) {
|
@NotNull ExecutionScope scope, @NotNull HuskSync plugin) {
|
||||||
super(name, aliases, getDescription(plugin, name), new Permission(createPermission(name), permissionDefault));
|
super(name, aliases, getDescription(plugin, name), new Permission(createPermission(name), defPerm), scope);
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +77,20 @@ public abstract class PluginCommand extends Command {
|
|||||||
return user.getUuid() == null ? plugin.getConsole() : plugin.getOnlineUser(user.getUuid()).orElseThrow();
|
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
|
@NotNull
|
||||||
protected <S> ArgumentElement<S, User> user(@NotNull String name) {
|
protected <S> ArgumentElement<S, User> user(@NotNull String name) {
|
||||||
return new ArgumentElement<>(name, reader -> {
|
return new ArgumentElement<>(name, reader -> {
|
||||||
@@ -83,20 +99,29 @@ public abstract class PluginCommand extends Command {
|
|||||||
() -> CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader)
|
() -> CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader)
|
||||||
);
|
);
|
||||||
}, (context, builder) -> {
|
}, (context, builder) -> {
|
||||||
plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getUsername()));
|
plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getName()));
|
||||||
return builder.buildFuture();
|
return builder.buildFuture();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
protected <S> ArgumentElement<S, UUID> uuid(@NotNull String name) {
|
protected <S> ArgumentElement<S, UUID> versionUuid() {
|
||||||
return new ArgumentElement<>(name, reader -> {
|
return new ArgumentElement<>("version", reader -> {
|
||||||
try {
|
try {
|
||||||
return UUID.fromString(reader.readString());
|
return UUID.fromString(reader.readString());
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
|
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
|
||||||
}
|
}
|
||||||
}, (context, builder) -> builder.buildFuture());
|
}, (context, builder) -> {
|
||||||
|
try {
|
||||||
|
plugin.getDatabase().getAllSnapshots(context.getArgument("username", User.class))
|
||||||
|
.stream().sorted(Comparator.comparing(d -> d.getTimestamp().toEpochSecond()))
|
||||||
|
.forEach(id -> builder.suggest(id.getId().toString()));
|
||||||
|
return builder.buildFuture();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return builder.buildFuture();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Type {
|
public enum Type {
|
||||||
|
|||||||
@@ -20,15 +20,19 @@
|
|||||||
package net.william278.husksync.command;
|
package net.william278.husksync.command;
|
||||||
|
|
||||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
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.BaseCommand;
|
||||||
import net.william278.uniform.CommandProvider;
|
import net.william278.uniform.CommandProvider;
|
||||||
import net.william278.uniform.Permission;
|
import net.william278.uniform.Permission;
|
||||||
@@ -44,7 +48,7 @@ import java.util.logging.Level;
|
|||||||
public class UserDataCommand extends PluginCommand {
|
public class UserDataCommand extends PluginCommand {
|
||||||
|
|
||||||
public UserDataCommand(@NotNull HuskSync plugin) {
|
public UserDataCommand(@NotNull HuskSync plugin) {
|
||||||
super("userdata", List.of("playerdata"), Permission.Default.IF_OP, plugin);
|
super("userdata", List.of("playerdata"), Permission.Default.IF_OP, ExecutionScope.ALL, plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -52,6 +56,7 @@ public class UserDataCommand extends PluginCommand {
|
|||||||
command.addSubCommand("view", needsOp("view"), view());
|
command.addSubCommand("view", needsOp("view"), view());
|
||||||
command.addSubCommand("list", needsOp("list"), list());
|
command.addSubCommand("list", needsOp("list"), list());
|
||||||
command.addSubCommand("delete", needsOp("delete"), delete());
|
command.addSubCommand("delete", needsOp("delete"), delete());
|
||||||
|
command.addSubCommand("save", needsOp("save"), save());
|
||||||
command.addSubCommand("restore", needsOp("restore"), restore());
|
command.addSubCommand("restore", needsOp("restore"), restore());
|
||||||
command.addSubCommand("pin", needsOp("pin"), pin());
|
command.addSubCommand("pin", needsOp("pin"), pin());
|
||||||
command.addSubCommand("dump", needsOp("dump"), dump());
|
command.addSubCommand("dump", needsOp("dump"), dump());
|
||||||
@@ -102,6 +107,13 @@ public class UserDataCommand extends PluginCommand {
|
|||||||
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)) {
|
||||||
@@ -113,7 +125,7 @@ public class UserDataCommand extends PluginCommand {
|
|||||||
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);
|
||||||
}
|
}
|
||||||
@@ -145,9 +157,9 @@ public class UserDataCommand extends PluginCommand {
|
|||||||
// 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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -169,11 +181,11 @@ public class UserDataCommand extends PluginCommand {
|
|||||||
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,
|
private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version,
|
||||||
@NotNull DumpType type) {
|
@NotNull DumpType type) {
|
||||||
final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, version);
|
final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, version);
|
||||||
@@ -182,14 +194,20 @@ public class UserDataCommand extends PluginCommand {
|
|||||||
.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();
|
||||||
(type == DumpType.WEB ? dumper.toWeb() : dumper.toFile()))
|
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getName())
|
||||||
.ifPresent(executor::sendMessage);
|
.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);
|
||||||
}
|
}
|
||||||
@@ -197,17 +215,11 @@ public class UserDataCommand extends PluginCommand {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private CommandProvider view() {
|
private CommandProvider view() {
|
||||||
return (sub) -> {
|
return (sub) -> sub.addSyntax((ctx) -> {
|
||||||
sub.addSyntax((ctx) -> {
|
final User user = ctx.getArgument("username", User.class);
|
||||||
final User user = ctx.getArgument("username", User.class);
|
final UUID version = ctx.getArgument("version", UUID.class);
|
||||||
viewLatestSnapshot(user(sub, ctx), user);
|
viewSnapshot(user(sub, ctx), user, version);
|
||||||
}, user("username"));
|
}, user("username"), versionUuid());
|
||||||
sub.addSyntax((ctx) -> {
|
|
||||||
final User user = ctx.getArgument("username", User.class);
|
|
||||||
final UUID version = ctx.getArgument("version", UUID.class);
|
|
||||||
viewSnapshot(user(sub, ctx), user, version);
|
|
||||||
}, user("username"), uuid("version"));
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -231,7 +243,15 @@ public class UserDataCommand extends PluginCommand {
|
|||||||
final User user = ctx.getArgument("username", User.class);
|
final User user = ctx.getArgument("username", User.class);
|
||||||
final UUID version = ctx.getArgument("version", UUID.class);
|
final UUID version = ctx.getArgument("version", UUID.class);
|
||||||
deleteSnapshot(user(sub, ctx), user, version);
|
deleteSnapshot(user(sub, ctx), user, version);
|
||||||
}, user("username"), uuid("version"));
|
}, user("username"), versionUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private CommandProvider save() {
|
||||||
|
return (sub) -> sub.addSyntax((ctx) -> {
|
||||||
|
final OnlineUser user = ctx.getArgument("username", OnlineUser.class);
|
||||||
|
createAndSaveSnapshot(user(sub, ctx), user);
|
||||||
|
}, onlineUser("username"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -240,7 +260,7 @@ public class UserDataCommand extends PluginCommand {
|
|||||||
final User user = ctx.getArgument("username", User.class);
|
final User user = ctx.getArgument("username", User.class);
|
||||||
final UUID version = ctx.getArgument("version", UUID.class);
|
final UUID version = ctx.getArgument("version", UUID.class);
|
||||||
restoreSnapshot(user(sub, ctx), user, version);
|
restoreSnapshot(user(sub, ctx), user, version);
|
||||||
}, user("username"), uuid("version"));
|
}, user("username"), versionUuid());
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -249,17 +269,32 @@ public class UserDataCommand extends PluginCommand {
|
|||||||
final User user = ctx.getArgument("username", User.class);
|
final User user = ctx.getArgument("username", User.class);
|
||||||
final UUID version = ctx.getArgument("version", UUID.class);
|
final UUID version = ctx.getArgument("version", UUID.class);
|
||||||
pinSnapshot(user(sub, ctx), user, version);
|
pinSnapshot(user(sub, ctx), user, version);
|
||||||
}, user("username"), uuid("version"));
|
}, user("username"), versionUuid());
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private CommandProvider dump() {
|
private CommandProvider dump() {
|
||||||
return (sub) -> sub.addSyntax((ctx) -> {
|
return (sub) -> {
|
||||||
final User user = ctx.getArgument("username", User.class);
|
sub.addSyntax((ctx) -> {
|
||||||
final UUID version = ctx.getArgument("version", UUID.class);
|
final User user = ctx.getArgument("username", User.class);
|
||||||
final DumpType type = ctx.getArgument("type", DumpType.class);
|
final CommandUser executor = user(sub, ctx);
|
||||||
dumpSnapshot(user(sub, ctx), user, version, type);
|
plugin.getRedisManager()
|
||||||
}, user("username"), uuid("version"), dumpType());
|
.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() {
|
private <S> ArgumentElement<S, DumpType> dumpType() {
|
||||||
|
|||||||
@@ -131,6 +131,15 @@ public interface ConfigProvider {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default void validateConfigFiles() {
|
||||||
|
// Validate server name is default
|
||||||
|
if (getServerName().equals("server")) {
|
||||||
|
getPlugin().log(Level.WARNING, "The server name set in ~/plugins/HuskSync/server.yml appears to" +
|
||||||
|
"be unchanged from the default (currently set to: \"server\"). Please check that this value has" +
|
||||||
|
"been updated to match the case-sensitive ID of this server in your proxy config file!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a plugin resource
|
* 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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public class Settings {
|
|||||||
private boolean checkForUpdates = true;
|
private boolean checkForUpdates = true;
|
||||||
|
|
||||||
@Comment("Specify a common ID for grouping servers running HuskSync. "
|
@Comment("Specify a common ID for grouping servers running HuskSync. "
|
||||||
+ "Don't modify this unless you know what you're doing!")
|
+ "Don't modify this unless you know what you're doing!")
|
||||||
private String clusterId = "";
|
private String clusterId = "";
|
||||||
|
|
||||||
@Comment("Enable development debug logging")
|
@Comment("Enable development debug logging")
|
||||||
@@ -141,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());
|
||||||
@@ -156,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
|
||||||
@@ -229,7 +232,7 @@ public class Settings {
|
|||||||
private boolean enabled = false;
|
private boolean enabled = false;
|
||||||
|
|
||||||
@Comment("What items to save in death snapshots? (DROPS or ITEMS_TO_KEEP). "
|
@Comment("What items to save in death snapshots? (DROPS or ITEMS_TO_KEEP). "
|
||||||
+ "Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server.")
|
+ "Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server.")
|
||||||
private DeathItemsMode itemsToSave = DeathItemsMode.DROPS;
|
private DeathItemsMode itemsToSave = DeathItemsMode.DROPS;
|
||||||
|
|
||||||
@Comment("Should a death snapshot still be created even if the items to save on the player's death are empty?")
|
@Comment("Should a death snapshot still be created even if the items to save on the player's death are empty?")
|
||||||
@@ -250,14 +253,14 @@ 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")
|
||||||
private boolean persistLockedMaps = true;
|
private boolean persistLockedMaps = true;
|
||||||
|
|
||||||
@Comment("If using the DELAY sync method, how long should this server listen for Redis key data updates before "
|
@Comment("If using the DELAY sync method, how long should this server listen for Redis key data updates before "
|
||||||
+ "pulling data from the database instead (i.e., if the user did not change servers).")
|
+ "pulling data from the database instead (i.e., if the user did not change servers).")
|
||||||
private int networkLatencyMilliseconds = 500;
|
private int networkLatencyMilliseconds = 500;
|
||||||
|
|
||||||
@Comment({"Which data types to synchronize.", "Docs: https://william278.net/docs/husksync/sync-features"})
|
@Comment({"Which data types to synchronize.", "Docs: https://william278.net/docs/husksync/sync-features"})
|
||||||
@@ -267,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(AccessLevel.NONE)
|
|
||||||
private List<String> ignoredAttributes = new ArrayList<>(List.of(""));
|
@Getter
|
||||||
|
@Configuration
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public static class AttributeSettings {
|
||||||
|
|
||||||
|
@Comment({"Which attribute types should be saved as part of attribute syncing. Supports wildcard matching.",
|
||||||
|
"(e.g. ['minecraft:generic.max_health', 'minecraft:generic.*'])"})
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private List<String> syncedAttributes = new ArrayList<>(List.of(
|
||||||
|
"minecraft:generic.max_health", "minecraft:max_health",
|
||||||
|
"minecraft:generic.max_absorption", "minecraft:max_absorption",
|
||||||
|
"minecraft:generic.luck", "minecraft:luck",
|
||||||
|
"minecraft:generic.scale", "minecraft:scale",
|
||||||
|
"minecraft:generic.step_height", "minecraft:step_height",
|
||||||
|
"minecraft:generic.gravity", "minecraft:gravity"
|
||||||
|
));
|
||||||
|
|
||||||
|
@Comment({"Which attribute modifiers should be saved. Supports wildcard matching.",
|
||||||
|
"(e.g. ['minecraft:effect.speed', 'minecraft:effect.*'])"})
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private List<String> ignoredModifiers = new ArrayList<>(List.of(
|
||||||
|
"minecraft:effect.*", "minecraft:creative_mode_*"
|
||||||
|
));
|
||||||
|
|
||||||
|
private boolean matchesWildcard(@NotNull String pat, @NotNull String value) {
|
||||||
|
if (!pat.contains(":")) {
|
||||||
|
pat = "minecraft:%s".formatted(pat);
|
||||||
|
}
|
||||||
|
if (!value.contains(":")) {
|
||||||
|
value = "minecraft:%s".formatted(value);
|
||||||
|
}
|
||||||
|
return pat.contains("*") ? value.matches(pat.replace("*", ".*")) : pat.equals(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isIgnoredAttribute(@NotNull String attribute) {
|
||||||
|
return syncedAttributes.stream().noneMatch(wildcard -> matchesWildcard(wildcard, attribute));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isIgnoredModifier(@NotNull String modifier) {
|
||||||
|
return ignoredModifiers.stream().anyMatch(wildcard -> matchesWildcard(wildcard, modifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Comment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts")
|
@Comment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts")
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
@@ -284,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 {
|
||||||
|
|||||||
@@ -22,9 +22,8 @@ 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.AccessLevel;
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.experimental.Accessors;
|
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;
|
||||||
@@ -132,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
|
||||||
@@ -155,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);
|
||||||
@@ -191,13 +190,13 @@ public interface Data {
|
|||||||
@NotNull
|
@NotNull
|
||||||
private static Map<String, Long> adaptDateMap(@NotNull Map<String, Date> dateMap) {
|
private static Map<String, Long> adaptDateMap(@NotNull Map<String, Date> dateMap) {
|
||||||
return dateMap.entrySet().stream()
|
return dateMap.entrySet().stream()
|
||||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getTime()));
|
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getTime()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private static Map<String, Date> adaptLongMap(@NotNull Map<String, Long> dateMap) {
|
private static Map<String, Date> adaptLongMap(@NotNull Map<String, Long> dateMap) {
|
||||||
return dateMap.entrySet().stream()
|
return dateMap.entrySet().stream()
|
||||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> new Date(e.getValue())));
|
.collect(Collectors.toMap(Map.Entry::getKey, e -> new Date(e.getValue())));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -250,9 +249,9 @@ public interface Data {
|
|||||||
void setWorld(@NotNull World world);
|
void setWorld(@NotNull World world);
|
||||||
|
|
||||||
record World(
|
record World(
|
||||||
@SerializedName("name") @NotNull String name,
|
@SerializedName("name") @NotNull String name,
|
||||||
@SerializedName("uuid") @NotNull UUID uuid,
|
@SerializedName("uuid") @NotNull UUID uuid,
|
||||||
@SerializedName("environment") @NotNull String environment
|
@SerializedName("environment") @NotNull String environment
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,9 +323,9 @@ public interface Data {
|
|||||||
List<Attribute> getAttributes();
|
List<Attribute> getAttributes();
|
||||||
|
|
||||||
record Attribute(
|
record Attribute(
|
||||||
@NotNull String name,
|
@NotNull String name,
|
||||||
double baseValue,
|
double baseValue,
|
||||||
@NotNull Set<Modifier> modifiers
|
@NotNull Set<Modifier> modifiers
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public double getValue() {
|
public double getValue() {
|
||||||
@@ -341,36 +340,60 @@ public interface Data {
|
|||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Accessors(fluent = true)
|
@Accessors(fluent = true)
|
||||||
@AllArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@NoArgsConstructor
|
|
||||||
final class Modifier {
|
final class Modifier {
|
||||||
|
final static String ANY_EQUIPMENT_SLOT_GROUP = "any";
|
||||||
|
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
@Nullable
|
@Nullable
|
||||||
@SerializedName("uuid")
|
@SerializedName("uuid")
|
||||||
private UUID uuid;
|
private UUID uuid = null;
|
||||||
|
|
||||||
|
// Since 1.21.1: Name, amount, operation, slotGroup
|
||||||
@SerializedName("name")
|
@SerializedName("name")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@SerializedName("amount")
|
@SerializedName("amount")
|
||||||
private double amount;
|
private double amount;
|
||||||
|
|
||||||
@SerializedName("operation")
|
@SerializedName("operation")
|
||||||
private int operationType;
|
private int operation;
|
||||||
|
|
||||||
@SerializedName("equipment_slot")
|
@SerializedName("equipment_slot")
|
||||||
|
@Deprecated(since = "3.7")
|
||||||
private int equipmentSlot;
|
private int equipmentSlot;
|
||||||
|
|
||||||
public Modifier(@NotNull String name, double amount, int operationType, int equipmentSlot) {
|
@SerializedName("equipment_slot_group")
|
||||||
|
private String slotGroup = ANY_EQUIPMENT_SLOT_GROUP;
|
||||||
|
|
||||||
|
public Modifier(@NotNull String name, double amount, int operation, @NotNull String slotGroup) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.amount = amount;
|
this.amount = amount;
|
||||||
this.operationType = operationType;
|
this.operation = operation;
|
||||||
|
this.slotGroup = slotGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated(since = "3.7")
|
||||||
|
public Modifier(@NotNull UUID uuid, @NotNull String name, double amount, int operation, int equipmentSlot) {
|
||||||
|
this.name = name;
|
||||||
|
this.amount = amount;
|
||||||
|
this.operation = operation;
|
||||||
this.equipmentSlot = equipmentSlot;
|
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);
|
||||||
@@ -378,6 +401,10 @@ public interface Data {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasUuid() {
|
||||||
|
return uuid != null;
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public UUID uuid() {
|
public UUID uuid() {
|
||||||
return uuid != null ? uuid : UUID.nameUUIDFromBytes(name.getBytes());
|
return uuid != null ? uuid : UUID.nameUUIDFromBytes(name.getBytes());
|
||||||
@@ -387,8 +414,8 @@ public interface Data {
|
|||||||
|
|
||||||
default Optional<Attribute> getAttribute(@NotNull Key key) {
|
default Optional<Attribute> getAttribute(@NotNull Key key) {
|
||||||
return getAttributes().stream()
|
return getAttributes().stream()
|
||||||
.filter(attribute -> attribute.name().equals(key.asString()))
|
.filter(attribute -> attribute.name().equals(key.asString()))
|
||||||
.findFirst();
|
.findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
default void removeAttribute(@NotNull Key key) {
|
default void removeAttribute(@NotNull Key key) {
|
||||||
@@ -397,8 +424,8 @@ public interface Data {
|
|||||||
|
|
||||||
default double getMaxHealth() {
|
default double getMaxHealth() {
|
||||||
return getAttribute(MAX_HEALTH_KEY)
|
return getAttribute(MAX_HEALTH_KEY)
|
||||||
.map(Attribute::getValue)
|
.map(Attribute::getValue)
|
||||||
.orElse(20.0);
|
.orElse(20.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
default void setMaxHealth(double maxHealth) {
|
default void setMaxHealth(double maxHealth) {
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ public class DataException extends IllegalStateException {
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public enum Reason {
|
public enum Reason {
|
||||||
INVALID_MINECRAFT_VERSION((plugin, snapshot) -> String.format("The Minecraft version of the snapshot (%s) is " +
|
INVALID_MINECRAFT_VERSION((plugin, snapshot) -> String.format("The Minecraft version of the snapshot (%s) is " +
|
||||||
"newer than the server's version (%s). Ensure each server is on the same version of Minecraft.",
|
"newer than the server's version (%s). Ensure each server is on the same version of Minecraft.",
|
||||||
snapshot.getMinecraftVersion(), plugin.getMinecraftVersion())),
|
snapshot.getMinecraftVersion(), plugin.getMinecraftVersion())),
|
||||||
INVALID_FORMAT_VERSION((plugin, snapshot) -> String.format("The format version of the snapshot (%s) is newer " +
|
INVALID_FORMAT_VERSION((plugin, snapshot) -> String.format("The format version of the snapshot (%s) is newer " +
|
||||||
"than the server's version (%s). Ensure each server is running the same version of HuskSync.",
|
"than the server's version (%s). Ensure each server is running the same version of HuskSync.",
|
||||||
snapshot.getFormatVersion(), DataSnapshot.CURRENT_FORMAT_VERSION)),
|
snapshot.getFormatVersion(), DataSnapshot.CURRENT_FORMAT_VERSION)),
|
||||||
INVALID_PLATFORM_TYPE((plugin, snapshot) -> String.format("The platform type of the snapshot (%s) does " +
|
INVALID_PLATFORM_TYPE((plugin, snapshot) -> String.format("The platform type of the snapshot (%s) does " +
|
||||||
"not match the server's platform type (%s). Ensure each server has the same platform type.",
|
"not match the server's platform type (%s). Ensure each server has the same platform type.",
|
||||||
snapshot.getPlatformType(), plugin.getPlatformType())),
|
snapshot.getPlatformType(), plugin.getPlatformType())),
|
||||||
NO_LEGACY_CONVERTER((plugin, snapshot) -> String.format("No legacy converter to convert format version: %s",
|
NO_LEGACY_CONVERTER((plugin, snapshot) -> String.format("No legacy converter to convert format version: %s",
|
||||||
snapshot.getFormatVersion()));
|
snapshot.getFormatVersion()));
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ public class DataSnapshot {
|
|||||||
.map(entry -> Map.entry(plugin.getIdentifier(entry.getKey()).orElseThrow(), entry.getValue()))
|
.map(entry -> Map.entry(plugin.getIdentifier(entry.getKey()).orElseThrow(), entry.getValue()))
|
||||||
.collect(Collectors.toMap(
|
.collect(Collectors.toMap(
|
||||||
Map.Entry::getKey,
|
Map.Entry::getKey,
|
||||||
entry -> plugin.deserializeData(entry.getKey(), entry.getValue()),
|
entry -> plugin.deserializeData(entry.getKey(), entry.getValue(), getMinecraftVersion()),
|
||||||
(a, b) -> b, () -> Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR)
|
(a, b) -> b, () -> Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ 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.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@@ -50,7 +51,8 @@ public class Identifier {
|
|||||||
Dependency.optional("game_mode")
|
Dependency.optional("game_mode")
|
||||||
);
|
);
|
||||||
public static final Identifier ATTRIBUTES = huskSync("attributes", true,
|
public static final Identifier ATTRIBUTES = huskSync("attributes", true,
|
||||||
Dependency.required("potion_effects")
|
Dependency.optional("inventory"),
|
||||||
|
Dependency.optional("potion_effects")
|
||||||
);
|
);
|
||||||
public static final Identifier HEALTH = huskSync("health", true,
|
public static final Identifier HEALTH = huskSync("health", true,
|
||||||
Dependency.optional("attributes")
|
Dependency.optional("attributes")
|
||||||
@@ -228,11 +230,13 @@ public class Identifier {
|
|||||||
* @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
|
@Override
|
||||||
public boolean equals(Object obj) {
|
public boolean equals(@Nullable Object obj) {
|
||||||
if (obj instanceof Identifier other) {
|
return obj instanceof Identifier other ? toString().equals(other.toString()) : super.equals(obj);
|
||||||
return key.equals(other.key);
|
}
|
||||||
}
|
|
||||||
return false;
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return key.toString().hashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the config entry for the identifier
|
// Get the config entry for the identifier
|
||||||
@@ -314,6 +318,11 @@ public class Identifier {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return key.toString().hashCode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
package net.william278.husksync.data;
|
package net.william278.husksync.data;
|
||||||
|
|
||||||
|
import net.william278.desertwell.util.Version;
|
||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -119,19 +120,36 @@ public interface SerializerRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deserialize data for the given {@link Identifier}
|
* Deserialize data of a given {@link Version Minecraft version} for the given {@link Identifier data identifier}
|
||||||
|
*
|
||||||
|
* @param identifier the {@link Identifier} to deserialize data for
|
||||||
|
* @param data the data to deserialize
|
||||||
|
* @param 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 identifier the {@link Identifier} to deserialize data for
|
||||||
* @param data the data to deserialize
|
* @param data the data to deserialize
|
||||||
* @return the deserialized data
|
* @return the deserialized data
|
||||||
* @throws IllegalStateException if no serializer is found for the given {@link Identifier}
|
|
||||||
* @since 3.5.4
|
* @since 3.5.4
|
||||||
|
* @deprecated Use {@link #deserializeData(Identifier, String, Version)} instead
|
||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
default Data deserializeData(@NotNull Identifier identifier, @NotNull String data) throws IllegalStateException {
|
@Deprecated(since = "3.6.5")
|
||||||
return getSerializer(identifier).map(serializer -> serializer.deserialize(data)).orElseThrow(
|
default Data deserializeData(@NotNull Identifier identifier, @NotNull String data) {
|
||||||
() -> new IllegalStateException("No serializer found for %s".formatted(identifier))
|
return deserializeData(identifier, data, getPlugin().getMinecraftVersion());
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,18 +101,13 @@ 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());
|
Document filter = new Document("uuid", existingUser.getUuid());
|
||||||
@@ -113,7 +116,7 @@ public class MongoDbDatabase extends Database {
|
|||||||
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()).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) {
|
||||||
@@ -157,12 +154,6 @@ public class MongoDbDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a user by their username (<i>case-insensitive</i>)
|
|
||||||
*
|
|
||||||
* @param username Username of the {@link User} to get (<i>case-insensitive</i>)
|
|
||||||
* @return An optional with the {@link User} present if they exist
|
|
||||||
*/
|
|
||||||
@Blocking
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public Optional<User> getUserByName(@NotNull String username) {
|
public Optional<User> getUserByName(@NotNull String username) {
|
||||||
@@ -180,12 +171,24 @@ 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) {
|
||||||
@@ -208,12 +211,6 @@ public class MongoDbDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all {@link DataSnapshot} entries for a user from the database.
|
|
||||||
*
|
|
||||||
* @param user The user to get data for
|
|
||||||
* @return The list of a user's {@link DataSnapshot} entries
|
|
||||||
*/
|
|
||||||
@Blocking
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -237,13 +234,6 @@ 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) {
|
||||||
@@ -265,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) {
|
||||||
@@ -296,12 +280,6 @@ public class MongoDbDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
|
|
||||||
*
|
|
||||||
* @param user The user to get data for
|
|
||||||
* @param versionUuid The UUID of the {@link DataSnapshot} entry to delete
|
|
||||||
*/
|
|
||||||
@Blocking
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||||
@@ -319,14 +297,6 @@ 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) {
|
||||||
@@ -351,12 +321,6 @@ public class MongoDbDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* <b>Internal</b> - Create user data in the database
|
|
||||||
*
|
|
||||||
* @param user The user to add data for
|
|
||||||
* @param data The {@link DataSnapshot} to set.
|
|
||||||
*/
|
|
||||||
@Blocking
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||||
@@ -373,12 +337,6 @@ public class MongoDbDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a saved {@link DataSnapshot} by given version UUID
|
|
||||||
*
|
|
||||||
* @param user The user whose data snapshot
|
|
||||||
* @param data The {@link DataSnapshot} to update
|
|
||||||
*/
|
|
||||||
@Blocking
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||||
@@ -395,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() {
|
||||||
@@ -409,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,8 +66,9 @@ 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
|
||||||
*/
|
*/
|
||||||
public void insertDocument(@NotNull String collectionName, @NotNull Document document) {
|
public void insertDocument(@NotNull String collectionName, @NotNull Document document) {
|
||||||
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
||||||
@@ -72,9 +77,10 @@ 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
|
||||||
*/
|
*/
|
||||||
public void updateDocument(@NotNull String collectionName, @NotNull Document document, @NotNull Bson updates) {
|
public void updateDocument(@NotNull String collectionName, @NotNull Document document, @NotNull Bson updates) {
|
||||||
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
||||||
@@ -83,8 +89,9 @@ 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
|
||||||
*/
|
*/
|
||||||
public void deleteDocument(@NotNull String collectionName, @NotNull Document document) {
|
public void deleteDocument(@NotNull String collectionName, @NotNull Document document) {
|
||||||
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
||||||
|
|||||||
@@ -35,9 +35,10 @@ 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) {
|
||||||
try {
|
try {
|
||||||
final MongoClientSettings settings = MongoClientSettings.builder()
|
final MongoClientSettings settings = MongoClientSettings.builder()
|
||||||
.applyConnectionString(uri)
|
.applyConnectionString(uri)
|
||||||
@@ -48,7 +49,7 @@ public class MongoConnectionHandler {
|
|||||||
this.database = mongoClient.getDatabase(databaseName);
|
this.database = mongoClient.getDatabase(databaseName);
|
||||||
} 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
plugin.debug(String.format("[%s] %s %s key %s Redis (%s)", user.getName(),
|
||||||
checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT));
|
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,11 +57,11 @@ public class LockstepDataSyncer extends DataSyncer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void saveUserData(@NotNull OnlineUser onlineUser) {
|
public void syncSaveUserData(@NotNull OnlineUser onlineUser) {
|
||||||
plugin.runAsync(() -> saveData(
|
plugin.runAsync(() -> 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);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import net.kyori.adventure.audience.Audience;
|
|||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public interface CommandUser {
|
public interface CommandUser {
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
Audience getAudience();
|
Audience getAudience();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -106,14 +106,14 @@ public class DataSnapshotOverview {
|
|||||||
.ifPresent(user::sendMessage);
|
.ifPresent(user::sendMessage);
|
||||||
|
|
||||||
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,17 +19,15 @@
|
|||||||
|
|
||||||
package net.william278.husksync.util;
|
package net.william278.husksync.util;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import lombok.AccessLevel;
|
||||||
import com.google.gson.JsonParser;
|
import lombok.AllArgsConstructor;
|
||||||
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.user.User;
|
import net.william278.husksync.user.User;
|
||||||
|
import net.william278.toilet.web.Flusher;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -38,94 +36,42 @@ import java.util.Locale;
|
|||||||
import java.util.StringJoiner;
|
import java.util.StringJoiner;
|
||||||
import java.util.logging.Level;
|
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
|
* Utility class for dumping {@link DataSnapshot}s to a file or as a paste on the web
|
||||||
*/
|
*/
|
||||||
public class DataDumper {
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class UserDataDumper implements Flusher {
|
||||||
|
|
||||||
private static final String LOGS_SITE_ENDPOINT = "https://api.mclo.gs/1/log";
|
private static final String PASTE_VIEWER_URL = "https://pastes.dev";
|
||||||
|
|
||||||
private final HuskSync plugin;
|
|
||||||
private final DataSnapshot.Packed snapshot;
|
private final DataSnapshot.Packed snapshot;
|
||||||
private final User user;
|
private final User user;
|
||||||
|
private final HuskSync plugin;
|
||||||
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}
|
* Create a {@link UserDataDumper} of the given {@link DataSnapshot}
|
||||||
*
|
*
|
||||||
* @param dataSnapshot The {@link DataSnapshot} to dump
|
* @param snapshot The {@link DataSnapshot} to dump
|
||||||
* @param user The {@link User} whose data is being dumped
|
* @param user The {@link User} whose data is being dumped
|
||||||
* @param plugin The implementing {@link HuskSync} plugin
|
* @param plugin The implementing {@link HuskSync} plugin
|
||||||
* @return A {@link DataDumper} for the given {@link DataSnapshot}
|
* @return A {@link UserDataDumper} for the given {@link DataSnapshot}
|
||||||
*/
|
*/
|
||||||
public static DataDumper create(@NotNull DataSnapshot.Packed dataSnapshot,
|
|
||||||
@NotNull User user, @NotNull HuskSync plugin) {
|
|
||||||
return new DataDumper(dataSnapshot, user, plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dumps the data snapshot to a string
|
|
||||||
*
|
|
||||||
* @return the data snapshot as a string
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public String toString() {
|
public static UserDataDumper create(@NotNull DataSnapshot.Packed snapshot, @NotNull User user,
|
||||||
return snapshot.asJson(plugin);
|
@NotNull HuskSync plugin) {
|
||||||
|
return new UserDataDumper(snapshot, user, plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public String toWeb() {
|
public String toWeb() {
|
||||||
try {
|
try {
|
||||||
final URL url = new URL(LOGS_SITE_ENDPOINT);
|
return "%s/%s".formatted(PASTE_VIEWER_URL, uploadDump(toString(), BYTEBIN_URL, "husksync"));
|
||||||
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) {
|
} catch (Throwable e) {
|
||||||
plugin.log(Level.SEVERE, "Failed to upload data to logs site", e);
|
plugin.log(Level.SEVERE, "Failed to upload data.", e);
|
||||||
}
|
}
|
||||||
return "(Failed to upload to logs site)";
|
return "(Failed to upload. Try dumping to a file instead.)";
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private String getWebContentField() {
|
|
||||||
return "content=" + URLEncoder.encode(toString(), StandardCharsets.UTF_8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -140,7 +86,7 @@ public class DataDumper {
|
|||||||
writer.write(toString()); // Write the data from #getString to the file using a writer
|
writer.write(toString()); // Write the data from #getString to the file using a writer
|
||||||
return filePath.toString();
|
return filePath.toString();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new IOException("Failed to write data to file", e);
|
throw new IOException("Failed to write dump to file", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,11 +124,22 @@ public class DataDumper {
|
|||||||
@NotNull
|
@NotNull
|
||||||
private String getFileName() {
|
private String getFileName() {
|
||||||
return new StringJoiner("_")
|
return new StringJoiner("_")
|
||||||
.add(user.getUsername())
|
.add(user.getName())
|
||||||
.add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")))
|
.add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")))
|
||||||
.add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH))
|
.add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH))
|
||||||
.add(snapshot.getShortId())
|
.add(snapshot.getShortId())
|
||||||
+ ".json";
|
+ ".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%)'
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ locales:
|
|||||||
data_list_title: '[%1% のユーザーデータスナップショット:](#00fb9a) [(%4%件中](#00fb9a bold) [%2%-%3%件](#00fb9a)[)](#00fb9a)\n'
|
data_list_title: '[%1% のユーザーデータスナップショット:](#00fb9a) [(%4%件中](#00fb9a bold) [%2%-%3%件](#00fb9a)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7%2% のユーザーデータスナップショット&8⚡ %4% run_command=/husksync:userdata view %2% %3%) [%5%](#d8ff2b show_text=&7ピン留め:\n&8ピン留めされたスナップショットは自動的にローテーションしません。 run_command=/husksync:userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7バージョンタイムスタンプ:&7\n&8データの保存時期\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7保存理由:\n&8データが保存された理由 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7スナップショットサイズ:&7\n&8スナップショットの推定ファイルサイズ (単位:KiB) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7%2% のユーザーデータスナップショット&8⚡ %4% run_command=/husksync:userdata view %2% %3%) [%5%](#d8ff2b show_text=&7ピン留め:\n&8ピン留めされたスナップショットは自動的にローテーションしません。 run_command=/husksync:userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7バージョンタイムスタンプ:&7\n&8データの保存時期\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7保存理由:\n&8データが保存された理由 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7スナップショットサイズ:&7\n&8スナップショットの推定ファイルサイズ (単位:KiB) run_command=/userdata view %2% %3%)'
|
||||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
|
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||||
data_deleted: '[❌](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [の消去に成功しました。](#00fb9a)'
|
data_deleted: '[❌](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [の消去に成功しました。](#00fb9a)'
|
||||||
data_restored: '[⏪](#00fb9a) [スナップショット](#00fb9a) [%3%](#00fb9a show_text=&7Version UUID:\n&8%4%) [から](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [の現在のユーザーデータの復元に成功しました。](#00fb9a)'
|
data_restored: '[⏪](#00fb9a) [スナップショット](#00fb9a) [%3%](#00fb9a show_text=&7Version UUID:\n&8%4%) [から](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [の現在のユーザーデータの復元に成功しました。](#00fb9a)'
|
||||||
data_pinned: '[※](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [のピン留めに成功しました。](#00fb9a)'
|
data_pinned: '[※](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [のピン留めに成功しました。](#00fb9a)'
|
||||||
data_unpinned: '[※](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [のピン外しに成功しました。](#00fb9a)'
|
data_unpinned: '[※](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [のピン外しに成功しました。](#00fb9a)'
|
||||||
data_dumped: '[☂ %2% のユーザーデータスナップショット %1% のダンプに成功:](#00fb9a) &7%3%'
|
data_dumped: '[☂ %2% のユーザーデータスナップショット %1% のダンプに成功:](#00fb9a)'
|
||||||
list_footer: '\n%1%[ページ](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
list_footer: '\n%1%[ページ](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||||
list_previous_page_button: '[◀](white show_text=&7前のページへ run_command=%2% %1%) '
|
list_previous_page_button: '[◀](white show_text=&7前のページへ run_command=%2% %1%) '
|
||||||
list_next_page_button: ' [▶](white show_text=&7次のページへ run_command=%2% %1%)'
|
list_next_page_button: ' [▶](white show_text=&7次のページへ 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) [| HuskSyncの最新バージョンが更新されています: v%1% (実行中: v%2%).](#ff7e5e)'
|
update_available: '[HuskSync](#ff7e5e bold) [| HuskSyncの最新バージョンが更新されています: v%1% (実行中: v%2%).](#ff7e5e)'
|
||||||
reload_complete: '[HuskSync](#00fb9a bold) [| 設定ファイルとメッセージファイルを再読み込みしました。](#00fb9a)\n[⚠ すべてのサーバーで設定ファイルが最新であることを確認してください!](#00fb9a)\n[設定の変更を有効にするには再起動が必要です。](#00fb9a italic)'
|
reload_complete: '[HuskSync](#00fb9a bold) [| 設定ファイルとメッセージファイルを再読み込みしました。](#00fb9a)\n[⚠ すべてのサーバーで設定ファイルが最新であることを確認してください!](#00fb9a)\n[設定の変更を有効にするには再起動が必要です。](#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) [構文が正しくありません。使用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&クリックでサジェスト suggest_command=%1%)'
|
error_invalid_syntax: '[Error:](#ff3300) [構文が正しくありません。使用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&クリックでサジェスト suggest_command=%1%)'
|
||||||
error_invalid_player: '[Error:](#ff3300) [そのプレイヤーは見つかりませんでした](#ff7e5e)'
|
error_invalid_player: '[Error:](#ff3300) [そのプレイヤーは見つかりませんでした](#ff7e5e)'
|
||||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ locales:
|
|||||||
data_list_title: '[%1%님의 유저 데이터 스냅샷 목록:](#00fb9a) [(%2%-%3% 중](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[%1%님의 유저 데이터 스냅샷 목록:](#00fb9a) [(%2%-%3% 중](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7%2%&7님의 유저 데이터 스냅샷&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7고정됨:\n&8고정된 스냅샷은 자동적으로 갱신되지 않습니다. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7저장 시각:&7\n&8데이터가 저장된 시각입니다.\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7저장 사유:\n&8데이터가 저장된 사유입니다. run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7스냅샷 크기:&7\n&8스냅샷 파일의 대략적인 크기입니다. (단위 KiB) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7%2%&7님의 유저 데이터 스냅샷&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7고정됨:\n&8고정된 스냅샷은 자동적으로 갱신되지 않습니다. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7저장 시각:&7\n&8데이터가 저장된 시각입니다.\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7저장 사유:\n&8데이터가 저장된 사유입니다. run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7스냅샷 크기:&7\n&8스냅샷 파일의 대략적인 크기입니다. (단위 KiB) run_command=/userdata view %2% %3%)'
|
||||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
|
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||||
data_deleted: '[❌ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%) [님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 삭제하였습니다.](#00fb9a)'
|
data_deleted: '[❌ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%) [님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 삭제하였습니다.](#00fb9a)'
|
||||||
data_restored: '[⏪ 성공적으로 복구되었습니다.](#00fb9a) [%1%](#00fb9a show_text=&7플레이어 UUID:\n&8%2%)[님의 현재 유저 데이터 스냅샷이](#00fb9a) [%3%](#00fb9a show_text=&7버전 UUID:\n&8%4%)[으로 변경되었습니다.](#00fb9a)'
|
data_restored: '[⏪ 성공적으로 복구되었습니다.](#00fb9a) [%1%](#00fb9a show_text=&7플레이어 UUID:\n&8%2%)[님의 현재 유저 데이터 스냅샷이](#00fb9a) [%3%](#00fb9a show_text=&7버전 UUID:\n&8%4%)[으로 변경되었습니다.](#00fb9a)'
|
||||||
data_pinned: '[※ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%)[님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 고정하였습니다.](#00fb9a)'
|
data_pinned: '[※ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%)[님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 고정하였습니다.](#00fb9a)'
|
||||||
data_unpinned: '[※ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%) [님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 고정 해제하였습니다.](#00fb9a)'
|
data_unpinned: '[※ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%) [님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 고정 해제하였습니다.](#00fb9a)'
|
||||||
data_dumped: '[☂ 성공적으로 %2%님의 유저 데이터 스냅샷 %1%를 다음으로 덤프하였습니다:](#00fb9a) &7%3%'
|
data_dumped: '[☂ 성공적으로 %2%님의 유저 데이터 스냅샷 %1%를 다음으로 덤프하였습니다:](#00fb9a)'
|
||||||
list_footer: '\n%1%[페이지](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
list_footer: '\n%1%[페이지](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||||
list_previous_page_button: '[◀](white show_text=&7이전 페이지 보기 run_command=%2% %1%) '
|
list_previous_page_button: '[◀](white show_text=&7이전 페이지 보기 run_command=%2% %1%) '
|
||||||
list_next_page_button: ' [▶](white show_text=&7다음 페이지 보기 run_command=%2% %1%)'
|
list_next_page_button: ' [▶](white show_text=&7다음 페이지 보기 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) [| 새로운 버전의 HuskSync가 존재합니다: v%1% (현재 버전: v%2%).](#ff7e5e)'
|
update_available: '[HuskSync](#ff7e5e bold) [| 새로운 버전의 HuskSync가 존재합니다: v%1% (현재 버전: v%2%).](#ff7e5e)'
|
||||||
reload_complete: '[HuskSync](#00fb9a bold) [| 콘피그와 메시지 파일을 다시 불러왔습니다.](#00fb9a)\n[⚠ 모든 서버의 컨피그 파일을 변경하였는지 확인하세요!](#00fb9a)\n[몇몇 설정은 재시작 후에 적용됩니다.](#00fb9a italic)'
|
reload_complete: '[HuskSync](#00fb9a bold) [| 콘피그와 메시지 파일을 다시 불러왔습니다.](#00fb9a)\n[⚠ 모든 서버의 컨피그 파일을 변경하였는지 확인하세요!](#00fb9a)\n[몇몇 설정은 재시작 후에 적용됩니다.](#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&클릭하여 입력할 수 있습니다. suggest_command=%1%)'
|
error_invalid_syntax: '[오류:](#ff3300) [잘못된 사용법. 사용법:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&클릭하여 입력할 수 있습니다. 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: '[%1%''s momentopnamen van gebruikersgegevens:](#00fb9a) [(%2%-%3% van](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[%1%''s momentopnamen van gebruikersgegevens:](#00fb9a) [(%2%-%3% van](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7Gebruikersgegevens momentopname voor %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Vastgezet:\n&8Vastgezette momentopnamen worden niet automatisch gerouleerd. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versie tijdmarkering:&7\n&8Wanneer de data was opgeslagen\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Reden opslaan:\n&8Waarom de data is opgeslagen run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Grootte van momentopname:&7\n&8Geschatte bestandsgrootte van de momentopname (in KiB) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7Gebruikersgegevens momentopname voor %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Vastgezet:\n&8Vastgezette momentopnamen worden niet automatisch gerouleerd. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versie tijdmarkering:&7\n&8Wanneer de data was opgeslagen\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Reden opslaan:\n&8Waarom de data is opgeslagen run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Grootte van momentopname:&7\n&8Geschatte bestandsgrootte van de momentopname (in KiB) run_command=/userdata view %2% %3%)'
|
||||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
|
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||||
data_deleted: '[❌ Momentopname van gebruikersgegevens is verwijderd](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
|
data_deleted: '[❌ Momentopname van gebruikersgegevens is verwijderd](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
|
||||||
data_restored: '[⏪ Succesvol hersteld](#00fb9a) [%1%](#00fb9a show_text=&7Speler UUID:\n&8%2%)[''s huidige gebruikersgegevens uit momentopname](#00fb9a) [%3%.](#00fb9a show_text=&7Versie UUID:\n&8%4%)'
|
data_restored: '[⏪ Succesvol hersteld](#00fb9a) [%1%](#00fb9a show_text=&7Speler UUID:\n&8%2%)[''s huidige gebruikersgegevens uit momentopname](#00fb9a) [%3%.](#00fb9a show_text=&7Versie UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Momentopname van gebruikersgegevens is vastgezet](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
|
data_pinned: '[※ Momentopname van gebruikersgegevens is vastgezet](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
|
||||||
data_unpinned: '[※ Momentopname van gebruikersgegevens is losgemaakt](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
|
data_unpinned: '[※ Momentopname van gebruikersgegevens is losgemaakt](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
|
||||||
data_dumped: '[☂ De momentopname van gebruikersgegevens %1% voor %2% is met succes gedumpt naar:](#00fb9a) &7%3%'
|
data_dumped: '[☂ De momentopname van gebruikersgegevens %1% voor %2% is met succes gedumpt naar:](#00fb9a)'
|
||||||
list_footer: '\n%1%[Pagina](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
list_footer: '\n%1%[Pagina](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||||
list_previous_page_button: '[◀](white show_text=&7Bekijk vorige pagina run_command=%2% %1%) '
|
list_previous_page_button: '[◀](white show_text=&7Bekijk vorige pagina run_command=%2% %1%) '
|
||||||
list_next_page_button: ' [▶](white show_text=&7Bekijk volgende pagina run_command=%2% %1%)'
|
list_next_page_button: ' [▶](white show_text=&7Bekijk volgende pagina run_command=%2% %1%)'
|
||||||
@@ -40,6 +41,8 @@ locales:
|
|||||||
save_cause_world_save: 'world save'
|
save_cause_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) [| Er is een nieuwe versie van HuskSync beschikbaar: v%1% (huidige versie: v%2%).](#ff7e5e)'
|
update_available: '[HuskSync](#ff7e5e bold) [| Er is een nieuwe versie van HuskSync beschikbaar: v%1% (huidige versie: v%2%).](#ff7e5e)'
|
||||||
reload_complete: '[HuskSync](#00fb9a bold) [| Configuratie- en berichtbestanden opnieuw geladen.](#00fb9a)\n[⚠ Controleer of de configuratiebestanden up-to-date zijn op alle servers!](#00fb9a)\n[Een herstart is nodig voor de configuratiewijzigingen van kracht te laten worden.](#00fb9a italic)'
|
reload_complete: '[HuskSync](#00fb9a bold) [| Configuratie- en berichtbestanden opnieuw geladen.](#00fb9a)\n[⚠ Controleer of de configuratiebestanden up-to-date zijn op alle servers!](#00fb9a)\n[Een herstart is nodig voor de configuratiewijzigingen van kracht te laten worden.](#00fb9a italic)'
|
||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||||
|
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||||
|
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||||
|
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Error:](#ff3300) [Onjuiste syntaxis. Gebruik:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
error_invalid_syntax: '[Error:](#ff3300) [Onjuiste syntaxis. Gebruik:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||||
error_invalid_player: '[Error:](#ff3300) [Kan geen speler met die naam vinden.](#ff7e5e)'
|
error_invalid_player: '[Error:](#ff3300) [Kan geen speler met die naam vinden.](#ff7e5e)'
|
||||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
error_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: '[❌ Snapshot de dados do usuário deletada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_deleted: '[❌ Snapshot de dados do usuário deletada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
data_restored: '[⏪ Restaurada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
data_restored: '[⏪ Restaurada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Snapshot de dados do usuário marcada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_pinned: '[※ Snapshot de dados do usuário marcada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
data_unpinned: '[※ Snapshot de dados do usuário desmarcada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_unpinned: '[※ Snapshot de dados do usuário desmarcada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
data_dumped: '[☂ Successfully dumped user data snapshot %1% for %2%. Click to view:](#00fb9a)'
|
||||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
list_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) [| Arquivos de configuração e mensagens recarregados.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
reload_complete: '[HuskSync](#00fb9a bold) [| Arquivos de configuração e mensagens recarregados.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||||
|
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||||
|
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||||
|
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Error:](#ff3300) [Sintaxe incorreta. Utilize:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
error_invalid_syntax: '[Error:](#ff3300) [Sintaxe incorreta. Utilize:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||||
error_invalid_player: '[Error:](#ff3300) [Não foi possível encontrar um jogador com esse nome.](#ff7e5e)'
|
error_invalid_player: '[Error:](#ff3300) [Não foi possível encontrar um jogador com esse nome.](#ff7e5e)'
|
||||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ locales:
|
|||||||
data_list_title: '[Снимки данных %1%:](#00fb9a) [(%2%-%3% из](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[Снимки данных %1%:](#00fb9a) [(%2%-%3% из](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7Снимок данных %4% пользователя %2%&8⚡ run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Закреплен:\n&8Закрепленные снимки данных не удаляются автоматически run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Время:&7\n&8Когда данные были сохранены\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Причина сохранения:\n&8Что привело к сохранению данных run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Размер:&7\n&8Предполагаемый размер снимка (в килобайтах) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7Снимок данных %4% пользователя %2%&8⚡ run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Закреплен:\n&8Закрепленные снимки данных не удаляются автоматически run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Время:&7\n&8Когда данные были сохранены\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Причина сохранения:\n&8Что привело к сохранению данных run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Размер:&7\n&8Предполагаемый размер снимка (в килобайтах) run_command=/userdata view %2% %3%)'
|
||||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
|
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||||
data_deleted: '[❌ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID игрока:\n&8%4%) [удален.](#00fb9a)'
|
data_deleted: '[❌ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID игрока:\n&8%4%) [удален.](#00fb9a)'
|
||||||
data_restored: '[⏪ Данные пользователя](#00fb9a) [%1%](#00fb9a show_text=&7UUID игрока:\n&8%2%) [из снимка](#00fb9a) [%3%](#00fb9a show_text=&7UUID снимка:\n&8%4%) [успешно восстановлены.](#00fb9a)'
|
data_restored: '[⏪ Данные пользователя](#00fb9a) [%1%](#00fb9a show_text=&7UUID игрока:\n&8%2%) [из снимка](#00fb9a) [%3%](#00fb9a show_text=&7UUID снимка:\n&8%4%) [успешно восстановлены.](#00fb9a)'
|
||||||
data_pinned: '[※ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID игрока:\n&8%4%) [успешно закреплен.](#00fb9a)'
|
data_pinned: '[※ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID игрока:\n&8%4%) [успешно закреплен.](#00fb9a)'
|
||||||
data_unpinned: '[※ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID пользователя:\n&8%4%) [успешно откреплен.](#00fb9a)'
|
data_unpinned: '[※ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID пользователя:\n&8%4%) [успешно откреплен.](#00fb9a)'
|
||||||
data_dumped: '[☂ Дамп снимка данных %1% пользователя %2% успешно выгружен:](#00fb9a) &7%3%'
|
data_dumped: '[☂ Дамп снимка данных %1% пользователя %2% успешно выгружен:](#00fb9a)'
|
||||||
list_footer: '\n%1%[Страница](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%\n'
|
list_footer: '\n%1%[Страница](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%\n'
|
||||||
list_previous_page_button: '[◀](white show_text=&7Просмотр предыдущей страницы run_command=%2% %1%) '
|
list_previous_page_button: '[◀](white show_text=&7Просмотр предыдущей страницы run_command=%2% %1%) '
|
||||||
list_next_page_button: ' [▶](white show_text=&7Просмотр следующей страницы run_command=%2% %1%)'
|
list_next_page_button: ' [▶](white show_text=&7Просмотр следующей страницы run_command=%2% %1%)'
|
||||||
@@ -40,6 +41,8 @@ locales:
|
|||||||
save_cause_world_save: 'сохранение мира'
|
save_cause_world_save: 'сохранение мира'
|
||||||
save_cause_death: 'смерть'
|
save_cause_death: 'смерть'
|
||||||
save_cause_server_shutdown: 'отключение сервера'
|
save_cause_server_shutdown: 'отключение сервера'
|
||||||
|
save_cause_save_command: 'save command'
|
||||||
|
save_cause_dump_command: 'dump command'
|
||||||
save_cause_inventory_command: 'команда inventory'
|
save_cause_inventory_command: 'команда inventory'
|
||||||
save_cause_enderchest_command: 'команда enderchest'
|
save_cause_enderchest_command: 'команда enderchest'
|
||||||
save_cause_backup_restore: 'восстановление из снимка'
|
save_cause_backup_restore: 'восстановление из снимка'
|
||||||
@@ -51,6 +54,9 @@ locales:
|
|||||||
update_available: '[HuskSync](#ff7e5e bold) [| Доступна новая версия HuskSync: v%1% (текущая: v%2%).](#ff7e5e)'
|
update_available: '[HuskSync](#ff7e5e bold) [| Доступна новая версия HuskSync: v%1% (текущая: v%2%).](#ff7e5e)'
|
||||||
reload_complete: '[HuskSync](#00fb9a bold) [| Конфигурация и файлы локализации перезагружены.](#00fb9a)\n[⚠ Убедитесь, что файлы конфигурации обновлены на всех серверах!](#00fb9a)\n[Необходима перезагрузка для вступления изменений конфигурации в силу.](#00fb9a italic)'
|
reload_complete: '[HuskSync](#00fb9a bold) [| Конфигурация и файлы локализации перезагружены.](#00fb9a)\n[⚠ Убедитесь, что файлы конфигурации обновлены на всех серверах!](#00fb9a)\n[Необходима перезагрузка для вступления изменений конфигурации в силу.](#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: '[%1%''ın kullanıcı veri anlıkları:](#00fb9a) [(%2%-%3% /](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[%1%''ın kullanıcı veri anlıkları:](#00fb9a) [(%2%-%3% /](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7Oyuncu Veri Anlığı %2% için %3%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Sabitlendi:\n&8Sabitlenmiş anlıklar otomatik olarak döndürülmez. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versiyon zaman damgası:&7\n&8Verinin ne zaman kaydedildiği\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Kaydetme sebebi:\n&8Verinin kaydedilme nedeni run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Anlık boyutu:&7\n&8Anlının tahmini dosya boyutu (KiB cinsinden) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7Oyuncu Veri Anlığı %2% için %3%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Sabitlendi:\n&8Sabitlenmiş anlıklar otomatik olarak döndürülmez. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versiyon zaman damgası:&7\n&8Verinin ne zaman kaydedildiği\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Kaydetme sebebi:\n&8Verinin kaydedilme nedeni run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Anlık boyutu:&7\n&8Anlının tahmini dosya boyutu (KiB cinsinden) run_command=/userdata view %2% %3%)'
|
||||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
|
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||||
data_deleted: '[❌ Kullanıcı veri anlığı başarıyla silindi](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
|
data_deleted: '[❌ Kullanıcı veri anlığı başarıyla silindi](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
|
||||||
data_restored: '[⏪ Başarıyla geri yüklendi](#00fb9a) [%1%](#00fb9a show_text=&7Oyuncu UUID:\n&8%2%)[''ın mevcut kullanıcı verisi anlığından](#00fb9a) [%3%.](#00fb9a show_text=&7Versiyon UUID:\n&8%4%)'
|
data_restored: '[⏪ Başarıyla geri yüklendi](#00fb9a) [%1%](#00fb9a show_text=&7Oyuncu UUID:\n&8%2%)[''ın mevcut kullanıcı verisi anlığından](#00fb9a) [%3%.](#00fb9a show_text=&7Versiyon UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Kullanıcı veri anlığı başarıyla sabitlendi](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
|
data_pinned: '[※ Kullanıcı veri anlığı başarıyla sabitlendi](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
|
||||||
data_unpinned: '[※ Kullanıcı veri anlığı başarıyla çözüldü](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
|
data_unpinned: '[※ Kullanıcı veri anlığı başarıyla çözüldü](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
|
||||||
data_dumped: '[☂ Kullanıcı veri anlığı başarıyla döküldü %1% için %2%:](#00fb9a) &7%3%'
|
data_dumped: '[☂ Kullanıcı veri anlığı başarıyla döküldü %1% için %2%:](#00fb9a)'
|
||||||
list_footer: '\n%1%[Sayfa](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
list_footer: '\n%1%[Sayfa](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||||
list_previous_page_button: '[◀](white show_text=&7Önceki sayfayı görüntüle run_command=%2% %1%) '
|
list_previous_page_button: '[◀](white show_text=&7Önceki sayfayı görüntüle run_command=%2% %1%) '
|
||||||
list_next_page_button: ' [▶](white show_text=&7Sonraki sayfayı görüntüle run_command=%2% %1%)'
|
list_next_page_button: ' [▶](white show_text=&7Sonraki sayfayı görüntüle run_command=%2% %1%)'
|
||||||
@@ -40,6 +41,8 @@ locales:
|
|||||||
save_cause_world_save: 'dünya kaydı'
|
save_cause_world_save: 'dünya kaydı'
|
||||||
save_cause_death: 'ölüm'
|
save_cause_death: 'ölüm'
|
||||||
save_cause_server_shutdown: 'sunucu kapatma'
|
save_cause_server_shutdown: 'sunucu kapatma'
|
||||||
|
save_cause_save_command: 'save command'
|
||||||
|
save_cause_dump_command: 'dump command'
|
||||||
save_cause_inventory_command: 'envanter komutu'
|
save_cause_inventory_command: 'envanter komutu'
|
||||||
save_cause_enderchest_command: 'ender sandığı komutu'
|
save_cause_enderchest_command: 'ender sandığı komutu'
|
||||||
save_cause_backup_restore: 'yedek geri yükleme'
|
save_cause_backup_restore: 'yedek geri yükleme'
|
||||||
@@ -51,6 +54,9 @@ locales:
|
|||||||
update_available: '[HuskSync](#ff7e5e bold) [| HuskSync\''in yeni bir sürümü mevcut: v%1% (kullanılan sürüm: v%2%).](#ff7e5e)'
|
update_available: '[HuskSync](#ff7e5e bold) [| HuskSync\''in yeni bir sürümü mevcut: v%1% (kullanılan sürüm: v%2%).](#ff7e5e)'
|
||||||
reload_complete: '[HuskSync](#00fb9a bold) [| Yapılandırma ve mesaj dosyaları yeniden yüklendi.](#00fb9a)\n[⚠ Lütfen yapılandırma dosyalarının tüm sunucularda güncel olduğundan emin olun!](#00fb9a)\n[Yapılandırma değişikliklerinin etkili olabilmesi için bir yeniden başlatma gereklidir.](#00fb9a italic)'
|
reload_complete: '[HuskSync](#00fb9a bold) [| Yapılandırma ve mesaj dosyaları yeniden yüklendi.](#00fb9a)\n[⚠ Lütfen yapılandırma dosyalarının tüm sunucularda güncel olduğundan emin olun!](#00fb9a)\n[Yapılandırma değişikliklerinin etkili olabilmesi için bir yeniden başlatma gereklidir.](#00fb9a italic)'
|
||||||
system_status_header: '[HuskSync](#00fb9a bold) [| Sistem durumu raporu:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| Sistem durumu raporu:](#00fb9a)'
|
||||||
|
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||||
|
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||||
|
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Hata:](#ff3300) [Yanlış sözdizimi. Kullanım:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Öneri için tıklayın Suggest_command=%1%)'
|
error_invalid_syntax: '[Hata:](#ff3300) [Yanlış sözdizimi. Kullanım:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Öneri için tıklayın Suggest_command=%1%)'
|
||||||
error_invalid_player: '[Hata:](#ff3300) [Bu isimde bir oyuncu bulunamadı.](#ff7e5e)'
|
error_invalid_player: '[Hata:](#ff3300) [Bu isimde bir oyuncu bulunamadı.](#ff7e5e)'
|
||||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
error_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) [| Перезавантажено конфіґ та файли повідомлень.](#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: '[%1%的玩家数据备份:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[%1%的玩家数据备份:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7玩家数据备份 %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7已置顶:\n&8已置顶的备份不会按照备份时间自动排序 run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7备份时间:&7\n&8数据保存时间\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7保存原因:\n&8导致数据保存的原因 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7备份大小:&7\n&8预计备份文件大小(以KiB为单位) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7玩家数据备份 %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7已置顶:\n&8已置顶的备份不会按照备份时间自动排序 run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7备份时间:&7\n&8数据保存时间\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7保存原因:\n&8导致数据保存的原因 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7备份大小:&7\n&8预计备份文件大小(以KiB为单位) run_command=/userdata view %2% %3%)'
|
||||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7%2%的用户数据快照\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7置顶:\n&8已置顶的快照不会自动排序. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&无效的快照数据\n&#ff7e5e&点击删除\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7%2%的用户数据快照\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7置顶:\n&8已置顶的快照不会自动排序. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&无效的快照数据\n&#ff7e5e&点击删除\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
|
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||||
data_deleted: '[❌ 成功删除玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&7%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本UUID:\n&7%2%)'
|
data_deleted: '[❌ 成功删除玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&7%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本UUID:\n&7%2%)'
|
||||||
data_restored: '[⏪ 成功恢复玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&7%2%)[的数据备份](#00fb9a) [%3%.](#00fb9a show_text=&7备份版本UUID:\n&7%4%)'
|
data_restored: '[⏪ 成功恢复玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&7%2%)[的数据备份](#00fb9a) [%3%.](#00fb9a show_text=&7备份版本UUID:\n&7%4%)'
|
||||||
data_pinned: '[※ 成功置顶玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本UUID:\n&8%2%)'
|
data_pinned: '[※ 成功置顶玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本UUID:\n&8%2%)'
|
||||||
data_unpinned: '[※ 成功取消置顶玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本UUID:\n&8%2%)'
|
data_unpinned: '[※ 成功取消置顶玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本UUID:\n&8%2%)'
|
||||||
data_dumped: '[☂ 已成功将 %1% 的玩家数据快照 %2% 转储到:](#00fb9a) &7%3%'
|
data_dumped: '[☂ 已成功将 %1% 的玩家数据快照 %2% 转储到:](#00fb9a)'
|
||||||
list_footer: '\n%1%[页数](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
list_footer: '\n%1%[页数](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||||
list_previous_page_button: '[◀](white show_text=&7查看上一页 run_command=%2% %1%) '
|
list_previous_page_button: '[◀](white show_text=&7查看上一页 run_command=%2% %1%) '
|
||||||
list_next_page_button: ' [▶](white show_text=&7查看下一页 run_command=%2% %1%)'
|
list_next_page_button: ' [▶](white show_text=&7查看下一页 run_command=%2% %1%)'
|
||||||
@@ -40,6 +41,8 @@ locales:
|
|||||||
save_cause_world_save: '保存世界'
|
save_cause_world_save: '保存世界'
|
||||||
save_cause_death: '死亡'
|
save_cause_death: '死亡'
|
||||||
save_cause_server_shutdown: '服务器关闭'
|
save_cause_server_shutdown: '服务器关闭'
|
||||||
|
save_cause_save_command: 'save command'
|
||||||
|
save_cause_dump_command: 'dump command'
|
||||||
save_cause_inventory_command: '背包命令'
|
save_cause_inventory_command: '背包命令'
|
||||||
save_cause_enderchest_command: '末影箱命令'
|
save_cause_enderchest_command: '末影箱命令'
|
||||||
save_cause_backup_restore: '备份还原'
|
save_cause_backup_restore: '备份还原'
|
||||||
@@ -51,6 +54,9 @@ locales:
|
|||||||
update_available: '[HuskSync](#ff7e5e bold) [| 检测到HuskSync有新版本可以更新了:v%1%(当前版本:v%2%).](#ff7e5e)'
|
update_available: '[HuskSync](#ff7e5e bold) [| 检测到HuskSync有新版本可以更新了:v%1%(当前版本:v%2%).](#ff7e5e)'
|
||||||
reload_complete: '[HuskSync](#00fb9a bold) [| 重新加载配置和消息文件完成.](#00fb9a)\n[⚠ 确保在所有服务器上更新配置文件!](#00fb9a)\n[需要重新启动才能使配置更改生效.](#00fb9a italic)'
|
reload_complete: '[HuskSync](#00fb9a bold) [| 重新加载配置和消息文件完成.](#00fb9a)\n[⚠ 确保在所有服务器上更新配置文件!](#00fb9a)\n[需要重新启动才能使配置更改生效.](#00fb9a italic)'
|
||||||
system_status_header: '[HuskSync](#00fb9a bold) [| 系统状态报告:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| 系统状态报告:](#00fb9a)'
|
||||||
|
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||||
|
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||||
|
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||||
error_invalid_syntax: '[错误:](#ff3300) [语法错误.用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&点击建议 suggest_command=%1%)'
|
error_invalid_syntax: '[错误:](#ff3300) [语法错误.用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&点击建议 suggest_command=%1%)'
|
||||||
error_invalid_player: '[错误:](#ff3300) [找不到这个名称的玩家.](#ff7e5e)'
|
error_invalid_player: '[错误:](#ff3300) [找不到这个名称的玩家.](#ff7e5e)'
|
||||||
error_invalid_data: '[错误:](#ff3300) [无法解压缩快照数据, 因为它无效或已损坏.](#ff7e5e) [(详情…)](gray show_text=&7⚠ %1%)'
|
error_invalid_data: '[错误:](#ff3300) [无法解压缩快照数据, 因为它无效或已损坏.](#ff7e5e) [(详情…)](gray show_text=&7⚠ %1%)'
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ locales:
|
|||||||
data_list_title: '[%1% 的玩家資料快照:](#00fb9a) [(%2%-%3% 共](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[%1% 的玩家資料快照:](#00fb9a) [(%2%-%3% 共](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7玩家資料快照 %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7已標記:\n&8標記的快照將不會自動輪換。 run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7版本時間戳:\n&8資料儲存時間\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7儲存原因:\n&8觸發儲存的原因 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7快照大小:\n&8快照的預估檔案大小(KiB) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7玩家資料快照 %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7已標記:\n&8標記的快照將不會自動輪換。 run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7版本時間戳:\n&8資料儲存時間\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7儲存原因:\n&8觸發儲存的原因 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7快照大小:\n&8快照的預估檔案大小(KiB) run_command=/userdata view %2% %3%)'
|
||||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7玩家資料快照 %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7已標記:\n&8標記的快照將不會自動輪換。 suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&無效的資料快照\n&#ff7e5e&點擊刪除\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7玩家資料快照 %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7已標記:\n&8標記的快照將不會自動輪換。 suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&無效的資料快照\n&#ff7e5e&點擊刪除\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
|
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||||
data_deleted: '[❌ 成功刪除:](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%)'
|
data_deleted: '[❌ 成功刪除:](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%)'
|
||||||
data_restored: '[⏪ 成功將玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&8%2%)[的資料恢復為 快照:](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
data_restored: '[⏪ 成功將玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&8%2%)[的資料恢復為 快照:](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ 成功標記](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%)'
|
data_pinned: '[※ 成功標記](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%)'
|
||||||
data_unpinned: '[※ 成功解除](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [的標記](#00fb9a)'
|
data_unpinned: '[※ 成功解除](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [的標記](#00fb9a)'
|
||||||
data_dumped: '[☂ 成功將 %2% 資料快照 %1% 儲存至:](#00fb9a) &7%3%'
|
data_dumped: '[☂ 成功將 %2% 資料快照 %1% 儲存至:](#00fb9a)'
|
||||||
list_footer: '\n%1%[頁面](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
list_footer: '\n%1%[頁面](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||||
list_previous_page_button: '[◀](white show_text=&7查看上一頁 run_command=%2% %1%) '
|
list_previous_page_button: '[◀](white show_text=&7查看上一頁 run_command=%2% %1%) '
|
||||||
list_next_page_button: ' [▶](white show_text=&7查看下一頁 run_command=%2% %1%)'
|
list_next_page_button: ' [▶](white show_text=&7查看下一頁 run_command=%2% %1%)'
|
||||||
@@ -40,6 +41,8 @@ locales:
|
|||||||
save_cause_world_save: '世界儲存'
|
save_cause_world_save: '世界儲存'
|
||||||
save_cause_death: '死亡'
|
save_cause_death: '死亡'
|
||||||
save_cause_server_shutdown: '伺服器關閉'
|
save_cause_server_shutdown: '伺服器關閉'
|
||||||
|
save_cause_save_command: 'save command'
|
||||||
|
save_cause_dump_command: 'dump command'
|
||||||
save_cause_inventory_command: '背包指令'
|
save_cause_inventory_command: '背包指令'
|
||||||
save_cause_enderchest_command: '終界箱指令'
|
save_cause_enderchest_command: '終界箱指令'
|
||||||
save_cause_backup_restore: '備份還原'
|
save_cause_backup_restore: '備份還原'
|
||||||
@@ -51,6 +54,9 @@ locales:
|
|||||||
update_available: '[HuskSync](#ff7e5e bold) [| 發現可用的新版本: v%1% (running: v%2%).](#ff7e5e)'
|
update_available: '[HuskSync](#ff7e5e bold) [| 發現可用的新版本: v%1% (running: v%2%).](#ff7e5e)'
|
||||||
reload_complete: '[HuskSync](#00fb9a bold) [| 配置和語言文件已重新加載。](#00fb9a)\n[⚠ 確保所有伺服器上的配置文件都是最新的!](#00fb9a)\n[重啟後配置變更才會生效。](#00fb9a italic)'
|
reload_complete: '[HuskSync](#00fb9a bold) [| 配置和語言文件已重新加載。](#00fb9a)\n[⚠ 確保所有伺服器上的配置文件都是最新的!](#00fb9a)\n[重啟後配置變更才會生效。](#00fb9a italic)'
|
||||||
system_status_header: '[HuskSync](#00fb9a bold) [| 系統狀態報告:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| 系統狀態報告:](#00fb9a)'
|
||||||
|
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||||
|
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||||
|
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||||
error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&點擊建議 suggest_command=%1%)'
|
error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&點擊建議 suggest_command=%1%)'
|
||||||
error_invalid_player: '[錯誤:](#ff3300) [找不到這位玩家](#ff7e5e)'
|
error_invalid_player: '[錯誤:](#ff3300) [找不到這位玩家](#ff7e5e)'
|
||||||
error_invalid_data: '[錯誤:](#ff3300) [無法解壓使用者資料,因為快照無效或已損壞。](#ff7e5e) [(詳細資訊…)](gray show_text=&7⚠ %1%)'
|
error_invalid_data: '[錯誤:](#ff3300) [無法解壓使用者資料,因為快照無效或已損壞。](#ff7e5e) [(詳細資訊…)](gray show_text=&7⚠ %1%)'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
> **Warning:** API v2 is no longer supported or compatible with HuskSync v3.0. See [[Data Snapshot API]] for the equivalent v3 API. 🚨
|
|
||||||
|
|
||||||
HuskSync v2.0 provides an API for fetching and retrieving `UserData`; a snapshot of a user's synchronization.
|
HuskSync v2.0 provides an API for fetching and retrieving `UserData`; a snapshot of a user's synchronization.
|
||||||
|
|
||||||
|
> **Warning:** API v2 is no longer supported or compatible with HuskSync v3.0. See [[Data Snapshot API]] for the equivalent v3 API. 🚨
|
||||||
|
|
||||||
This page assumes you've read the general [[API]] introduction and imported HuskSync (v2.x) into your project, and added it as a dependency.
|
This page assumes you've read the general [[API]] introduction and imported HuskSync (v2.x) into your project, and added it as a dependency.
|
||||||
|
|
||||||
🚨 HuskSync API v2 only targets HuskSync v2.0-2.2.8. It is **not compatible with HuskSync v3.0+**. The equivalent API for HuskSync v3 is the [[Data Snapshot API]].
|
🚨 HuskSync API v2 only targets HuskSync v2.0-2.2.8. It is **not compatible with HuskSync v3.0+**. The equivalent API for HuskSync v3 is the [[Data Snapshot API]].
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ The HuskSync API is available for the following platforms:
|
|||||||
* `fabric` - Fabric API for Minecraft. Provides Fabric API event listeners and adapters to `net.minecraft` objects.
|
* `fabric` - Fabric API for Minecraft. Provides Fabric API event listeners and adapters to `net.minecraft` objects.
|
||||||
* `common` - Common API for all platforms.
|
* `common` - Common API for all platforms.
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Targeting older versions</summary>
|
<summary>Targeting older versions</summary>
|
||||||
|
|
||||||
@@ -53,12 +52,12 @@ Add the repository to your `pom.xml` as per below. You can alternatively specify
|
|||||||
</repository>
|
</repository>
|
||||||
</repositories>
|
</repositories>
|
||||||
```
|
```
|
||||||
Add the dependency to your `pom.xml` as per below. Replace `VERSION` with the latest version of HuskSync (without the v): . Note for Fabric you must append the target Minecraft version to the version number (e.g. `3.6.1+1.20.1`).
|
Add the dependency to your `pom.xml` as per below. Replace `HUSKSYNC_VERSION` with the latest version of HuskSync (without the v): . and `MINECRAFT_VERSION` with the version of Minecraft you want to target (e.g. `1.20.1`). A correctly formed version target should look like: `3.7+1.20.1`. Omit the plus symbol and Minecraft version if you are targeting the `common` platform.
|
||||||
```xml
|
```xml
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.william278.husksync</groupId>
|
<groupId>net.william278.husksync</groupId>
|
||||||
<artifactId>husksync-PLATFORM</artifactId>
|
<artifactId>husksync-PLATFORM</artifactId>
|
||||||
<version>VERSION</version>
|
<version>HUSKSYNC_VERSION+MINECRAFT_VERSION</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
@@ -76,11 +75,11 @@ allprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
Add the dependency as per below. Replace `VERSION` with the latest version of HuskSync (without the v): . Note for Fabric you must append the target Minecraft version to the version number (e.g. `3.6.1+1.20.1`).
|
Add the dependency as per below. Replace `HUSKSYNC_VERSION` with the latest version of HuskSync (without the v): . and `MINECRAFT_VERSION` with the version of Minecraft you want to target (e.g. `1.20.1`). A correctly formed version target should look like: `3.7+1.20.1`. Omit the plus symbol and Minecraft version if you are targeting the `common` platform.
|
||||||
|
|
||||||
```groovy
|
```groovy
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly 'net.william278.husksync:husksync-PLATFORM:VERSION'
|
compileOnly 'net.william278.husksync:husksync-PLATFORM:HUSKSYNC_VERSION+MINECRAFT_VERSION'
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ This page contains a table of HuskSync commands and their required permission no
|
|||||||
<tbody>
|
<tbody>
|
||||||
<!-- /husksync command -->
|
<!-- /husksync command -->
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="6"><code>/husksync</code></td>
|
<td rowspan="7"><code>/husksync</code></td>
|
||||||
<td><code>/husksync</code></td>
|
<td><code>/husksync</code></td>
|
||||||
<td>View & manage plugin system information</td>
|
<td>View & manage plugin system information</td>
|
||||||
<td><code>husksync.command.husksync</code></td>
|
<td><code>husksync.command.husksync</code></td>
|
||||||
@@ -26,6 +26,11 @@ This page contains a table of HuskSync commands and their required permission no
|
|||||||
<td>View plugin system status information</td>
|
<td>View plugin system status information</td>
|
||||||
<td><code>husksync.command.husksync.status</code></td>
|
<td><code>husksync.command.husksync.status</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>/husksync dump</code></td>
|
||||||
|
<td>Perform a web dump of the plugin system & server status.</td>
|
||||||
|
<td><code>husksync.command.husksync.dump</code></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>/husksync reload</code></td>
|
<td><code>/husksync reload</code></td>
|
||||||
<td>Reload the plugin configuration</td>
|
<td>Reload the plugin configuration</td>
|
||||||
@@ -43,7 +48,7 @@ This page contains a table of HuskSync commands and their required permission no
|
|||||||
</tr>
|
</tr>
|
||||||
<!-- /userdata command -->
|
<!-- /userdata command -->
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="7"><code>/userdata</code></td>
|
<td rowspan="8"><code>/userdata</code></td>
|
||||||
<td><code>/userdata</code></td>
|
<td><code>/userdata</code></td>
|
||||||
<td>View & manage user data snapshots</td>
|
<td>View & manage user data snapshots</td>
|
||||||
<td><code>husksync.command.userdata</code></td>
|
<td><code>husksync.command.userdata</code></td>
|
||||||
@@ -63,6 +68,11 @@ This page contains a table of HuskSync commands and their required permission no
|
|||||||
<td>Restore a data snapshot for a user</td>
|
<td>Restore a data snapshot for a user</td>
|
||||||
<td><code>husksync.command.userdata.restore</code></td>
|
<td><code>husksync.command.userdata.restore</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>/userdata save</code></td>
|
||||||
|
<td>Create and save a snapshot of a user's current data</td>
|
||||||
|
<td><code>husksync.command.userdata.save</code></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>/userdata delete</code></td>
|
<td><code>/userdata delete</code></td>
|
||||||
<td>Delete user data snapshots</td>
|
<td>Delete user data snapshots</td>
|
||||||
|
|||||||
35
docs/Compatibility.md
Normal file
35
docs/Compatibility.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
HuskSync supports the following versions of Minecraft. Since v3.7, you must download the correct version of HuskSync for your server:
|
||||||
|
|
||||||
|
| Minecraft | Latest HuskSync | Java Version | Platforms | Support Status |
|
||||||
|
|:---------------:|:---------------:|:------------:|:--------------|:-----------------------------|
|
||||||
|
| 1.21.4 | _latest_ | 21 | Paper, Fabric | ✅ **Active Release** |
|
||||||
|
| 1.21.3 | 3.7.1 | 21 | Paper, Fabric | 🗃️ Archived (December 2024) |
|
||||||
|
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **November 2025** (LTS) |
|
||||||
|
| 1.20.6 | 3.6.8 | 17 | Paper | 🗃️ Archived (October 2024) |
|
||||||
|
| 1.20.4 | 3.6.8 | 17 | Paper | 🗃️ Archived (July 2024) |
|
||||||
|
| 1.20.1 | _latest_ | 17 | Paper, Fabric | ✅ **November 2025** (LTS) |
|
||||||
|
| 1.17.1 - 1.19.4 | 3.6.8 | 17 | Paper | 🗃️ Archived |
|
||||||
|
| 1.16.5 | 3.2.1 | 16 | Paper | 🗃️ Archived |
|
||||||
|
|
||||||
|
HuskSync is primarily developed against the latest release. Old Minecraft versions are allocated a support channel based on popularity, mod support, etc:
|
||||||
|
|
||||||
|
* Long Term Support (LTS) – Supported for up to 12-18 months
|
||||||
|
* Non-Long Term Support (Non-LTS) – Supported for 3-6 months
|
||||||
|
|
||||||
|
## Incompatible versions
|
||||||
|
This plugin does not support the following software-Minecraft version combinations. The plugin will fail to load if you attempt to run it with these versions. Apologies for the inconvenience.
|
||||||
|
|
||||||
|
| Minecraft | Server Software | Notes |
|
||||||
|
|-------------------|-------------------------------------------|----------------------------------------|
|
||||||
|
| 1.19.4 | Only: `Purpur, Pufferfish`† | Older Paper builds also not supported. |
|
||||||
|
| 1.19.3 | Only: `Paper, Purpur, Pufferfish`† | Upgrade to 1.19.4 or use Spigot |
|
||||||
|
| 1.16.5 | _All_ | Please use v3.3.1 or lower |
|
||||||
|
| below 1.16.5 | _All_ | Upgrade to Minecraft 1.16.5 |
|
||||||
|
|
||||||
|
†Further downstream forks of this server software are also affected.
|
||||||
|
|
||||||
|
## Incompatible plugins / mods
|
||||||
|
Please note the following plugins / mods can cause issues with HuskSync:
|
||||||
|
|
||||||
|
* Restart plugins / mods are not supported. These will cause [player data to not save correctly when your server restarts](troubleshooting#issues-with-player-data-going-out-of-sync-during-a-server-restart) due to the way these plugins utilise bash scripts. It's important to understand that restart plugins don't actually restart yur server, they just trigger some (often unstable) process-killing scripting logic to occur!
|
||||||
|
* Combat logging plugins / mods are not supported. Some have built-in support for HuskSync and should work as expected, but for others you may wish to modify the [[Event Priorities]]
|
||||||
@@ -65,7 +65,7 @@ database:
|
|||||||
user_data: husksync_user_data
|
user_data: husksync_user_data
|
||||||
# Redis settings
|
# Redis settings
|
||||||
redis:
|
redis:
|
||||||
# Specify the credentials of your Redis database here. Set "password" to '' if you don't have one
|
# Specify the credentials of your Redis server here. Set "password" to '' if you don't have one
|
||||||
credentials:
|
credentials:
|
||||||
host: localhost
|
host: localhost
|
||||||
port: 6379
|
port: 6379
|
||||||
@@ -109,7 +109,7 @@ synchronization:
|
|||||||
sync_dead_players_changing_server: true
|
sync_dead_players_changing_server: true
|
||||||
# Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing
|
# Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing
|
||||||
compress_data: true
|
compress_data: true
|
||||||
# Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)
|
# Where to display sync notifications (ACTION_BAR, CHAT or NONE)
|
||||||
notification_display_slot: ACTION_BAR
|
notification_display_slot: ACTION_BAR
|
||||||
# Persist maps locked in a Cartography Table to let them be viewed on any server
|
# Persist maps locked in a Cartography Table to let them be viewed on any server
|
||||||
persist_locked_maps: true
|
persist_locked_maps: true
|
||||||
@@ -134,9 +134,26 @@ synchronization:
|
|||||||
# Commands which should be blocked before a player has finished syncing (Use * to block all commands)
|
# Commands which should be blocked before a player has finished syncing (Use * to block all commands)
|
||||||
blacklisted_commands_while_locked:
|
blacklisted_commands_while_locked:
|
||||||
- '*'
|
- '*'
|
||||||
# For attribute syncing, which attributes should be ignored/skipped when syncing
|
# Configuration for how to sync attributes
|
||||||
# (e.g. ['minecraft:generic.max_health', 'minecraft:generic.attack_damage'])
|
attributes:
|
||||||
ignored_attributes: []
|
# Which attribute types should be saved as part of attribute syncing. Supports wildcard matching.
|
||||||
|
# (e.g. ['minecraft:generic.max_health', 'minecraft:generic.*'])
|
||||||
|
synced_attributes:
|
||||||
|
- "minecraft:generic.max_health"
|
||||||
|
- "minecraft:max_health"
|
||||||
|
- "minecraft:generic.max_absorption"
|
||||||
|
- "minecraft:max_absorption"
|
||||||
|
- "minecraft:generic.luck"
|
||||||
|
- "minecraft:luck"
|
||||||
|
- "minecraft:generic.scale"
|
||||||
|
- "minecraft:scale"
|
||||||
|
- "minecraft:generic.step_height"
|
||||||
|
- "minecraft:step_height"
|
||||||
|
- "minecraft:generic.gravity"
|
||||||
|
- "minecraft:gravity"
|
||||||
|
# Which attribute modifiers should not be saved when syncing users. Supports wildcard matching.
|
||||||
|
# (e.g. ['minecraft:effect.speed', 'minecraft:effect.*'])
|
||||||
|
ignored_modifiers: ['minecraft:effect.*', 'minecraft:creative_mode_*']
|
||||||
# Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts
|
# Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts
|
||||||
event_priorities:
|
event_priorities:
|
||||||
quit_listener: LOWEST
|
quit_listener: LOWEST
|
||||||
|
|||||||
@@ -37,12 +37,13 @@ huskSyncAPI.getUser(uuid).thenAccept(optionalUser -> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The User object provides methods for getting a user's UUID and username
|
// The User object provides methods for getting a user's UUID and username
|
||||||
System.out.println("Found %s", optionalUser.get().getUsername());
|
System.out.println("Found %s", optionalUser.get().getName());
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
* If you have an online `org.bukkit.Player` object, you can use `BukkitPlayer#adapt(player)` to get an `OnlineUser` (extends `User`), representing a logged-in user.
|
* If you have an online `org.bukkit.Player` object, you can use `BukkitPlayer#adapt(player)` to get an `OnlineUser` (extends `User`), representing a logged-in user.
|
||||||
|
* You can also use `#getOnlineUser(UUID)` to get an OnlineUser by their UUID - note this only works for players online on the server the logic is called from, however.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Code Example — Getting an online user</summary>
|
<summary>Code Example — Getting an online user</summary>
|
||||||
@@ -50,7 +51,7 @@ huskSyncAPI.getUser(uuid).thenAccept(optionalUser -> {
|
|||||||
```java
|
```java
|
||||||
// Get an online user
|
// Get an online user
|
||||||
OnlineUser user = huskSyncAPI.getUser(player);
|
OnlineUser user = huskSyncAPI.getUser(player);
|
||||||
System.out.println("Hello, %s!", user.getUsername());
|
System.out.println("Hello, %s!", user.getName());
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ System.out.println("Hello, %s!", user.getUsername());
|
|||||||
// Get a user's current data
|
// Get a user's current data
|
||||||
huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> {
|
huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> {
|
||||||
if (optionalSnapshot.isEmpty()) {
|
if (optionalSnapshot.isEmpty()) {
|
||||||
System.out.println("Couldn't get data for %s", user.getUsername());
|
System.out.println("Couldn't get data for %s", user.getName());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> {
|
|||||||
// Get a user's latest saved snapshot
|
// Get a user's latest saved snapshot
|
||||||
huskSyncAPI.getLatestSnapshot(user).thenAccept(optionalSnapshot -> {
|
huskSyncAPI.getLatestSnapshot(user).thenAccept(optionalSnapshot -> {
|
||||||
if (optionalSnapshot.isEmpty()) {
|
if (optionalSnapshot.isEmpty()) {
|
||||||
System.out.println("%s has no saved snapshots!", user.getUsername());
|
System.out.println("%s has no saved snapshots!", user.getName());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@ huskSyncAPI.getLatestSnapshot(user).thenAccept(optionalSnapshot -> {
|
|||||||
// Get a user's saved snapshots
|
// Get a user's saved snapshots
|
||||||
huskSyncAPI.getSnapshots(user).thenAccept(optionalSnapshots -> {
|
huskSyncAPI.getSnapshots(user).thenAccept(optionalSnapshots -> {
|
||||||
if (optionalSnapshots.isEmpty()) {
|
if (optionalSnapshots.isEmpty()) {
|
||||||
System.out.println("%s has no saved snapshots!", user.getUsername());
|
System.out.println("%s has no saved snapshots!", user.getName());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
73
docs/Database.md
Normal file
73
docs/Database.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
HuskSync persists player data and snapshots in a database of your choice. This is separate from a [[Redis]] server, which HuskSync uses for caching and inter-server messaging, which is also required to use HuskSync.
|
||||||
|
|
||||||
|
## Database types
|
||||||
|
> **Warning:** There is no automatic way of migrating between _database_ types. Changing the database type will cause data to be lost.
|
||||||
|
|
||||||
|
| Type | Database Software |
|
||||||
|
|:--------------------------|:--------------------------|
|
||||||
|
| `MYSQL` | MySQL 8.0 or newer |
|
||||||
|
| `MARIADB` | MariaDB 5.0 or newer |
|
||||||
|
| `POSTGRES` | PostgreSQL |
|
||||||
|
| [`MONGO`](#mongodb-setup) | MongoDB |
|
||||||
|
|
||||||
|
## Configuring
|
||||||
|
To change the database type, navigate to your [`config.yml`](Config-File) file and modify the properties under `database`.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Database options (config.yml)</summary>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Database settings
|
||||||
|
database:
|
||||||
|
# Type of database to use (MYSQL, MARIADB, POSTGRES, MONGO)
|
||||||
|
type: MYSQL
|
||||||
|
# Specify credentials here for your MYSQL, MARIADB, POSTGRES OR MONGO database
|
||||||
|
credentials:
|
||||||
|
host: localhost
|
||||||
|
port: 3306
|
||||||
|
database: minecraft
|
||||||
|
username: root
|
||||||
|
password: ''
|
||||||
|
# Only change this if you're using MARIADB or POSTGRES
|
||||||
|
parameters: ?autoReconnect=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8
|
||||||
|
# MYSQL, MARIADB, POSTGRES database Hikari connection pool properties. Don't modify this unless you know what you're doing!
|
||||||
|
connection_pool:
|
||||||
|
maximum_pool_size: 10
|
||||||
|
minimum_idle: 10
|
||||||
|
maximum_lifetime: 1800000
|
||||||
|
keepalive_time: 0
|
||||||
|
connection_timeout: 5000
|
||||||
|
# Advanced MongoDB settings. Don't modify unless you know what you're doing!
|
||||||
|
mongo_settings:
|
||||||
|
using_atlas: false
|
||||||
|
parameters: ?retryWrites=true&w=majority&authSource=HuskSync
|
||||||
|
# Names of tables to use on your database. Don't modify this unless you know what you're doing!
|
||||||
|
table_names:
|
||||||
|
users: husksync_users
|
||||||
|
user_data: husksync_user_data
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Credentials
|
||||||
|
You will need to specify the credentials (hostname, port, username, password and the database). These credentials are used to connect to your database server.
|
||||||
|
|
||||||
|
If your database server account doesn't have a password (not recommended), leave the password field blank (`password: ''`') and the plugin will attempt to connect without a password.
|
||||||
|
|
||||||
|
### Connection Pool properties
|
||||||
|
If you're using MySQL, MariaDB, or PostgreSQL as your database type, you can modify the HikariCP connection pool properties if you know what you're doing.
|
||||||
|
|
||||||
|
Please note that modifying these values can cause issues if you don't know what you're doing. The default values should be fine for most users.
|
||||||
|
|
||||||
|
## MongoDB Setup
|
||||||
|
If you're using a MongoDB database, in addition to setting the database type to `MONGO`, you'll need to perform slightly different configuration steps.
|
||||||
|
|
||||||
|
- Under `credentials` in the `database` section, enter the credentials of your MongoDB Database. You shouldn't touch the `connection_pool` properties.
|
||||||
|
- Under `parameters` in the `mongo_settings` section, ensure the specified `&authSource=` matches the database you are using (default is `HuskSync`).
|
||||||
|
|
||||||
|
### MongoDB Atlas setup
|
||||||
|
If you're using a MongoDB Atlas database, you'll also need to set the Atlas settings and adjust the connection parameters string.
|
||||||
|
|
||||||
|
- Set `using_atlas` in the `mongo_settings` section to `true`.
|
||||||
|
- Remove `&authSource=HuskSync` from `parameters` in the `mongo_settings`.
|
||||||
|
|
||||||
|
Note that the `port` setting in `credentials` is ignored when using Atlas.
|
||||||
55
docs/FAQs.md
55
docs/FAQs.md
@@ -1,9 +1,9 @@
|
|||||||
This page addresses a number of frequently asked questions about the plugin.
|
This page addresses a number of frequently asked questions about HuskSync.
|
||||||
|
|
||||||
## Frequently Asked Questions
|
## Frequently Asked Questions
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary> <b>What data can be synchronized?</b></summary>
|
<summary> <b>What data can be synced?</b></summary>
|
||||||
|
|
||||||
HuskSync supports synchronising a wide range of different data elements, each of which can be toggled to your liking. Please check out the [[Sync Features]] page for a full list.
|
HuskSync supports synchronising a wide range of different data elements, each of which can be toggled to your liking. Please check out the [[Sync Features]] page for a full list.
|
||||||
|
|
||||||
@@ -30,16 +30,56 @@ Please note we cannot guarantee compatibility with everything — test thoro
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary> <b>Is Redis required? What is Redis?</b></summary>
|
<summary> <b>What versions of Minecraft does HuskSync support?</b></summary>
|
||||||
|
|
||||||
Yes! HuskSync requires Redis to operate (for reasons demonstrated below).
|
Check the [[Compatibility]] table. In addition to the latest release of Minecraft, the latest version of HuskSync will support specific older versions based on popularity and mod support.
|
||||||
|
|
||||||
Redis is an in-memory database server used for caching data at scale and sending messages across a network. You have a Redis server in a similar fashion to the way you have a MySQL database server. If you're using a Minecraft hosting company, you'll want to contact their support and ask if they offer Redis. If you're looking for a host, I have a list of some popular hosts and whether they support Redis [available to read here.](https://william278.net/redis-hosts)
|
If your server's version of Minecraft isn't supported by the latest release, there's plenty of older, stable versions of HuskSync you can download, though note support for these versions will be limited.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary> <b>How does the plugin synchronize data?</b></summary>
|
<summary> <b>What do I need to run HuskSync?</b></summary>
|
||||||
|
|
||||||
|
See the [Requirements](setup#requirements) section under Setup.
|
||||||
|
|
||||||
|
You need a [[Database]] server, a [[Redis]] server, and [compatible Minecraft servers](compatibility).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary> <b>Is Redis required? What is Redis?</b></summary>
|
||||||
|
|
||||||
|
Yes, HuskSync requires a [[Redis]] server **in addition to a [[Database]] server** to operate.
|
||||||
|
|
||||||
|
Redis is an in-memory database server used for caching data at scale and sending messages across a network. You have a Redis server in a similar fashion to the way you have a MySQL database server. If you're using a Minecraft hosting company, you'll want to contact their support and ask if they offer Redis. If you're looking for a host, I have a list of some popular hosts and whether they support Redis [available to view here.](https://william278.net/docs/website/redis-hosts)
|
||||||
|
|
||||||
|
For more information, check our [Redis setup instructions](redis).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary> <b>How much RAM does my Redis server need?</b></summary>
|
||||||
|
|
||||||
|
We recommend your Redis server has 1GB of RAM, and that your Redis server is installed locally (on the same server as your game servers, or at least on the server running your Velocity/BungeeCord/Waterfall proxy).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary> <b>Is a Database required? What Databases are supported?</b></summary>
|
||||||
|
|
||||||
|
Yes. HuskSync requires both a [[Database]] server and a [[Redis]] server to operate.
|
||||||
|
|
||||||
|
HuskSync supports the following database types:
|
||||||
|
* MySQL v8.0+
|
||||||
|
* MariaDB v5.0+
|
||||||
|
* PostgreSQL
|
||||||
|
* MongoDB
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary> <b>How does data syncing work?</b></summary>
|
||||||
|
|
||||||
HuskSync makes use of both MySQL and Redis for optimal data synchronization. You have the option of using one of two [[Sync Modes]], which synchronize data between servers (`DELAY` or `LOCKSTEP`)
|
HuskSync makes use of both MySQL and Redis for optimal data synchronization. You have the option of using one of two [[Sync Modes]], which synchronize data between servers (`DELAY` or `LOCKSTEP`)
|
||||||
|
|
||||||
@@ -71,9 +111,10 @@ Indeed, there exist economy plugins — such as [XConomy](https://github.com
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary> <b>Is this better than MySQLPlayerDataBridge?</b></summary>
|
<summary> <b>Is HuskSync better than MySQLPlayerDataBridge?</b></summary>
|
||||||
|
|
||||||
I can't provide a fair answer to this question! What I can say is that your mileage will of course vary.
|
I can't provide a fair answer to this question! What I can say is that your mileage will of course vary.
|
||||||
|
|
||||||
The performance improvements offered by HuskSync's synchronization method will depend on your network environment and the economies of scale that come with your player count. In terms of featureset, HuskSync does feature greater rollback and snapshot backup/management features if this is something you are looking for.
|
The performance improvements offered by HuskSync's synchronization method will depend on your network environment and the economies of scale that come with your player count. In terms of featureset, HuskSync does feature greater rollback and snapshot backup/management features if this is something you are looking for.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user