mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-21 15:49:20 +00:00
Compare commits
196 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
087c787ec2 | ||
|
|
7218390f65 | ||
|
|
bd312c48ea | ||
|
|
e4cc792f54 | ||
|
|
7941745ed0 | ||
|
|
21f125c48a | ||
|
|
18b8b958fe | ||
|
|
35c23c7970 | ||
|
|
4bb38f67d3 | ||
|
|
98cf42065b | ||
|
|
328d4476aa | ||
|
|
8293d767da | ||
|
|
7b8fb92737 | ||
|
|
0f1cc2d24f | ||
|
|
676ba7a10a | ||
|
|
82dc765f66 | ||
|
|
16cfbc9410 | ||
|
|
2b4c7e6c3d | ||
|
|
a03d540938 | ||
|
|
6bcb3e7908 | ||
|
|
facbda65a8 | ||
|
|
2f5ddf6164 | ||
|
|
4dfbc0e32b | ||
|
|
07d0376dd6 | ||
|
|
d23ea087c1 | ||
|
|
ea77f2d782 | ||
|
|
ef3dc7e602 | ||
|
|
3fe6245ddf | ||
|
|
a35e83a424 | ||
|
|
be5d1128de | ||
|
|
8463e1bb7a | ||
|
|
5456b232f0 | ||
|
|
b0e585841c | ||
|
|
cd298af5ae | ||
|
|
e19477aada | ||
|
|
7f75b9a917 | ||
|
|
819421492b | ||
|
|
8f13a3955c | ||
|
|
73de0ff392 | ||
|
|
93edb0de4c | ||
|
|
bb5ae0b741 | ||
|
|
ccd7601a0e | ||
|
|
50d15e9580 | ||
|
|
aa1e8b8e95 | ||
|
|
3ff01f7bb3 | ||
|
|
93ab25bf44 | ||
|
|
4c0addfd67 | ||
|
|
b77cf2524d | ||
|
|
501ea3f609 | ||
|
|
a93af95fd2 | ||
|
|
39767c5cd0 | ||
|
|
48f7037898 | ||
|
|
67dddf0cfa | ||
|
|
eeb5e57c1e | ||
|
|
5a6ea2cffe | ||
|
|
07ddd34f8e | ||
|
|
a0b86c298f | ||
|
|
6fbef032bc | ||
|
|
318aacd432 | ||
|
|
ba1b2ff62e | ||
|
|
67ef4888da | ||
|
|
a5d3015c6e | ||
|
|
131a364f53 | ||
|
|
19636d9447 | ||
|
|
f803a0b57b | ||
|
|
28afffe95e | ||
|
|
c7e100a78a | ||
|
|
12e223618d | ||
|
|
f6773f4e68 | ||
|
|
b9434a56e8 | ||
|
|
325fac41bf | ||
|
|
87377bffc1 | ||
|
|
c6fb7fb10f | ||
|
|
c2ae9bd20a | ||
|
|
e580c4f2bd | ||
|
|
dabd9bc57d | ||
|
|
fa7f6f0d6e | ||
|
|
267cf1ff35 | ||
|
|
08944ffd35 | ||
|
|
c75114b858 | ||
|
|
350a8b864d | ||
|
|
df0bd7a7cb | ||
|
|
9fc9e8caf4 | ||
|
|
2e3db2fffa | ||
|
|
530b3ef24d | ||
|
|
a9bd4dd2f0 | ||
|
|
85706d97c5 | ||
|
|
f7e3104e6b | ||
|
|
f56d7f6113 | ||
|
|
685431a40d | ||
|
|
9da3ff5281 | ||
|
|
24453d0e1a | ||
|
|
280e90e297 | ||
|
|
31920d056d | ||
|
|
6641e11fd9 | ||
|
|
66bbde0b5d | ||
|
|
7dde6423e4 | ||
|
|
0eac12e3f8 | ||
|
|
5df58e4ef9 | ||
|
|
4a6583d8bd | ||
|
|
059ee6f660 | ||
|
|
414246f243 | ||
|
|
a3e269c00b | ||
|
|
bf9f29ffe9 | ||
|
|
29bd2e1319 | ||
|
|
2475a9b3c6 | ||
|
|
2a52cc9086 | ||
|
|
237abf9698 | ||
|
|
adbc264532 | ||
|
|
f9cfec7d03 | ||
|
|
29805bfe04 | ||
|
|
8d2e5a6a52 | ||
|
|
d4f61bd646 | ||
|
|
55173be04b | ||
|
|
e7078c9542 | ||
|
|
2aa33b2f2c | ||
|
|
972fee1bc7 | ||
|
|
efe34977b5 | ||
|
|
02ed9687ee | ||
|
|
08889a1739 | ||
|
|
9cf6d1eab6 | ||
|
|
33c2eb2237 | ||
|
|
299586aa86 | ||
|
|
05c988f2c7 | ||
|
|
8e0ad76968 | ||
|
|
4db162e78f | ||
|
|
272bc1278a | ||
|
|
35fdcf7106 | ||
|
|
48e087a3d7 | ||
|
|
ca000197e4 | ||
|
|
a6bab88cee | ||
|
|
f0c64df439 | ||
|
|
ac5ab56717 | ||
|
|
c2025350ba | ||
|
|
4c2bb5c6df | ||
|
|
fb069296e1 | ||
|
|
22eedc8522 | ||
|
|
664c8c3352 | ||
|
|
e7e6f9cfa7 | ||
|
|
5ec0f1b098 | ||
|
|
8fad075357 | ||
|
|
83e27cca83 | ||
|
|
729230a646 | ||
|
|
029407613f | ||
|
|
3d6ff7c30b | ||
|
|
5833ce955f | ||
|
|
b3a5091828 | ||
|
|
693209ff00 | ||
|
|
5d1bd7c3a9 | ||
|
|
7b8c75dbeb | ||
|
|
b7a30bd6e9 | ||
|
|
2daf5fedef | ||
|
|
5fd40915d0 | ||
|
|
c49700e9ec | ||
|
|
0f35331441 | ||
|
|
0153e14ce5 | ||
|
|
419434bdca | ||
|
|
f1be4d2d88 | ||
|
|
c973dc5f05 | ||
|
|
b530941687 | ||
|
|
c09fde4c36 | ||
|
|
8d3beab145 | ||
|
|
cdf666bde6 | ||
|
|
350528e394 | ||
|
|
a1d3e5fddc | ||
|
|
e096e58c45 | ||
|
|
75eafe57e2 | ||
|
|
0005392cd3 | ||
|
|
93913ca4ef | ||
|
|
aa09639e55 | ||
|
|
b205643fdd | ||
|
|
6fc827dedf | ||
|
|
b8aa1d9701 | ||
|
|
2db3bb313f | ||
|
|
4d23377a18 | ||
|
|
51116cbdfb | ||
|
|
6831ce094d | ||
|
|
289227e763 | ||
|
|
3b8a9e4ed1 | ||
|
|
7db3ed678f | ||
|
|
6d9e68a65b | ||
|
|
2c33f3b0b4 | ||
|
|
c002d86fc0 | ||
|
|
a384de8e42 | ||
|
|
cae17f6e68 | ||
|
|
03ca335293 | ||
|
|
c2b9e6c932 | ||
|
|
518853c921 | ||
|
|
fe9dda31bd | ||
|
|
0fd29bca57 | ||
|
|
37a671dae9 | ||
|
|
c406f40898 | ||
|
|
7561762c25 | ||
|
|
d245245083 | ||
|
|
2b55e129b3 | ||
|
|
0caec74436 |
21
.github/dependabot.yml
vendored
21
.github/dependabot.yml
vendored
@@ -1,7 +1,22 @@
|
||||
# Dependabot configuration file for GitHub
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gradle" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
# CI workflow action updates
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "ci"
|
||||
|
||||
# Gradle package updates
|
||||
- package-ecosystem: "gradle"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "deps"
|
||||
ignore:
|
||||
- dependency-name: 'org.spigotmc:spigot-api'
|
||||
- dependency-name: 'org.papermc:paper-api'
|
||||
1
.github/funding.yml
vendored
1
.github/funding.yml
vendored
@@ -1,3 +1,4 @@
|
||||
# Funding metadata for GitHub
|
||||
|
||||
github: WiIIiam278
|
||||
custom: https://buymeacoff.ee/william278
|
||||
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@@ -1,4 +1,3 @@
|
||||
# Builds, tests the project with Gradle
|
||||
name: CI Tests
|
||||
|
||||
on:
|
||||
@@ -17,21 +16,29 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Set up JDK 17 📦'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Build with Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- name: 'Build with Gradle 🏗️'
|
||||
uses: gradle/gradle-build-action@v3
|
||||
with:
|
||||
arguments: build test publish
|
||||
env:
|
||||
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: Publish Test Report
|
||||
uses: mikepenz/action-junit-report@v3
|
||||
if: success() || failure() # always run even if the previous step fails
|
||||
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
- name: 'Fetch Version Name 📝'
|
||||
run: |
|
||||
echo "::set-output name=VERSION_NAME::$(${{github.workspace}}/gradlew properties --no-daemon --console=plain -q | grep "^version:" | awk '{printf $2}')"
|
||||
id: fetch-version
|
||||
- name: Get Version
|
||||
run: |
|
||||
echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV
|
||||
20
.github/workflows/pr_tests.yml
vendored
20
.github/workflows/pr_tests.yml
vendored
@@ -1,4 +1,3 @@
|
||||
# Carry out tests on pull requests
|
||||
name: PR Tests
|
||||
|
||||
on:
|
||||
@@ -7,18 +6,25 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
- name: 'Checkout for CI 🛎'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Set up JDK 17 📦'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Test Pull Request
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- name: 'Build with Gradle 🏗️'
|
||||
uses: gradle/gradle-build-action@v3
|
||||
with:
|
||||
arguments: test
|
||||
arguments: test
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@@ -1,9 +1,8 @@
|
||||
# Builds, tests and publishes to maven when a release is published
|
||||
name: Release Tests
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
types: [ 'published' ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -13,21 +12,22 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Set up JDK 17 📦'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Build with Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- name: 'Build with Gradle 🏗️'
|
||||
uses: gradle/gradle-build-action@v3
|
||||
with:
|
||||
arguments: build test publish
|
||||
env:
|
||||
RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
RELEASES_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: Publish Test Report
|
||||
uses: mikepenz/action-junit-report@v3
|
||||
if: success() || failure() # always run even if the previous step fails
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v4
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
16
.github/workflows/update_docs.yml
vendored
16
.github/workflows/update_docs.yml
vendored
@@ -1,4 +1,3 @@
|
||||
# Update the GitHub Wiki documentation when a push is made to docs/
|
||||
name: Update Docs
|
||||
|
||||
on:
|
||||
@@ -17,12 +16,9 @@ jobs:
|
||||
deploy-wiki:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Code'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Push Changes to Wiki'
|
||||
uses: Andrew-Chen-Wang/github-wiki-action@v3
|
||||
env:
|
||||
WIKI_DIR: 'docs/'
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_MAIL: 'actions@github.com'
|
||||
GH_NAME: 'github-actions[bot]'
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Push Docs to Github Wiki 📄️'
|
||||
uses: Andrew-Chen-Wang/github-wiki-action@v4
|
||||
with:
|
||||
path: 'docs'
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
JV=$(java -version 2>&1 >/dev/null | head -1)
|
||||
echo "$JV" | sed -E 's/^.*version "([^".]*)\.[^"]*".*$/\1/'
|
||||
|
||||
if [ "$JV" != 16 ]; then
|
||||
case "$1" in
|
||||
install)
|
||||
echo "installing sdkman..."
|
||||
curl -s "https://get.sdkman.io" | bash
|
||||
source ~/.sdkman/bin/sdkman-init.sh
|
||||
sdk install java 16.0.1-open
|
||||
;;
|
||||
use)
|
||||
echo "must source ~/.sdkman/bin/sdkman-init.sh"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
22
README.md
22
README.md
@@ -3,10 +3,10 @@
|
||||
<img src="images/banner.png" alt="HuskSync" />
|
||||
<a href="https://github.com/WiIIiam278/HuskSync/actions/workflows/ci.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/ci.yml?branch=master&logo=github"/>
|
||||
</a>
|
||||
<a href="https://jitpack.io/#net.william278/HuskSync">
|
||||
<img src="https://img.shields.io/jitpack/version/net.william278/HuskSync?color=%2300fb9a&label=api&logo=gradle" />
|
||||
</a>
|
||||
</a>
|
||||
<a href="https://repo.william278.net/#/releases/net/william278/husksync/">
|
||||
<img src="https://repo.william278.net/api/badge/latest/releases/net/william278/husksync?color=00fb9a&name=Maven&prefix=v" />
|
||||
</a>
|
||||
<a href="https://discord.gg/tVYhJfyDWG">
|
||||
<img src="https://img.shields.io/discord/818135932103557162.svg?label=&logo=discord&logoColor=fff&color=7389D8&labelColor=6A7EC2" />
|
||||
</a>
|
||||
@@ -26,7 +26,7 @@
|
||||
</p>
|
||||
<br/>
|
||||
|
||||
**HuskSync** is a modern, cross-server player data synchronization system that enables the comprehensive synchronization of your user's data across multiple proxied servers. It does this by making use of Redis and MySQL to optimally cache data while players change servers.
|
||||
**HuskSync** is a modern, cross-server player data synchronization system that enables the comprehensive synchronization of your user's data across multiple proxied servers. It does this by making use of Redis and a MySQL/Mongo/PostgreSQL to optimally cache data while players change servers.
|
||||
|
||||
## Features
|
||||
**⭐ Seamless synchronization** — Utilises optimised Redis caching when players change server to sync player data super quickly for a seamless experience.
|
||||
@@ -44,11 +44,11 @@
|
||||
**Ready?** [It's syncing time!](https://william278.net/docs/husksync/setup)
|
||||
|
||||
## Setup
|
||||
Requires a MySQL (v8.0+) database, a Redis (v5.0+) server and any number of Spigot-based 1.16.5+ Minecraft servers, running Java 16+.
|
||||
Requires a MySQL/Mongo/PostgreSQL database, a Redis (v5.0+) server and any number of Spigot-based 1.17.1+ Minecraft servers, running Java 17+.
|
||||
|
||||
1. Place the plugin jar file in the /plugins/ directory of each Spigot server. You do not need to install HuskSync as a proxy plugin.
|
||||
2. Start, then stop every server to let HuskSync generate the config file.
|
||||
3. Navigate to the HuskSync config file on each server (~/plugins/HuskSync/config.yml) and fill in both the MySQL and Redis database credentials.
|
||||
3. Navigate to the HuskSync config file on each server (~/plugins/HuskSync/config.yml) and fill in both your database and Redis server credentials.
|
||||
4. Start every server again and synchronization will begin.
|
||||
|
||||
## Development
|
||||
@@ -66,17 +66,17 @@ HuskSync is licensed under the Apache 2.0 license.
|
||||
Contributions to the project are welcome—feel free to open a pull request with new features, improvements and/or fixes!
|
||||
|
||||
### Support
|
||||
Due to its complexity, official support for HuskSync is provided through a paid model. This means that support is only available to users who have purchased a license to the plugin from Spigot, Polymart, or Craftaro and have provided proof of purchase. Please join our Discord server if you have done so and need help!
|
||||
Due to its complexity, official binaries and customer support for HuskSync is provided through a paid model. This means that support is only available to users who have purchased a license to the plugin from Spigot, Polymart, Craftaro, or BuiltByBit and have provided proof of purchase. Please join our Discord server if you have done so and need help!
|
||||
|
||||
### Translations
|
||||
Translations of the plugin locales are welcome to help make the plugin more accessible. Please submit a pull request with your translations as a `.yml` file.
|
||||
|
||||
- [Locales Directory](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/languages)
|
||||
- [English Locales](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/languages/en-gb.yml)
|
||||
- [Locales Directory](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/locales)
|
||||
- [English Locales](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/locales/en-gb.yml)
|
||||
|
||||
## Links
|
||||
- [Docs](https://william278.net/docs/husksync/) — Read the plugin documentation!
|
||||
- [Spigot](https://www.spigotmc.org/resources/husksync.97144/) — View the Spigot resource page (Also: [Polymart](https://polymart.org/resource/husksync.1634), [Craftaro](https://craftaro.com/marketplace/product/husksync.758))
|
||||
- [Spigot](https://www.spigotmc.org/resources/husksync.97144/) — View the Spigot resource page (Also: [Polymart](https://polymart.org/resource/husksync.1634), [Craftaro](https://craftaro.com/marketplace/product/husksync.758), [BuiltByBit](https://builtbybit.com/resources/husksync.34956/))
|
||||
- [Issues](https://github.com/WiIIiam278/HuskSync/issues) — File a bug report or feature request
|
||||
- [Discord](https://discord.gg/tVYhJfyDWG) — Get help, ask questions (Purchase required)
|
||||
- [bStats](https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140) — View plugin metrics
|
||||
|
||||
159
build.gradle
159
build.gradle
@@ -1,7 +1,9 @@
|
||||
import org.apache.tools.ant.filters.ReplaceTokens
|
||||
|
||||
plugins {
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||
id 'org.cadixdev.licenser' version '0.6.1' apply false
|
||||
id 'org.ajoberstar.grgit' version '5.2.0'
|
||||
id 'org.ajoberstar.grgit' version '5.2.2'
|
||||
id 'maven-publish'
|
||||
id 'java'
|
||||
}
|
||||
@@ -18,10 +20,41 @@ ext {
|
||||
set 'jedis_version', jedis_version.toString()
|
||||
set 'mysql_driver_version', mysql_driver_version.toString()
|
||||
set 'mariadb_driver_version', mariadb_driver_version.toString()
|
||||
set 'postgres_driver_version', postgres_driver_version.toString()
|
||||
set 'mongodb_driver_version', mongodb_driver_version.toString()
|
||||
set 'snappy_version', snappy_version.toString()
|
||||
}
|
||||
|
||||
import org.apache.tools.ant.filters.ReplaceTokens
|
||||
publishing {
|
||||
repositories {
|
||||
if (System.getenv("RELEASES_MAVEN_USERNAME") != null) {
|
||||
maven {
|
||||
name = "william278-releases"
|
||||
url = "https://repo.william278.net/releases"
|
||||
credentials {
|
||||
username = System.getenv("RELEASES_MAVEN_USERNAME")
|
||||
password = System.getenv("RELEASES_MAVEN_PASSWORD")
|
||||
}
|
||||
authentication {
|
||||
basic(BasicAuthentication)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (System.getenv("SNAPSHOTS_MAVEN_USERNAME") != null) {
|
||||
maven {
|
||||
name = "william278-snapshots"
|
||||
url = "https://repo.william278.net/snapshots"
|
||||
credentials {
|
||||
username = System.getenv("SNAPSHOTS_MAVEN_USERNAME")
|
||||
password = System.getenv("SNAPSHOTS_MAVEN_PASSWORD")
|
||||
}
|
||||
authentication {
|
||||
basic(BasicAuthentication)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
apply plugin: 'com.github.johnrengelman.shadow'
|
||||
@@ -29,15 +62,15 @@ allprojects {
|
||||
apply plugin: 'java'
|
||||
|
||||
compileJava.options.encoding = 'UTF-8'
|
||||
compileJava.options.release.set 17
|
||||
javadoc.options.encoding = 'UTF-8'
|
||||
javadoc.options.addStringOption('Xdoclint:none', '-quiet')
|
||||
|
||||
compileJava.options.release.set 16
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
|
||||
maven { url "https://repo.dmulloy2.net/repository/public/" }
|
||||
maven { url 'https://repo.codemc.io/repository/maven-public/' }
|
||||
maven { url 'https://repo.minebench.de/' }
|
||||
maven { url 'https://repo.alessiodp.com/releases/' }
|
||||
@@ -48,9 +81,9 @@ allprojects {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2'
|
||||
}
|
||||
|
||||
test {
|
||||
@@ -64,8 +97,10 @@ allprojects {
|
||||
}
|
||||
|
||||
processResources {
|
||||
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
|
||||
tokens: rootProject.ext.properties
|
||||
filesMatching(['**/*.json', '**/*.yml']) {
|
||||
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
|
||||
tokens: rootProject.ext.properties
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,87 +112,67 @@ subprojects {
|
||||
from '../LICENSE'
|
||||
}
|
||||
|
||||
if (['bukkit', 'plugin'].contains(project.name)) {
|
||||
shadowJar {
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
archiveClassifier.set('')
|
||||
shadowJar {
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
archiveClassifier.set('')
|
||||
}
|
||||
|
||||
// API publishing
|
||||
if (['common', 'bukkit'].contains(project.name)) {
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
sourcesJar {
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
}
|
||||
javadocJar {
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
}
|
||||
shadowJar.dependsOn(sourcesJar, javadocJar)
|
||||
|
||||
// API publishing
|
||||
if ('bukkit'.contains(project.name)) {
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
sourcesJar {
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
}
|
||||
javadocJar {
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
}
|
||||
shadowJar.dependsOn(sourcesJar, javadocJar)
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
if (System.getenv("RELEASES_MAVEN_USERNAME") != null) {
|
||||
maven {
|
||||
name = "william278-releases"
|
||||
url = "https://repo.william278.net/releases"
|
||||
credentials {
|
||||
username = System.getenv("RELEASES_MAVEN_USERNAME")
|
||||
password = System.getenv("RELEASES_MAVEN_PASSWORD")
|
||||
}
|
||||
authentication {
|
||||
basic(BasicAuthentication)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (System.getenv("SNAPSHOTS_MAVEN_USERNAME") != null) {
|
||||
maven {
|
||||
name = "william278-snapshots"
|
||||
url = "https://repo.william278.net/snapshots"
|
||||
credentials {
|
||||
username = System.getenv("SNAPSHOTS_MAVEN_USERNAME")
|
||||
password = System.getenv("SNAPSHOTS_MAVEN_PASSWORD")
|
||||
}
|
||||
authentication {
|
||||
basic(BasicAuthentication)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
if (['common'].contains(project.name)) {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
groupId = 'net.william278'
|
||||
artifactId = 'husksync'
|
||||
mavenJavaCommon(MavenPublication) {
|
||||
groupId = 'net.william278.husksync'
|
||||
artifactId = 'husksync-common'
|
||||
version = "$rootProject.version"
|
||||
artifact shadowJar
|
||||
artifact javadocJar
|
||||
artifact sourcesJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (['bukkit'].contains(project.name)) {
|
||||
publications {
|
||||
mavenJavaBukkit(MavenPublication) {
|
||||
groupId = 'net.william278.husksync'
|
||||
artifactId = 'husksync-bukkit'
|
||||
version = "$rootProject.version"
|
||||
artifact shadowJar
|
||||
artifact sourcesJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jar.dependsOn(shadowJar)
|
||||
clean.delete "$rootDir/target"
|
||||
}
|
||||
|
||||
jar.dependsOn(shadowJar)
|
||||
clean.delete "$rootDir/target"
|
||||
}
|
||||
|
||||
logger.lifecycle("Building HuskSync ${version} by William278")
|
||||
|
||||
@SuppressWarnings('GrMethodMayBeStatic')
|
||||
def versionMetadata() {
|
||||
// Get if there is a tag for this commit
|
||||
def tag = grgit.tag.list().find { it.commit.id == grgit.head().id }
|
||||
if (tag != null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// Otherwise, get the last commit hash and if it's a clean head
|
||||
// Get the last commit hash and if it's a clean head
|
||||
if (grgit == null) {
|
||||
return '-' + System.getenv("GITHUB_RUN_NUMBER") ? 'build.' + System.getenv("GITHUB_RUN_NUMBER") : 'unknown'
|
||||
}
|
||||
// If grgit DOES exist, get tag for this commit
|
||||
def tag = grgit.tag.list().find { it.commit.id == grgit.head().id }
|
||||
return '-' + grgit.head().abbreviatedId + (grgit.status().clean ? '' : '-indev')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,54 +7,54 @@ dependencies {
|
||||
implementation 'net.william278:mapdataapi:1.0.3'
|
||||
implementation 'net.william278:andjam:1.0.2'
|
||||
implementation 'me.lucko:commodore:2.2'
|
||||
implementation 'net.kyori:adventure-platform-bukkit:4.3.0'
|
||||
implementation 'dev.triumphteam:triumph-gui:3.1.5'
|
||||
implementation 'space.arim.morepaperlib:morepaperlib:0.4.3'
|
||||
implementation 'de.tr7zw:item-nbt-api:2.12.0'
|
||||
implementation 'net.kyori:adventure-platform-bukkit:4.3.2'
|
||||
implementation 'dev.triumphteam:triumph-gui:3.1.7'
|
||||
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
|
||||
implementation 'de.tr7zw:item-nbt-api:2.12.3'
|
||||
|
||||
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
|
||||
compileOnly 'commons-io:commons-io:2.13.0'
|
||||
compileOnly 'org.json:json:20230618'
|
||||
compileOnly 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
|
||||
compileOnly 'dev.dejvokep:boosted-yaml:1.3.1'
|
||||
compileOnly 'com.zaxxer:HikariCP:5.0.1'
|
||||
compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT'
|
||||
compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||
compileOnly 'commons-io:commons-io:2.16.0'
|
||||
compileOnly 'org.json:json:20240303'
|
||||
compileOnly 'net.william278:minedown:1.8.2'
|
||||
compileOnly 'de.exlll:configlib-yaml:4.5.0'
|
||||
compileOnly 'com.zaxxer:HikariCP:5.1.0'
|
||||
compileOnly 'net.william278:DesertWell:2.0.4'
|
||||
compileOnly 'net.william278:annotaml:2.0.7'
|
||||
compileOnly 'net.william278:AdvancementAPI:97a9583413'
|
||||
compileOnly "redis.clients:jedis:$jedis_version"
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
dependencies {
|
||||
exclude(dependency('com.mojang:brigadier'))
|
||||
}
|
||||
|
||||
relocate 'org.apache.commons.io', 'net.william278.husksync.libraries.commons.io'
|
||||
relocate 'org.apache.commons.text', 'net.william278.husksync.libraries.commons.text'
|
||||
relocate 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3'
|
||||
relocate 'com.google.gson', 'net.william278.husksync.libraries.gson'
|
||||
relocate 'org.json', 'net.william278.husksync.libraries.json'
|
||||
relocate 'com.fatboyindustrial', 'net.william278.husktowns.libraries'
|
||||
relocate 'com.fatboyindustrial', 'net.william278.husksync.libraries'
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'net.kyori', 'net.william278.husksync.libraries'
|
||||
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'dev.dejvokep', 'net.william278.husksync.libraries'
|
||||
relocate 'de.exlll', 'net.william278.husksync.libraries'
|
||||
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
|
||||
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
|
||||
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
|
||||
relocate 'net.william278.andjam', 'net.william278.husksync.libraries.andjam'
|
||||
relocate 'net.querz', 'net.william278.husksync.libraries.nbtparser'
|
||||
relocate 'net.roxeez', 'net.william278.husksync.libraries'
|
||||
|
||||
relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore'
|
||||
relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
|
||||
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
|
||||
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
|
||||
relocate 'net.william278.annotaml', 'net.william278.husksync.libraries.annotaml'
|
||||
relocate 'org.json', 'net.william278.husksync.libraries.json'
|
||||
relocate 'net.querz', 'net.william278.husksync.libraries.nbtparser'
|
||||
relocate 'net.roxeez', 'net.william278.husksync.libraries'
|
||||
relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
|
||||
relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib'
|
||||
relocate 'de.tr7zw.changeme.nbtapi', 'net.william278.husksync.libraries.nbtapi'
|
||||
|
||||
minimize()
|
||||
}
|
||||
@@ -19,7 +19,15 @@
|
||||
|
||||
package net.william278.husksync;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.gson.Gson;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.kyori.adventure.platform.AudienceProvider;
|
||||
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
@@ -28,13 +36,13 @@ import net.william278.husksync.adapter.SnappyGsonAdapter;
|
||||
import net.william278.husksync.api.BukkitHuskSyncAPI;
|
||||
import net.william278.husksync.command.BukkitCommand;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.config.Server;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.BukkitSerializer;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.Serializer;
|
||||
import net.william278.husksync.data.*;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.database.MongoDbDatabase;
|
||||
import net.william278.husksync.database.MySqlDatabase;
|
||||
import net.william278.husksync.database.PostgresDatabase;
|
||||
import net.william278.husksync.event.BukkitEventDispatcher;
|
||||
import net.william278.husksync.hook.PlanHook;
|
||||
import net.william278.husksync.listener.BukkitEventListener;
|
||||
@@ -43,32 +51,31 @@ import net.william278.husksync.migrator.LegacyMigrator;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.migrator.MpdbMigrator;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.sync.DataSyncer;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import net.william278.husksync.user.ConsoleUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.util.BukkitLegacyConverter;
|
||||
import net.william278.husksync.util.BukkitMapPersister;
|
||||
import net.william278.husksync.util.BukkitTask;
|
||||
import net.william278.husksync.util.LegacyConverter;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.map.MapView;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import space.arim.morepaperlib.MorePaperLib;
|
||||
import space.arim.morepaperlib.commands.CommandRegistration;
|
||||
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
|
||||
import space.arim.morepaperlib.scheduling.GracefulScheduling;
|
||||
import space.arim.morepaperlib.scheduling.RegionalScheduler;
|
||||
import space.arim.morepaperlib.scheduling.*;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier, BukkitEventDispatcher,
|
||||
BukkitMapPersister {
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier,
|
||||
BukkitEventDispatcher, BukkitMapPersister {
|
||||
|
||||
/**
|
||||
* Metrics ID for <a href="https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140">HuskSync on Bukkit</a>.
|
||||
@@ -76,40 +83,50 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
private static final int METRICS_ID = 13140;
|
||||
private static final String PLATFORM_TYPE_ID = "bukkit";
|
||||
|
||||
private final Map<Identifier, Serializer<? extends Data>> serializers = Maps.newLinkedHashMap();
|
||||
private final Map<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap();
|
||||
private final Map<Integer, MapView> mapViews = Maps.newConcurrentMap();
|
||||
private final List<Migrator> availableMigrators = Lists.newArrayList();
|
||||
private final Set<UUID> lockedPlayers = Sets.newConcurrentHashSet();
|
||||
|
||||
private boolean disabling;
|
||||
private Gson gson;
|
||||
private AudienceProvider audiences;
|
||||
private MorePaperLib paperLib;
|
||||
private Database database;
|
||||
private RedisManager redisManager;
|
||||
private EventListener eventListener;
|
||||
private DataAdapter dataAdapter;
|
||||
private Map<Identifier, Serializer<? extends Data>> serializers;
|
||||
private Map<UUID, Map<Identifier, Data>> playerCustomDataStore;
|
||||
private Settings settings;
|
||||
private Locales locales;
|
||||
private List<Migrator> availableMigrators;
|
||||
private DataSyncer dataSyncer;
|
||||
private LegacyConverter legacyConverter;
|
||||
private Map<Integer, MapView> mapViews;
|
||||
private BukkitAudiences audiences;
|
||||
private MorePaperLib paperLib;
|
||||
private AsynchronousScheduler asyncScheduler;
|
||||
private RegionalScheduler regionalScheduler;
|
||||
private Gson gson;
|
||||
@Setter
|
||||
private Settings settings;
|
||||
@Setter
|
||||
private Locales locales;
|
||||
@Setter
|
||||
@Getter(AccessLevel.NONE)
|
||||
private Server serverName;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// Initial plugin setup
|
||||
this.disabling = false;
|
||||
this.gson = createGson();
|
||||
this.audiences = BukkitAudiences.create(this);
|
||||
this.paperLib = new MorePaperLib(this);
|
||||
this.availableMigrators = new ArrayList<>();
|
||||
this.serializers = new LinkedHashMap<>();
|
||||
this.playerCustomDataStore = new ConcurrentHashMap<>();
|
||||
this.mapViews = new ConcurrentHashMap<>();
|
||||
|
||||
// Load settings and locales
|
||||
initialize("plugin config & locale files", (plugin) -> this.loadConfigs());
|
||||
initialize("plugin config & locale files", (plugin) -> {
|
||||
loadSettings();
|
||||
loadLocales();
|
||||
loadServer();
|
||||
});
|
||||
|
||||
// Prepare data adapter
|
||||
initialize("data adapter", (plugin) -> {
|
||||
if (settings.doCompressData()) {
|
||||
if (settings.getSynchronization().isCompressData()) {
|
||||
dataAdapter = new SnappyGsonAdapter(this);
|
||||
} else {
|
||||
dataAdapter = new GsonAdapter(this);
|
||||
@@ -121,13 +138,15 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this));
|
||||
registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this));
|
||||
registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(this));
|
||||
registerSerializer(Identifier.LOCATION, new BukkitSerializer.Location(this));
|
||||
registerSerializer(Identifier.HEALTH, new BukkitSerializer.Health(this));
|
||||
registerSerializer(Identifier.HUNGER, new BukkitSerializer.Hunger(this));
|
||||
registerSerializer(Identifier.GAME_MODE, new BukkitSerializer.GameMode(this));
|
||||
registerSerializer(Identifier.LOCATION, new BukkitSerializer.Json<>(this, BukkitData.Location.class));
|
||||
registerSerializer(Identifier.HEALTH, new BukkitSerializer.Json<>(this, BukkitData.Health.class));
|
||||
registerSerializer(Identifier.HUNGER, new BukkitSerializer.Json<>(this, BukkitData.Hunger.class));
|
||||
registerSerializer(Identifier.ATTRIBUTES, new BukkitSerializer.Json<>(this, BukkitData.Attributes.class));
|
||||
registerSerializer(Identifier.GAME_MODE, new BukkitSerializer.Json<>(this, BukkitData.GameMode.class));
|
||||
registerSerializer(Identifier.FLIGHT_STATUS, new BukkitSerializer.Json<>(this, BukkitData.FlightStatus.class));
|
||||
registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this));
|
||||
registerSerializer(Identifier.STATISTICS, new BukkitSerializer.Statistics(this));
|
||||
registerSerializer(Identifier.EXPERIENCE, new BukkitSerializer.Experience(this));
|
||||
registerSerializer(Identifier.STATISTICS, new BukkitSerializer.Json<>(this, BukkitData.Statistics.class));
|
||||
registerSerializer(Identifier.EXPERIENCE, new BukkitSerializer.Json<>(this, BukkitData.Experience.class));
|
||||
registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
|
||||
});
|
||||
|
||||
@@ -141,8 +160,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
});
|
||||
|
||||
// Initialize the database
|
||||
initialize(getSettings().getDatabaseType().getDisplayName() + " database connection", (plugin) -> {
|
||||
this.database = new MySqlDatabase(this);
|
||||
initialize(getSettings().getDatabase().getType().getDisplayName() + " database connection", (plugin) -> {
|
||||
this.database = switch (settings.getDatabase().getType()) {
|
||||
case MYSQL, MARIADB -> new MySqlDatabase(this);
|
||||
case POSTGRES -> new PostgresDatabase(this);
|
||||
case MONGO -> new MongoDbDatabase(this);
|
||||
};
|
||||
this.database.initialize();
|
||||
});
|
||||
|
||||
@@ -152,15 +175,21 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
this.redisManager.initialize();
|
||||
});
|
||||
|
||||
// Prepare data syncer
|
||||
initialize("data syncer", (plugin) -> {
|
||||
dataSyncer = getSettings().getSynchronization().getMode().create(this);
|
||||
dataSyncer.initialize();
|
||||
});
|
||||
|
||||
// Register events
|
||||
initialize("events", (plugin) -> this.eventListener = new BukkitEventListener(this));
|
||||
initialize("events", (plugin) -> this.eventListener = createEventListener());
|
||||
|
||||
// Register commands
|
||||
initialize("commands", (plugin) -> BukkitCommand.Type.registerCommands(this));
|
||||
|
||||
// Register plugin hooks
|
||||
initialize("hooks", (plugin) -> {
|
||||
if (isDependencyLoaded("Plan") && getSettings().usePlanHook()) {
|
||||
if (isDependencyLoaded("Plan") && getSettings().isEnablePlanHook()) {
|
||||
new PlanHook(this).hookIntoPlan();
|
||||
}
|
||||
});
|
||||
@@ -176,6 +205,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
@Override
|
||||
public void onDisable() {
|
||||
// Handle shutdown
|
||||
this.disabling = true;
|
||||
|
||||
// Close the event listener / data syncer
|
||||
if (this.dataSyncer != null) {
|
||||
this.dataSyncer.terminate();
|
||||
}
|
||||
if (this.eventListener != null) {
|
||||
this.eventListener.handlePluginDisable();
|
||||
}
|
||||
@@ -188,10 +223,15 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected BukkitEventListener createEventListener() {
|
||||
return new BukkitEventListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Set<OnlineUser> getOnlineUsers() {
|
||||
return Bukkit.getOnlinePlayers().stream()
|
||||
return getServer().getOnlinePlayers().stream()
|
||||
.map(player -> BukkitUser.adapt(player, this))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
@@ -199,7 +239,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
@Override
|
||||
@NotNull
|
||||
public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
|
||||
final Player player = Bukkit.getPlayer(uuid);
|
||||
final Player player = getServer().getPlayer(uuid);
|
||||
if (player == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
@@ -207,71 +247,29 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Database getDatabase() {
|
||||
return database;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public RedisManager getRedisManager() {
|
||||
return redisManager;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public DataAdapter getDataAdapter() {
|
||||
return dataAdapter;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Map<Identifier, Serializer<? extends Data>> getSerializers() {
|
||||
return serializers;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<Migrator> getAvailableMigrators() {
|
||||
return availableMigrators;
|
||||
public void setDataSyncer(@NotNull DataSyncer dataSyncer) {
|
||||
log(Level.INFO, String.format("Switching data syncer to %s", dataSyncer.getClass().getSimpleName()));
|
||||
this.dataSyncer = dataSyncer;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
|
||||
if (playerCustomDataStore.containsKey(user.getUuid())) {
|
||||
return playerCustomDataStore.get(user.getUuid());
|
||||
}
|
||||
final Map<Identifier, Data> data = new HashMap<>();
|
||||
playerCustomDataStore.put(user.getUuid(), data);
|
||||
return data;
|
||||
return playerCustomDataStore.compute(
|
||||
user.getUuid(),
|
||||
(uuid, data) -> data == null ? Maps.newHashMap() : data
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Settings getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSettings(@NotNull Settings settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Locales getLocales() {
|
||||
return locales;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLocales(@NotNull Locales locales) {
|
||||
this.locales = locales;
|
||||
public String getServerName() {
|
||||
return serverName == null ? "server" : serverName.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDependencyLoaded(@NotNull String name) {
|
||||
return Bukkit.getPluginManager().getPlugin(name) != null;
|
||||
return getServer().getPluginManager().getPlugin(name) != null;
|
||||
}
|
||||
|
||||
// Register bStats metrics
|
||||
@@ -296,12 +294,6 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ConsoleUser getConsole() {
|
||||
return new ConsoleUser(audiences.console());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Version getPluginVersion() {
|
||||
@@ -311,7 +303,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
@NotNull
|
||||
@Override
|
||||
public Version getMinecraftVersion() {
|
||||
return Version.fromString(Bukkit.getBukkitVersion());
|
||||
return Version.fromString(getServer().getBukkitVersion());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -325,23 +317,6 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
return Optional.of(legacyConverter);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<UUID> getLockedPlayers() {
|
||||
return this.eventListener.getLockedPlayers();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Gson getGson() {
|
||||
return gson;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Map<Integer, MapView> getMapViews() {
|
||||
return mapViews;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public GracefulScheduling getScheduler() {
|
||||
return paperLib.scheduling();
|
||||
@@ -354,14 +329,14 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public RegionalScheduler getRegionalScheduler() {
|
||||
public RegionalScheduler getSyncScheduler() {
|
||||
return regionalScheduler == null
|
||||
? regionalScheduler = getScheduler().globalRegionalScheduler() : regionalScheduler;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public BukkitAudiences getAudiences() {
|
||||
return audiences;
|
||||
public AttachedScheduler getUserSyncScheduler(@NotNull UserDataHolder user) {
|
||||
return getScheduler().entitySpecificScheduler(((BukkitUser) user).getPlayer());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -371,7 +346,13 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public HuskSync getPlugin() {
|
||||
public Path getConfigDirectory() {
|
||||
return getDataFolder().toPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public BukkitHuskSync getPlugin() {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,9 +43,6 @@ import java.util.function.Consumer;
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitHuskSyncAPI extends HuskSyncAPI {
|
||||
|
||||
// Instance of the plugin
|
||||
private static BukkitHuskSyncAPI instance;
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Constructor, instantiating the API.
|
||||
*/
|
||||
@@ -55,7 +52,7 @@ public class BukkitHuskSyncAPI extends HuskSyncAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint to the HuskSync API - returns an instance of the API
|
||||
* Entrypoint to the HuskSync API on the bukkit platform - returns an instance of the API
|
||||
*
|
||||
* @return instance of the HuskSync API
|
||||
* @since 3.0
|
||||
@@ -65,7 +62,7 @@ public class BukkitHuskSyncAPI extends HuskSyncAPI {
|
||||
if (instance == null) {
|
||||
throw new NotRegisteredException();
|
||||
}
|
||||
return instance;
|
||||
return (BukkitHuskSyncAPI) instance;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,14 +76,6 @@ public class BukkitHuskSyncAPI extends HuskSyncAPI {
|
||||
instance = new BukkitHuskSyncAPI(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Unregister the API for this platform.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public static void unregister() {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link OnlineUser} instance for the given bukkit {@link Player}.
|
||||
*
|
||||
|
||||
@@ -98,7 +98,7 @@ public class BukkitCommand extends org.bukkit.command.Command {
|
||||
}
|
||||
|
||||
// Register commodore TAB completion
|
||||
if (CommodoreProvider.isSupported() && plugin.getSettings().doBrigadierTabCompletion()) {
|
||||
if (CommodoreProvider.isSupported() && plugin.getSettings().isBrigadierTabCompletion()) {
|
||||
BrigadierUtil.registerCommodore(plugin, this, command);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,8 @@ import com.google.gson.reflect.TypeToken;
|
||||
import de.tr7zw.changeme.nbtapi.NBT;
|
||||
import de.tr7zw.changeme.nbtapi.NBTContainer;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.api.HuskSyncAPI;
|
||||
@@ -34,14 +36,11 @@ import java.util.List;
|
||||
|
||||
import static net.william278.husksync.data.BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
|
||||
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class BukkitSerializer {
|
||||
|
||||
protected final HuskSync plugin;
|
||||
|
||||
private BukkitSerializer(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public BukkitSerializer(@NotNull HuskSyncAPI api) {
|
||||
this.plugin = api.getPlugin();
|
||||
@@ -149,46 +148,6 @@ public class BukkitSerializer {
|
||||
}
|
||||
}
|
||||
|
||||
public static class Location extends BukkitSerializer implements Serializer<BukkitData.Location> {
|
||||
|
||||
public Location(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.Location deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
return plugin.getDataAdapter().fromJson(serialized, BukkitData.Location.class);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.Location element) throws SerializationException {
|
||||
return plugin.getDataAdapter().toJson(element);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Statistics extends BukkitSerializer implements Serializer<BukkitData.Statistics> {
|
||||
|
||||
public Statistics(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.Statistics deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
return BukkitData.Statistics.from(plugin.getGson().fromJson(
|
||||
serialized,
|
||||
BukkitData.Statistics.StatisticsMap.class
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.Statistics element) throws SerializationException {
|
||||
return plugin.getGson().toJson(element.getStatisticsSet());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class PersistentData extends BukkitSerializer implements Serializer<BukkitData.PersistentData> {
|
||||
|
||||
public PersistentData(@NotNull HuskSync plugin) {
|
||||
@@ -208,43 +167,11 @@ public class BukkitSerializer {
|
||||
|
||||
}
|
||||
|
||||
public static class Health extends Json<BukkitData.Health> implements Serializer<BukkitData.Health> {
|
||||
|
||||
public Health(@NotNull HuskSync plugin) {
|
||||
super(plugin, BukkitData.Health.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Hunger extends Json<BukkitData.Hunger> implements Serializer<BukkitData.Hunger> {
|
||||
|
||||
public Hunger(@NotNull HuskSync plugin) {
|
||||
super(plugin, BukkitData.Hunger.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Experience extends Json<BukkitData.Experience> implements Serializer<BukkitData.Experience> {
|
||||
|
||||
public Experience(@NotNull HuskSync plugin) {
|
||||
super(plugin, BukkitData.Experience.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class GameMode extends Json<BukkitData.GameMode> implements Serializer<BukkitData.GameMode> {
|
||||
|
||||
public GameMode(@NotNull HuskSync plugin) {
|
||||
super(plugin, BukkitData.GameMode.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static abstract class Json<T extends Data & Adaptable> extends BukkitSerializer implements Serializer<T> {
|
||||
public static class Json<T extends Data & Adaptable> extends BukkitSerializer implements Serializer<T> {
|
||||
|
||||
private final Class<T> type;
|
||||
|
||||
protected Json(@NotNull HuskSync plugin, Class<T> type) {
|
||||
public Json(@NotNull HuskSync plugin, Class<T> type) {
|
||||
super(plugin);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
@@ -42,8 +41,10 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
case "statistics" -> getStatistics();
|
||||
case "health" -> getHealth();
|
||||
case "hunger" -> getHunger();
|
||||
case "attributes" -> getAttributes();
|
||||
case "experience" -> getExperience();
|
||||
case "game_mode" -> getGameMode();
|
||||
case "flight_status" -> getFlightStatus();
|
||||
case "persistent_data" -> getPersistentData();
|
||||
default -> throw new IllegalStateException(String.format("Unexpected data type: %s", id));
|
||||
};
|
||||
@@ -62,7 +63,8 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Items.Inventory> getInventory() {
|
||||
if ((isDead() && !getPlugin().getSettings().doSynchronizeDeadPlayersChangingServer())) {
|
||||
if ((isDead() && !getPlugin().getSettings().getSynchronization().getSaveOnDeath()
|
||||
.isSyncDeadPlayersChangingServer())) {
|
||||
return Optional.of(BukkitData.Items.Inventory.empty());
|
||||
}
|
||||
final PlayerInventory inventory = getBukkitPlayer().getInventory();
|
||||
@@ -116,6 +118,12 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
return Optional.of(BukkitData.Hunger.adapt(getBukkitPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Attributes> getAttributes() {
|
||||
return Optional.of(BukkitData.Attributes.adapt(getBukkitPlayer(), getPlugin()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Experience> getExperience() {
|
||||
@@ -128,6 +136,12 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
return Optional.of(BukkitData.GameMode.adapt(getBukkitPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.FlightStatus> getFlightStatus() {
|
||||
return Optional.of(BukkitData.FlightStatus.adapt(getBukkitPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.PersistentData> getPersistentData() {
|
||||
@@ -139,9 +153,6 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
@NotNull
|
||||
Player getBukkitPlayer();
|
||||
|
||||
@NotNull
|
||||
Map<Identifier, Data> getCustomDataStore();
|
||||
|
||||
@NotNull
|
||||
default BukkitMapPersister getMapPersister() {
|
||||
return (BukkitHuskSync) getPlugin();
|
||||
|
||||
@@ -24,53 +24,52 @@ import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Projectile;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.entity.EntityPickupItemEvent;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.entity.ProjectileLaunchEvent;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||
import org.bukkit.event.inventory.PrepareItemCraftEvent;
|
||||
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
||||
import org.bukkit.event.player.PlayerDropItemEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.event.server.MapInitializeEvent;
|
||||
import org.bukkit.event.world.WorldSaveEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
|
||||
BukkitDeathEventListener, Listener {
|
||||
protected final List<String> blacklistedCommands;
|
||||
|
||||
public BukkitEventListener(@NotNull BukkitHuskSync huskSync) {
|
||||
super(huskSync);
|
||||
this.blacklistedCommands = huskSync.getSettings().getBlacklistedCommandsWhileLocked();
|
||||
Bukkit.getServer().getPluginManager().registerEvents(this, huskSync);
|
||||
protected final LockedHandler lockedHandler;
|
||||
|
||||
public BukkitEventListener(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
this.lockedHandler = createLockedHandler(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private LockedHandler createLockedHandler(@NotNull BukkitHuskSync plugin) {
|
||||
if (getPlugin().isDependencyLoaded("ProtocolLib") && getPlugin().getSettings().isCancelPackets()) {
|
||||
return new BukkitLockedPacketListener(plugin);
|
||||
} else {
|
||||
return new BukkitLockedEventListener(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleEvent(@NotNull ListenerType type, @NotNull Priority priority) {
|
||||
return plugin.getSettings().getEventPriority(type).equals(priority);
|
||||
return plugin.getSettings().getSynchronization().getEventPriority(type).equals(priority);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePlayerQuit(@NotNull BukkitUser bukkitUser) {
|
||||
final Player player = bukkitUser.getPlayer();
|
||||
if (!bukkitUser.isLocked() && !player.getItemOnCursor().getType().isAir()) {
|
||||
player.getWorld().dropItem(player.getLocation(), player.getItemOnCursor());
|
||||
final ItemStack itemOnCursor = player.getItemOnCursor();
|
||||
if (!bukkitUser.isLocked() && !itemOnCursor.getType().isAir()) {
|
||||
player.setItemOnCursor(null);
|
||||
player.getWorld().dropItem(player.getLocation(), itemOnCursor);
|
||||
plugin.debug("Dropped " + itemOnCursor + " for " + player.getName() + " on quit");
|
||||
}
|
||||
super.handlePlayerQuit(bukkitUser);
|
||||
}
|
||||
@@ -85,13 +84,13 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
|
||||
final OnlineUser user = BukkitUser.adapt(event.getEntity(), plugin);
|
||||
|
||||
// If the player is locked or the plugin disabling, clear their drops
|
||||
if (cancelPlayerEvent(user.getUuid())) {
|
||||
if (lockedHandler.cancelPlayerEvent(user.getUuid())) {
|
||||
event.getDrops().clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle saving player data snapshots on death
|
||||
if (!plugin.getSettings().doSaveOnDeath()) {
|
||||
if (!plugin.getSettings().getSynchronization().getSaveOnDeath().isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,7 +104,7 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onWorldSave(@NotNull WorldSaveEvent event) {
|
||||
if (!plugin.getSettings().doSaveOnWorldSave()) {
|
||||
if (!plugin.getSettings().getSynchronization().isSaveOnWorldSave()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -115,81 +114,21 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
|
||||
.collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Events to cancel if the player has not been set yet
|
||||
*/
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onProjectileLaunch(@NotNull ProjectileLaunchEvent event) {
|
||||
final Projectile projectile = event.getEntity();
|
||||
if (projectile.getShooter() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onDropItem(@NotNull PlayerDropItemEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPickupItem(@NotNull EntityPickupItemEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteract(@NotNull PlayerInteractEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteractEntity(@NotNull PlayerInteractEntityEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockPlace(@NotNull BlockPlaceEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockBreak(@NotNull BlockBreakEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
|
||||
if (event.getPlayer() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryClick(@NotNull InventoryClickEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getWhoClicked().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onCraftItem(@NotNull PrepareItemCraftEvent event) {
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onMapInitialize(@NotNull MapInitializeEvent event) {
|
||||
if (plugin.getSettings().getSynchronization().isPersistLockedMaps() && event.getMap().isLocked()) {
|
||||
getPlugin().runAsync(() -> ((BukkitHuskSync) plugin).renderMapFromFile(event.getMap()));
|
||||
}
|
||||
}
|
||||
|
||||
// We handle commands here to allow specific command handling on ProtocolLib servers
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onPermissionCommand(@NotNull PlayerCommandPreprocessEvent event) {
|
||||
final String[] commandArgs = event.getMessage().substring(1).split(" ");
|
||||
final String commandLabel = commandArgs[0].toLowerCase(Locale.ENGLISH);
|
||||
|
||||
if (blacklistedCommands.contains("*") || blacklistedCommands.contains(commandLabel)) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
public void onCommandProcessed(@NotNull PlayerCommandPreprocessEvent event) {
|
||||
if (!lockedHandler.isCommandDisabled(event.getMessage().substring(1).split(" ")[0])) {
|
||||
return;
|
||||
}
|
||||
if (lockedHandler.cancelPlayerEvent(event.getPlayer().getUniqueId())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Projectile;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.entity.EntityPickupItemEvent;
|
||||
import org.bukkit.event.entity.ProjectileLaunchEvent;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||
import org.bukkit.event.player.PlayerArmorStandManipulateEvent;
|
||||
import org.bukkit.event.player.PlayerDropItemEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
public class BukkitLockedEventListener implements LockedHandler, Listener {
|
||||
|
||||
protected final BukkitHuskSync plugin;
|
||||
|
||||
protected BukkitLockedEventListener(@NotNull BukkitHuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onProjectileLaunch(@NotNull ProjectileLaunchEvent event) {
|
||||
final Projectile projectile = event.getEntity();
|
||||
if (projectile.getShooter() instanceof Player player) {
|
||||
cancelPlayerEvent(player.getUniqueId(), event);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onDropItem(@NotNull PlayerDropItemEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPickupItem(@NotNull EntityPickupItemEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
cancelPlayerEvent(player.getUniqueId(), event);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteract(@NotNull PlayerInteractEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteractEntity(@NotNull PlayerInteractEntityEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteractArmorStand(@NotNull PlayerArmorStandManipulateEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockPlace(@NotNull BlockPlaceEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockBreak(@NotNull BlockBreakEvent event) {
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
|
||||
if (event.getPlayer() instanceof Player player) {
|
||||
cancelPlayerEvent(player.getUniqueId(), event);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryClick(@NotNull InventoryClickEvent event) {
|
||||
cancelPlayerEvent(event.getWhoClicked().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
cancelPlayerEvent(player.getUniqueId(), event);
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelPlayerEvent(@NotNull UUID uuid, @NotNull Cancellable event) {
|
||||
if (cancelPlayerEvent(uuid)) {
|
||||
event.setCancelled(true);
|
||||
plugin.debug("Cancelled event " + event.getClass().getSimpleName() + " from " + Objects.requireNonNull(plugin.getServer().getPlayer(uuid)).getName());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import com.comphenix.protocol.PacketType;
|
||||
import com.comphenix.protocol.ProtocolLibrary;
|
||||
import com.comphenix.protocol.events.ListenerPriority;
|
||||
import com.comphenix.protocol.events.PacketAdapter;
|
||||
import com.comphenix.protocol.events.PacketEvent;
|
||||
import com.google.common.collect.Sets;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.comphenix.protocol.PacketType.Play.Client;
|
||||
|
||||
public class BukkitLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
|
||||
|
||||
protected BukkitLockedPacketListener(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
ProtocolLibrary.getProtocolManager().addPacketListener(new PlayerPacketAdapter(this));
|
||||
plugin.log(Level.INFO, "Using ProtocolLib to cancel packets for locked players");
|
||||
}
|
||||
|
||||
private static class PlayerPacketAdapter extends PacketAdapter {
|
||||
|
||||
// Packets we want the player to still be able to send/receiver to/from the server
|
||||
private static final Set<PacketType> ALLOWED_PACKETS = Set.of(
|
||||
Client.KEEP_ALIVE, Client.PONG, Client.CUSTOM_PAYLOAD, // Connection packets
|
||||
Client.CHAT_COMMAND, Client.CLIENT_COMMAND, Client.CHAT, Client.CHAT_SESSION_UPDATE, // Chat / command packets
|
||||
Client.POSITION, Client.POSITION_LOOK, Client.LOOK, // Movement packets
|
||||
Client.HELD_ITEM_SLOT, Client.ARM_ANIMATION, Client.TELEPORT_ACCEPT // Animation packets
|
||||
);
|
||||
|
||||
private final BukkitLockedPacketListener listener;
|
||||
|
||||
public PlayerPacketAdapter(@NotNull BukkitLockedPacketListener listener) {
|
||||
super(listener.getPlugin(), ListenerPriority.HIGHEST, getPacketsToListenFor());
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketReceiving(@NotNull PacketEvent event) {
|
||||
if (listener.cancelPlayerEvent(event.getPlayer().getUniqueId()) && !event.isReadOnly()) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketSending(@NotNull PacketEvent event) {
|
||||
if (listener.cancelPlayerEvent(event.getPlayer().getUniqueId()) && !event.isReadOnly()) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the set of ALL Server packets, excluding the set of allowed packets
|
||||
@NotNull
|
||||
private static Set<PacketType> getPacketsToListenFor() {
|
||||
return Sets.difference(
|
||||
Client.getInstance().values().stream().filter(PacketType::isSupported).collect(Collectors.toSet()),
|
||||
ALLOWED_PACKETS
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
package net.william278.husksync.migrator;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import me.william278.husksync.bukkit.data.DataSerializer;
|
||||
import net.william278.hslmigrator.HSLConverter;
|
||||
@@ -42,6 +44,8 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static net.william278.husksync.config.Settings.DatabaseSettings;
|
||||
|
||||
public class LegacyMigrator extends Migrator {
|
||||
|
||||
private final HSLConverter hslConverter;
|
||||
@@ -56,11 +60,13 @@ public class LegacyMigrator extends Migrator {
|
||||
public LegacyMigrator(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
this.hslConverter = HSLConverter.getInstance();
|
||||
this.sourceHost = plugin.getSettings().getMySqlHost();
|
||||
this.sourcePort = plugin.getSettings().getMySqlPort();
|
||||
this.sourceUsername = plugin.getSettings().getMySqlUsername();
|
||||
this.sourcePassword = plugin.getSettings().getMySqlPassword();
|
||||
this.sourceDatabase = plugin.getSettings().getMySqlDatabase();
|
||||
|
||||
final DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
|
||||
this.sourceHost = credentials.getHost();
|
||||
this.sourcePort = credentials.getPort();
|
||||
this.sourceUsername = credentials.getUsername();
|
||||
this.sourcePassword = credentials.getPassword();
|
||||
this.sourceDatabase = credentials.getDatabase();
|
||||
this.sourcePlayersTable = "husksync_players";
|
||||
this.sourceDataTable = "husksync_data";
|
||||
}
|
||||
@@ -87,7 +93,7 @@ public class LegacyMigrator extends Migrator {
|
||||
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase(Locale.ENGLISH));
|
||||
|
||||
plugin.log(Level.INFO, "Downloading raw data from the legacy database (this might take a while)...");
|
||||
final List<LegacyData> dataToMigrate = new ArrayList<>();
|
||||
final List<LegacyData> dataToMigrate = Lists.newArrayList();
|
||||
try (final Connection connection = connectionPool.getConnection()) {
|
||||
try (final PreparedStatement statement = connection.prepareStatement("""
|
||||
SELECT `uuid`, `username`, `inventory`, `ender_chest`, `health`, `max_health`, `health_scale`, `hunger`, `saturation`, `saturation_exhaustion`, `selected_slot`, `status_effects`, `total_experience`, `exp_level`, `exp_progress`, `game_mode`, `statistics`, `is_flying`, `advancements`, `location`
|
||||
@@ -217,16 +223,16 @@ public class LegacyMigrator extends Migrator {
|
||||
@NotNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "HuskSync v1.x --> v2.x Migrator";
|
||||
return "HuskSync v1.x --> v3.x Migrator";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getHelpMenu() {
|
||||
return """
|
||||
=== HuskSync v1.x --> v2.x Migration Wizard =========
|
||||
=== HuskSync v1.x --> v3.x Migration Wizard =========
|
||||
This will migrate all user data from HuskSync v1.x to
|
||||
HuskSync v2.x's new format. To perform the migration,
|
||||
HuskSync v3.x's new format. To perform the migration,
|
||||
please follow the steps below carefully.
|
||||
|
||||
[!] Existing data in the database will be wiped. [!]
|
||||
@@ -317,18 +323,18 @@ public class LegacyMigrator extends Migrator {
|
||||
|
||||
// Stats
|
||||
.statistics(BukkitData.Statistics.from(
|
||||
BukkitData.Statistics.createStatisticsMap(
|
||||
convertStatisticMap(stats.untypedStatisticValues()),
|
||||
convertMaterialStatisticMap(stats.blockStatisticValues()),
|
||||
convertMaterialStatisticMap(stats.itemStatisticValues()),
|
||||
convertEntityStatisticMap(stats.entityStatisticValues())
|
||||
)))
|
||||
convertStatisticMap(stats.untypedStatisticValues()),
|
||||
convertMaterialStatisticMap(stats.blockStatisticValues()),
|
||||
convertMaterialStatisticMap(stats.itemStatisticValues()),
|
||||
convertEntityStatisticMap(stats.entityStatisticValues())
|
||||
))
|
||||
|
||||
// Health, hunger, experience & game mode
|
||||
.health(BukkitData.Health.from(health, maxHealth, healthScale))
|
||||
.health(BukkitData.Health.from(health, healthScale))
|
||||
.hunger(BukkitData.Hunger.from(hunger, saturation, saturationExhaustion))
|
||||
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
|
||||
.gameMode(BukkitData.GameMode.from(gameMode, isFlying, isFlying))
|
||||
.gameMode(BukkitData.GameMode.from(gameMode))
|
||||
.flightStatus(BukkitData.FlightStatus.from(isFlying, isFlying))
|
||||
|
||||
// Build & pack into new format
|
||||
.saveCause(DataSnapshot.SaveCause.LEGACY_MIGRATION).buildAndPack();
|
||||
@@ -338,7 +344,7 @@ public class LegacyMigrator extends Migrator {
|
||||
}
|
||||
|
||||
private Map<String, Integer> convertStatisticMap(@NotNull HashMap<Statistic, Integer> rawMap) {
|
||||
final HashMap<String, Integer> convertedMap = new HashMap<>();
|
||||
final HashMap<String, Integer> convertedMap = Maps.newHashMap();
|
||||
for (Map.Entry<Statistic, Integer> entry : rawMap.entrySet()) {
|
||||
convertedMap.put(entry.getKey().getKey().toString(), entry.getValue());
|
||||
}
|
||||
@@ -346,7 +352,7 @@ public class LegacyMigrator extends Migrator {
|
||||
}
|
||||
|
||||
private Map<String, Map<String, Integer>> convertMaterialStatisticMap(@NotNull HashMap<Statistic, HashMap<Material, Integer>> rawMap) {
|
||||
final Map<String, Map<String, Integer>> convertedMap = new HashMap<>();
|
||||
final Map<String, Map<String, Integer>> convertedMap = Maps.newHashMap();
|
||||
for (Map.Entry<Statistic, HashMap<Material, Integer>> entry : rawMap.entrySet()) {
|
||||
for (Map.Entry<Material, Integer> materialEntry : entry.getValue().entrySet()) {
|
||||
convertedMap.computeIfAbsent(entry.getKey().getKey().toString(), k -> new HashMap<>())
|
||||
@@ -357,7 +363,7 @@ public class LegacyMigrator extends Migrator {
|
||||
}
|
||||
|
||||
private Map<String, Map<String, Integer>> convertEntityStatisticMap(@NotNull HashMap<Statistic, HashMap<EntityType, Integer>> rawMap) {
|
||||
final Map<String, Map<String, Integer>> convertedMap = new HashMap<>();
|
||||
final Map<String, Map<String, Integer>> convertedMap = Maps.newHashMap();
|
||||
for (Map.Entry<Statistic, HashMap<EntityType, Integer>> entry : rawMap.entrySet()) {
|
||||
for (Map.Entry<EntityType, Integer> materialEntry : entry.getValue().entrySet()) {
|
||||
convertedMap.computeIfAbsent(entry.getKey().getKey().toString(), k -> new HashMap<>())
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
package net.william278.husksync.migrator;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
@@ -35,12 +36,17 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static net.william278.husksync.config.Settings.DatabaseSettings;
|
||||
|
||||
/**
|
||||
* A migrator for migrating MySQLPlayerDataBridge data to HuskSync {@link DataSnapshot}s
|
||||
*/
|
||||
@@ -62,11 +68,12 @@ public class MpdbMigrator extends Migrator {
|
||||
Bukkit.getPluginManager().getPlugin("MySQLPlayerDataBridge"),
|
||||
"MySQLPlayerDataBridge dependency not found!"
|
||||
));
|
||||
this.sourceHost = plugin.getSettings().getMySqlHost();
|
||||
this.sourcePort = plugin.getSettings().getMySqlPort();
|
||||
this.sourceUsername = plugin.getSettings().getMySqlUsername();
|
||||
this.sourcePassword = plugin.getSettings().getMySqlPassword();
|
||||
this.sourceDatabase = plugin.getSettings().getMySqlDatabase();
|
||||
final DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
|
||||
this.sourceHost = credentials.getHost();
|
||||
this.sourcePort = credentials.getPort();
|
||||
this.sourceUsername = credentials.getUsername();
|
||||
this.sourcePassword = credentials.getPassword();
|
||||
this.sourceDatabase = credentials.getDatabase();
|
||||
this.sourceInventoryTable = "mpdb_inventory";
|
||||
this.sourceEnderChestTable = "mpdb_enderchest";
|
||||
this.sourceExperienceTable = "mpdb_experience";
|
||||
@@ -95,7 +102,7 @@ public class MpdbMigrator extends Migrator {
|
||||
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase(Locale.ENGLISH));
|
||||
|
||||
plugin.log(Level.INFO, "Downloading raw data from the MySQLPlayerDataBridge database (this might take a while)...");
|
||||
final List<MpdbData> dataToMigrate = new ArrayList<>();
|
||||
final List<MpdbData> dataToMigrate = Lists.newArrayList();
|
||||
try (final Connection connection = connectionPool.getConnection()) {
|
||||
try (final PreparedStatement statement = connection.prepareStatement("""
|
||||
SELECT `%source_inventory_table%`.`player_uuid`, `%source_inventory_table%`.`player_name`, `inventory`, `armor`, `enderchest`, `exp_lvl`, `exp`, `total_exp`
|
||||
@@ -143,7 +150,7 @@ public class MpdbMigrator extends Migrator {
|
||||
});
|
||||
plugin.log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "Error while migrating data: " + e.getMessage() + " - are your source database credentials correct?");
|
||||
return false;
|
||||
}
|
||||
@@ -221,6 +228,9 @@ public class MpdbMigrator extends Migrator {
|
||||
public String getHelpMenu() {
|
||||
return """
|
||||
=== MySQLPlayerDataBridge Migration Wizard ==========
|
||||
NOTE: This migrator currently WORKS WITH MPDB version
|
||||
v4.9.2 and below!
|
||||
|
||||
This will migrate inventories, ender chests and XP
|
||||
from the MySQLPlayerDataBridge plugin to HuskSync.
|
||||
|
||||
@@ -254,6 +264,9 @@ public class MpdbMigrator extends Migrator {
|
||||
|
||||
STEP 4] To start the migration, please run:
|
||||
"husksync migrate mpdb start"
|
||||
|
||||
NOTE: This migrator currently WORKS WITH MPDB version
|
||||
v4.9.2 and below!
|
||||
""".replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost))
|
||||
.replaceAll(Pattern.quote("%source_port%"), Integer.toString(sourcePort))
|
||||
.replaceAll(Pattern.quote("%source_username%"), obfuscateDataString(sourceUsername))
|
||||
@@ -307,7 +320,7 @@ public class MpdbMigrator extends Migrator {
|
||||
.inventory(BukkitData.Items.Inventory.from(inventory.getContents(), 0))
|
||||
.enderChest(BukkitData.Items.EnderChest.adapt(enderChest))
|
||||
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
|
||||
.gameMode(BukkitData.GameMode.from("SURVIVAL", false, false))
|
||||
.gameMode(BukkitData.GameMode.from("SURVIVAL"))
|
||||
.saveCause(DataSnapshot.SaveCause.MPDB_MIGRATION)
|
||||
.buildAndPack();
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import de.themoep.minedown.adventure.MineDown;
|
||||
import dev.triumphteam.gui.builder.gui.StorageBuilder;
|
||||
import dev.triumphteam.gui.guis.Gui;
|
||||
import dev.triumphteam.gui.guis.StorageGui;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.roxeez.advancement.display.FrameType;
|
||||
import net.william278.andjam.Toast;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
@@ -41,6 +40,8 @@ import java.util.Arrays;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static net.william278.husksync.util.BukkitKeyedAdapter.matchMaterial;
|
||||
|
||||
/**
|
||||
* Bukkit platform implementation of an {@link OnlineUser}
|
||||
*/
|
||||
@@ -77,17 +78,11 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
||||
return player == null || !player.isOnline();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Audience getAudience() {
|
||||
return ((BukkitHuskSync) plugin).getAudiences().player(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
|
||||
@NotNull String iconMaterial, @NotNull String backgroundType) {
|
||||
try {
|
||||
final Material material = Material.matchMaterial(iconMaterial);
|
||||
final Material material = matchMaterial(iconMaterial);
|
||||
Toast.builder((BukkitHuskSync) plugin)
|
||||
.setTitle(title.toComponent())
|
||||
.setDescription(description.toComponent())
|
||||
@@ -114,7 +109,7 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
||||
gui.setCloseGuiAction((close) -> onClose.accept(BukkitData.Items.ItemArray.adapt(
|
||||
Arrays.stream(close.getInventory().getContents()).limit(size).toArray(ItemStack[]::new)
|
||||
)));
|
||||
plugin.runSync(() -> gui.open(player));
|
||||
plugin.runSync(() -> gui.open(player), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
// Utility class for adapting "Keyed" Bukkit objects
|
||||
public final class BukkitKeyedAdapter {
|
||||
|
||||
@Nullable
|
||||
public static Statistic matchStatistic(@NotNull String key) {
|
||||
return Registry.STATISTIC.get(Objects.requireNonNull(NamespacedKey.fromString(key), "Null key"));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static EntityType matchEntityType(@NotNull String key) {
|
||||
return Registry.ENTITY_TYPE.get(Objects.requireNonNull(NamespacedKey.fromString(key), "Null key"));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Material matchMaterial(@NotNull String key) {
|
||||
return Registry.MATERIAL.get(Objects.requireNonNull(NamespacedKey.fromString(key), "Null key"));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Attribute matchAttribute(@NotNull String key) {
|
||||
return Registry.ATTRIBUTE.get(Objects.requireNonNull(NamespacedKey.fromString(key), "Null key"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,15 +19,14 @@
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -43,15 +42,18 @@ import java.time.OffsetDateTime;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static net.william278.husksync.util.BukkitKeyedAdapter.matchEntityType;
|
||||
import static net.william278.husksync.util.BukkitKeyedAdapter.matchMaterial;
|
||||
|
||||
public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
public BukkitLegacyConverter(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public DataSnapshot.Packed convert(@NotNull byte[] data, @NotNull UUID id,
|
||||
@NotNull
|
||||
public DataSnapshot.Packed convert(byte @NotNull [] data, @NotNull UUID id,
|
||||
@NotNull OffsetDateTime timestamp) throws DataAdapter.AdaptionException {
|
||||
final JSONObject object = new JSONObject(plugin.getDataAdapter().bytesToString(data));
|
||||
final int version = object.getInt("format_version");
|
||||
@@ -79,11 +81,10 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
}
|
||||
|
||||
final JSONObject status = object.getJSONObject("status_data");
|
||||
final HashMap<Identifier, Data> containers = new HashMap<>();
|
||||
final HashMap<Identifier, Data> containers = Maps.newHashMap();
|
||||
if (shouldImport(Identifier.HEALTH)) {
|
||||
containers.put(Identifier.HEALTH, BukkitData.Health.from(
|
||||
status.getDouble("health"),
|
||||
status.getDouble("max_health"),
|
||||
status.getDouble("health_scale")
|
||||
));
|
||||
}
|
||||
@@ -103,7 +104,11 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
}
|
||||
if (shouldImport(Identifier.GAME_MODE)) {
|
||||
containers.put(Identifier.GAME_MODE, BukkitData.GameMode.from(
|
||||
status.getString("game_mode"),
|
||||
status.getString("game_mode")
|
||||
));
|
||||
}
|
||||
if (shouldImport(Identifier.FLIGHT_STATUS)) {
|
||||
containers.put(Identifier.FLIGHT_STATUS, BukkitData.FlightStatus.from(
|
||||
status.getBoolean("is_flying"),
|
||||
status.getBoolean("is_flying")
|
||||
));
|
||||
@@ -163,7 +168,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
}
|
||||
|
||||
final JSONArray advancements = object.getJSONArray("advancements");
|
||||
final List<Data.Advancements.Advancement> converted = new ArrayList<>();
|
||||
final List<Data.Advancements.Advancement> converted = Lists.newArrayList();
|
||||
advancements.iterator().forEachRemaining(o -> {
|
||||
final JSONObject advancement = (JSONObject) JSONObject.wrap(o);
|
||||
final String key = advancement.getString("key");
|
||||
@@ -197,36 +202,47 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
@NotNull
|
||||
private BukkitData.Statistics readStatisticMaps(@NotNull JSONObject untyped, @NotNull JSONObject blocks,
|
||||
@NotNull JSONObject items, @NotNull JSONObject entities) {
|
||||
final Map<Statistic, Integer> genericStats = new HashMap<>();
|
||||
untyped.keys().forEachRemaining(stat -> genericStats.put(Statistic.valueOf(stat), untyped.getInt(stat)));
|
||||
// Read generic stats
|
||||
final Map<String, Integer> genericStats = Maps.newHashMap();
|
||||
untyped.keys().forEachRemaining(stat -> genericStats.put(stat, untyped.getInt(stat)));
|
||||
|
||||
final Map<Statistic, Map<Material, Integer>> blockStats = new HashMap<>();
|
||||
blocks.keys().forEachRemaining(stat -> {
|
||||
final JSONObject blockStat = blocks.getJSONObject(stat);
|
||||
final Map<Material, Integer> blockMap = new HashMap<>();
|
||||
blockStat.keys().forEachRemaining(block -> blockMap.put(Material.valueOf(block), blockStat.getInt(block)));
|
||||
blockStats.put(Statistic.valueOf(stat), blockMap);
|
||||
});
|
||||
// Read block & item stats
|
||||
final Map<String, Map<String, Integer>> blockStats, itemStats, entityStats;
|
||||
blockStats = readMaterialStatistics(blocks);
|
||||
itemStats = readMaterialStatistics(items);
|
||||
|
||||
final Map<Statistic, Map<Material, Integer>> itemStats = new HashMap<>();
|
||||
items.keys().forEachRemaining(stat -> {
|
||||
final JSONObject itemStat = items.getJSONObject(stat);
|
||||
final Map<Material, Integer> itemMap = new HashMap<>();
|
||||
itemStat.keys().forEachRemaining(item -> itemMap.put(Material.valueOf(item), itemStat.getInt(item)));
|
||||
itemStats.put(Statistic.valueOf(stat), itemMap);
|
||||
});
|
||||
|
||||
final Map<Statistic, Map<EntityType, Integer>> entityStats = new HashMap<>();
|
||||
// Read entity stats
|
||||
entityStats = Maps.newHashMap();
|
||||
entities.keys().forEachRemaining(stat -> {
|
||||
final JSONObject entityStat = entities.getJSONObject(stat);
|
||||
final Map<EntityType, Integer> entityMap = new HashMap<>();
|
||||
entityStat.keys().forEachRemaining(entity -> entityMap.put(EntityType.valueOf(entity), entityStat.getInt(entity)));
|
||||
entityStats.put(Statistic.valueOf(stat), entityMap);
|
||||
final Map<String, Integer> entityMap = Maps.newHashMap();
|
||||
entityStat.keys().forEachRemaining(entity -> {
|
||||
if (matchEntityType(entity) != null) {
|
||||
entityMap.put(entity, entityStat.getInt(entity));
|
||||
}
|
||||
});
|
||||
entityStats.put(stat, entityMap);
|
||||
});
|
||||
|
||||
return BukkitData.Statistics.from(genericStats, blockStats, itemStats, entityStats);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map<String, Map<String, Integer>> readMaterialStatistics(@NotNull JSONObject items) {
|
||||
final Map<String, Map<String, Integer>> itemStats = Maps.newHashMap();
|
||||
items.keys().forEachRemaining(stat -> {
|
||||
final JSONObject itemStat = items.getJSONObject(stat);
|
||||
final Map<String, Integer> itemMap = Maps.newHashMap();
|
||||
itemStat.keys().forEachRemaining(item -> {
|
||||
if (matchMaterial(item) != null) {
|
||||
itemMap.put(item, itemStat.getInt(item));
|
||||
}
|
||||
});
|
||||
itemStats.put(stat, itemMap);
|
||||
});
|
||||
return itemStats;
|
||||
}
|
||||
|
||||
// Deserialize a legacy item stack array
|
||||
@NotNull
|
||||
public ItemStack[] deserializeLegacyItemStacks(@NotNull String items) {
|
||||
@@ -258,6 +274,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
}
|
||||
|
||||
// Deserialize a single legacy item stack
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
private static ItemStack deserializeLegacyItemStack(@Nullable Object serializedItemStack) {
|
||||
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
|
||||
@@ -265,7 +282,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
|
||||
private boolean shouldImport(@NotNull Identifier type) {
|
||||
return plugin.getSettings().isSyncFeatureEnabled(type);
|
||||
return plugin.getSettings().getSynchronization().isFeatureEnabled(type);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -19,10 +19,13 @@
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import de.tr7zw.changeme.nbtapi.NBT;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadableNBT;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.querz.nbt.io.NBTUtil;
|
||||
import net.querz.nbt.tag.CompoundTag;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.mapdataapi.MapBanner;
|
||||
import net.william278.mapdataapi.MapData;
|
||||
import org.bukkit.Bukkit;
|
||||
@@ -34,8 +37,10 @@ import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.map.*;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
@@ -59,7 +64,7 @@ public interface BukkitMapPersister {
|
||||
*/
|
||||
@NotNull
|
||||
default ItemStack[] persistLockedMaps(@NotNull ItemStack[] items, @NotNull Player delegateRenderer) {
|
||||
if (!getPlugin().getSettings().doPersistLockedMaps()) {
|
||||
if (!getPlugin().getSettings().getSynchronization().isPersistLockedMaps()) {
|
||||
return items;
|
||||
}
|
||||
return forEachMap(items, map -> this.persistMapView(map, delegateRenderer));
|
||||
@@ -71,9 +76,9 @@ public interface BukkitMapPersister {
|
||||
* @param items the array of {@link ItemStack}s to apply persisted locked maps to
|
||||
* @return the array of {@link ItemStack}s with persisted locked maps applied
|
||||
*/
|
||||
@NotNull
|
||||
default ItemStack[] setMapViews(@NotNull ItemStack[] items) {
|
||||
if (!getPlugin().getSettings().doPersistLockedMaps()) {
|
||||
@Nullable
|
||||
default ItemStack @NotNull [] setMapViews(@Nullable ItemStack @NotNull [] items) {
|
||||
if (!getPlugin().getSettings().getSynchronization().isPersistLockedMaps()) {
|
||||
return items;
|
||||
}
|
||||
return forEachMap(items, this::applyMapView);
|
||||
@@ -81,7 +86,7 @@ public interface BukkitMapPersister {
|
||||
|
||||
// Perform an operation on each map in an array of ItemStacks
|
||||
@NotNull
|
||||
private ItemStack[] forEachMap(@NotNull ItemStack[] items, @NotNull Function<ItemStack, ItemStack> function) {
|
||||
private ItemStack[] forEachMap(ItemStack[] items, @NotNull Function<ItemStack, ItemStack> function) {
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
final ItemStack item = items[i];
|
||||
if (item == null) {
|
||||
@@ -133,15 +138,18 @@ public interface BukkitMapPersister {
|
||||
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
|
||||
NBT.get(map, nbt -> {
|
||||
if (!nbt.hasTag(MAP_DATA_KEY)) {
|
||||
return nbt;
|
||||
return;
|
||||
}
|
||||
final ReadableNBT mapData = nbt.getCompound(MAP_DATA_KEY);
|
||||
final ReadableNBT mapIds = nbt.getCompound(MAP_VIEW_ID_MAPPINGS_KEY);
|
||||
if (mapData == null || mapIds == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Search for an existing map view
|
||||
final ReadableNBT mapIds = nbt.getCompound(MAP_VIEW_ID_MAPPINGS_KEY);
|
||||
Optional<String> world = Optional.empty();
|
||||
for (String worldUid : mapIds.getKeys()) {
|
||||
world = Bukkit.getWorlds().stream()
|
||||
world = getPlugin().getServer().getWorlds().stream()
|
||||
.map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid))
|
||||
.findFirst();
|
||||
if (world.isPresent()) {
|
||||
@@ -157,7 +165,7 @@ public interface BukkitMapPersister {
|
||||
meta.setMapView(view);
|
||||
map.setItemMeta(meta);
|
||||
getPlugin().debug(String.format("View exists (#%s); updated map (UID: %s)", view.getId(), uid));
|
||||
return nbt;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,28 +173,84 @@ public interface BukkitMapPersister {
|
||||
final MapData canvasData;
|
||||
try {
|
||||
getPlugin().debug("Deserializing map data from NBT and generating view...");
|
||||
canvasData = MapData.fromByteArray(mapData.getByteArray(MAP_PIXEL_DATA_KEY));
|
||||
canvasData = MapData.fromByteArray(Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY),
|
||||
"Map pixel data is null"));
|
||||
} catch (Throwable e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to deserialize map data from NBT", e);
|
||||
return nbt;
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a renderer to the map with the data
|
||||
// Add a renderer to the map with the data and save to file
|
||||
final MapView view = generateRenderedMap(canvasData);
|
||||
final String worldUid = getDefaultMapWorld().getUID().toString();
|
||||
meta.setMapView(view);
|
||||
map.setItemMeta(meta);
|
||||
saveMapToFile(canvasData, view.getId());
|
||||
|
||||
// Set the map view ID in NBT
|
||||
NBT.modify(map, editable -> {
|
||||
editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY).setInteger(worldUid, view.getId());
|
||||
Objects.requireNonNull(editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY),
|
||||
"Map view ID mappings compound is null")
|
||||
.setInteger(worldUid, view.getId());
|
||||
});
|
||||
getPlugin().debug(String.format("Generated view (#%s) and updated map (UID: %s)", view.getId(), worldUid));
|
||||
return nbt;
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
default void renderMapFromFile(@NotNull MapView view) {
|
||||
final File mapFile = new File(getMapCacheFolder(), view.getId() + ".dat");
|
||||
if (!mapFile.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final MapData canvasData;
|
||||
try {
|
||||
canvasData = MapData.fromNbt(mapFile);
|
||||
} catch (Throwable e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to deserialize map data from file", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new map view renderer with the map data color at each pixel
|
||||
// use view.removeRenderer() to remove all this maps renderers
|
||||
view.getRenderers().forEach(view::removeRenderer);
|
||||
view.addRenderer(new PersistentMapRenderer(canvasData));
|
||||
view.setLocked(true);
|
||||
view.setScale(MapView.Scale.NORMAL);
|
||||
view.setTrackingPosition(false);
|
||||
view.setUnlimitedTracking(false);
|
||||
|
||||
// Set the view to the map
|
||||
setMapView(view);
|
||||
}
|
||||
|
||||
default void saveMapToFile(@NotNull MapData data, int id) {
|
||||
getPlugin().runAsync(() -> {
|
||||
final File mapFile = new File(getMapCacheFolder(), id + ".dat");
|
||||
if (mapFile.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final CompoundTag rootTag = new CompoundTag();
|
||||
rootTag.put("data", data.toNBT().getTag());
|
||||
NBTUtil.write(rootTag, mapFile);
|
||||
} catch (Throwable e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to serialize map data to file", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private File getMapCacheFolder() {
|
||||
final File mapCache = new File(getPlugin().getDataFolder(), "maps");
|
||||
if (!mapCache.exists() && !mapCache.mkdirs()) {
|
||||
getPlugin().log(Level.WARNING, "Failed to create maps folder");
|
||||
}
|
||||
return mapCache;
|
||||
}
|
||||
|
||||
// Sets the renderer of a map, and returns the generated MapView
|
||||
@NotNull
|
||||
private MapView generateRenderedMap(@NotNull MapData canvasData) {
|
||||
@@ -245,6 +309,10 @@ public interface BukkitMapPersister {
|
||||
|
||||
// Set the map banners and markers
|
||||
final MapCursorCollection cursors = canvas.getCursors();
|
||||
while (cursors.size() > 0) {
|
||||
cursors.removeCursor(cursors.getCursor(0));
|
||||
}
|
||||
|
||||
canvasData.getBanners().forEach(banner -> cursors.addCursor(createBannerCursor(banner)));
|
||||
canvas.setCursors(cursors);
|
||||
}
|
||||
@@ -255,7 +323,7 @@ public interface BukkitMapPersister {
|
||||
return new MapCursor(
|
||||
(byte) banner.getPosition().getX(),
|
||||
(byte) banner.getPosition().getZ(),
|
||||
(byte) 0,
|
||||
(byte) 8, // Always rotate banners upright
|
||||
switch (banner.getColor().toLowerCase(Locale.ENGLISH)) {
|
||||
case "white" -> MapCursor.Type.BANNER_WHITE;
|
||||
case "orange" -> MapCursor.Type.BANNER_ORANGE;
|
||||
@@ -350,13 +418,14 @@ public interface BukkitMapPersister {
|
||||
*/
|
||||
@NotNull
|
||||
private MapData extractMapData() {
|
||||
final List<MapBanner> banners = new ArrayList<>();
|
||||
final List<MapBanner> banners = Lists.newArrayList();
|
||||
final String BANNER_PREFIX = "banner_";
|
||||
for (int i = 0; i < getCursors().size(); i++) {
|
||||
final MapCursor cursor = getCursors().getCursor(i);
|
||||
final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH);
|
||||
if (type.startsWith("banner_")) {
|
||||
if (type.startsWith(BANNER_PREFIX)) {
|
||||
banners.add(new MapBanner(
|
||||
type.replaceAll("banner_", ""),
|
||||
type.replaceAll(BANNER_PREFIX, ""),
|
||||
cursor.getCaption() == null ? "" : cursor.getCaption(),
|
||||
cursor.getX(),
|
||||
mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128,
|
||||
@@ -373,6 +442,6 @@ public interface BukkitMapPersister {
|
||||
|
||||
@ApiStatus.Internal
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
BukkitHuskSync getPlugin();
|
||||
|
||||
}
|
||||
|
||||
@@ -21,8 +21,11 @@ package net.william278.husksync.util;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.UserDataHolder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
|
||||
import space.arim.morepaperlib.scheduling.AttachedScheduler;
|
||||
import space.arim.morepaperlib.scheduling.RegionalScheduler;
|
||||
import space.arim.morepaperlib.scheduling.ScheduledTask;
|
||||
|
||||
@@ -34,9 +37,12 @@ public interface BukkitTask extends Task {
|
||||
class Sync extends Task.Sync implements BukkitTask {
|
||||
|
||||
private ScheduledTask task;
|
||||
private final @Nullable UserDataHolder user;
|
||||
|
||||
protected Sync(@NotNull HuskSync plugin, @NotNull Runnable runnable, long delayTicks) {
|
||||
protected Sync(@NotNull HuskSync plugin, @NotNull Runnable runnable,
|
||||
@Nullable UserDataHolder user, long delayTicks) {
|
||||
super(plugin, runnable, delayTicks);
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -57,7 +63,19 @@ public interface BukkitTask extends Task {
|
||||
return;
|
||||
}
|
||||
|
||||
final RegionalScheduler scheduler = ((BukkitHuskSync) getPlugin()).getRegionalScheduler();
|
||||
// Use entity-specific scheduler if user is not null
|
||||
if (user != null) {
|
||||
final AttachedScheduler scheduler = ((BukkitHuskSync) getPlugin()).getUserSyncScheduler(user);
|
||||
if (delayTicks > 0) {
|
||||
this.task = scheduler.runDelayed(runnable, null, delayTicks);
|
||||
} else {
|
||||
this.task = scheduler.run(runnable, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Or default to the global scheduler
|
||||
final RegionalScheduler scheduler = ((BukkitHuskSync) getPlugin()).getSyncScheduler();
|
||||
if (delayTicks > 0) {
|
||||
this.task = scheduler.runDelayed(runnable, delayTicks);
|
||||
} else {
|
||||
@@ -146,8 +164,8 @@ public interface BukkitTask extends Task {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Task.Sync getSyncTask(@NotNull Runnable runnable, long delayTicks) {
|
||||
return new Sync(getPlugin(), runnable, delayTicks);
|
||||
default Task.Sync getSyncTask(@NotNull Runnable runnable, @Nullable UserDataHolder user, long delayTicks) {
|
||||
return new Sync(getPlugin(), runnable, user, delayTicks);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
husksync {
|
||||
update;
|
||||
about;
|
||||
status;
|
||||
reload;
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
name: 'HuskSync'
|
||||
version: '${version}'
|
||||
main: 'net.william278.husksync.BukkitHuskSync'
|
||||
api-version: 1.16
|
||||
api-version: 1.17
|
||||
author: 'William278'
|
||||
description: '${description}'
|
||||
website: 'https://william278.net'
|
||||
folia-supported: true
|
||||
softdepend:
|
||||
- 'ProtocolLib'
|
||||
- 'MysqlPlayerDataBridge'
|
||||
- 'Plan'
|
||||
libraries:
|
||||
- 'redis.clients:jedis:${jedis_version}'
|
||||
- 'com.mysql:mysql-connector-j:${mysql_driver_version}'
|
||||
- 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}'
|
||||
- 'org.postgresql:postgresql:${postgres_driver_version}'
|
||||
- 'org.mongodb:mongodb-driver-sync:${mongodb_driver_version}'
|
||||
- 'org.xerial.snappy:snappy-java:${snappy_version}'
|
||||
@@ -3,31 +3,38 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'commons-io:commons-io:2.13.0'
|
||||
api 'org.apache.commons:commons-text:1.10.0'
|
||||
api 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
|
||||
api 'net.kyori:adventure-api:4.14.0'
|
||||
api 'org.json:json:20230618'
|
||||
api 'commons-io:commons-io:2.16.0'
|
||||
api 'org.apache.commons:commons-text:1.11.0'
|
||||
api 'net.william278:minedown:1.8.2'
|
||||
api 'org.json:json:20240303'
|
||||
api 'com.google.code.gson:gson:2.10.1'
|
||||
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
|
||||
api 'dev.dejvokep:boosted-yaml:1.3.1'
|
||||
api 'net.william278:annotaml:2.0.7'
|
||||
api 'de.exlll:configlib-yaml:4.5.0'
|
||||
api 'net.william278:paginedown:1.1.2'
|
||||
api 'net.william278:DesertWell:2.0.4'
|
||||
api 'net.william278:PagineDown:1.1'
|
||||
api('com.zaxxer:HikariCP:5.0.1') {
|
||||
api('com.zaxxer:HikariCP:5.1.0') {
|
||||
exclude module: 'slf4j-api'
|
||||
}
|
||||
|
||||
compileOnly 'org.jetbrains:annotations:24.0.1'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||
compileOnly 'org.jetbrains:annotations:24.1.0'
|
||||
compileOnly 'net.kyori:adventure-api:4.16.0'
|
||||
compileOnly 'net.kyori:adventure-platform-api:4.3.2'
|
||||
compileOnly 'com.google.guava:guava:33.1.0-jre'
|
||||
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
compileOnly "redis.clients:jedis:$jedis_version"
|
||||
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
|
||||
compileOnly "org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version"
|
||||
compileOnly "org.postgresql:postgresql:$postgres_driver_version"
|
||||
compileOnly "org.mongodb:mongodb-driver-sync:$mongodb_driver_version"
|
||||
compileOnly "org.xerial.snappy:snappy-java:$snappy_version"
|
||||
|
||||
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
testImplementation "redis.clients:jedis:$jedis_version"
|
||||
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
|
||||
testCompileOnly 'dev.dejvokep:boosted-yaml:1.3.1'
|
||||
testCompileOnly 'org.jetbrains:annotations:24.0.1'
|
||||
testImplementation 'com.google.guava:guava:33.1.0-jre'
|
||||
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
testCompileOnly 'de.exlll:configlib-yaml:4.5.0'
|
||||
testCompileOnly 'org.jetbrains:annotations:24.1.0'
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
||||
}
|
||||
@@ -22,13 +22,13 @@ package net.william278.husksync;
|
||||
import com.fatboyindustrial.gsonjavatime.Converters;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import net.william278.annotaml.Annotaml;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.platform.AudienceProvider;
|
||||
import net.william278.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.desertwell.util.UpdateChecker;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.config.ConfigProvider;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.Serializer;
|
||||
@@ -36,6 +36,7 @@ import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.event.EventDispatcher;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.sync.DataSyncer;
|
||||
import net.william278.husksync.user.ConsoleUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.util.LegacyConverter;
|
||||
@@ -43,16 +44,15 @@ import net.william278.husksync.util.Task;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Abstract implementation of the HuskSync plugin.
|
||||
*/
|
||||
public interface HuskSync extends Task.Supplier, EventDispatcher {
|
||||
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider {
|
||||
|
||||
int SPIGOT_RESOURCE_ID = 97144;
|
||||
|
||||
@@ -90,6 +90,11 @@ public interface HuskSync extends Task.Supplier, EventDispatcher {
|
||||
@NotNull
|
||||
RedisManager getRedisManager();
|
||||
|
||||
/**
|
||||
* Returns the implementing adapter for serializing data
|
||||
*
|
||||
* @return the {@link DataAdapter}
|
||||
*/
|
||||
@NotNull
|
||||
DataAdapter getDataAdapter();
|
||||
|
||||
@@ -130,6 +135,21 @@ public interface HuskSync extends Task.Supplier, EventDispatcher {
|
||||
return getSerializers().keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data syncer implementation
|
||||
*
|
||||
* @return the {@link DataSyncer} implementation
|
||||
*/
|
||||
@NotNull
|
||||
DataSyncer getDataSyncer();
|
||||
|
||||
/**
|
||||
* Set the data syncer implementation
|
||||
*
|
||||
* @param dataSyncer the {@link DataSyncer} implementation
|
||||
*/
|
||||
void setDataSyncer(@NotNull DataSyncer dataSyncer);
|
||||
|
||||
/**
|
||||
* Returns a list of available data {@link Migrator}s
|
||||
*
|
||||
@@ -157,26 +177,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher {
|
||||
log(Level.INFO, "Successfully initialized " + name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plugin {@link Settings}
|
||||
*
|
||||
* @return the {@link Settings}
|
||||
*/
|
||||
@NotNull
|
||||
Settings getSettings();
|
||||
|
||||
void setSettings(@NotNull Settings settings);
|
||||
|
||||
/**
|
||||
* Returns the plugin {@link Locales}
|
||||
*
|
||||
* @return the {@link Locales}
|
||||
*/
|
||||
@NotNull
|
||||
Locales getLocales();
|
||||
|
||||
void setLocales(@NotNull Locales locales);
|
||||
|
||||
/**
|
||||
* Returns if a dependency is loaded
|
||||
*
|
||||
@@ -217,18 +217,47 @@ public interface HuskSync extends Task.Supplier, EventDispatcher {
|
||||
* @param throwable a throwable to log
|
||||
*/
|
||||
default void debug(@NotNull String message, @NotNull Throwable... throwable) {
|
||||
if (getSettings().doDebugLogging()) {
|
||||
log(Level.INFO, String.format("[DEBUG] %s", message), throwable);
|
||||
if (getSettings().isDebugLogging()) {
|
||||
log(Level.INFO, getDebugString(message), throwable);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the debug log message format
|
||||
@NotNull
|
||||
private String getDebugString(@NotNull String message) {
|
||||
return String.format("[DEBUG] [%s] %s", new SimpleDateFormat("mm:ss.SSS").format(new Date()), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the console user
|
||||
* Get the {@link AudienceProvider} instance
|
||||
*
|
||||
* @return the {@link ConsoleUser}
|
||||
* @return the {@link AudienceProvider} instance
|
||||
* @since 1.0
|
||||
*/
|
||||
@NotNull
|
||||
ConsoleUser getConsole();
|
||||
AudienceProvider getAudiences();
|
||||
|
||||
/**
|
||||
* Get the {@link Audience} instance for the given {@link OnlineUser}
|
||||
*
|
||||
* @param user the {@link OnlineUser} to get the {@link Audience} for
|
||||
* @return the {@link Audience} instance
|
||||
*/
|
||||
@NotNull
|
||||
default Audience getAudience(@NotNull UUID user) {
|
||||
return getAudiences().player(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link ConsoleUser} instance
|
||||
*
|
||||
* @return the {@link ConsoleUser} instance
|
||||
* @since 1.0
|
||||
*/
|
||||
@NotNull
|
||||
default ConsoleUser getConsole() {
|
||||
return new ConsoleUser(getAudiences());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plugin version
|
||||
@@ -255,34 +284,12 @@ public interface HuskSync extends Task.Supplier, EventDispatcher {
|
||||
String getPlatformType();
|
||||
|
||||
/**
|
||||
* Returns the legacy data converter, if it exists
|
||||
* Returns the legacy data converter if it exists
|
||||
*
|
||||
* @return the {@link LegacyConverter}
|
||||
*/
|
||||
Optional<LegacyConverter> getLegacyConverter();
|
||||
|
||||
/**
|
||||
* Reloads the {@link Settings} and {@link Locales} from their respective config files.
|
||||
*/
|
||||
default void loadConfigs() {
|
||||
try {
|
||||
// Load settings
|
||||
setSettings(Annotaml.create(new File(getDataFolder(), "config.yml"), Settings.class).get());
|
||||
|
||||
// Load locales from language preset default
|
||||
final Locales languagePresets = Annotaml.create(
|
||||
Locales.class,
|
||||
Objects.requireNonNull(getResource(String.format("locales/%s.yml", getSettings().getLanguage())))
|
||||
).get();
|
||||
setLocales(Annotaml.create(new File(
|
||||
getDataFolder(),
|
||||
String.format("messages_%s.yml", getSettings().getLanguage())
|
||||
), languagePresets).get());
|
||||
} catch (IOException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
|
||||
throw new FailedToLoadException("Failed to load config or message files", e);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default UpdateChecker getUpdateChecker() {
|
||||
return UpdateChecker.builder()
|
||||
@@ -293,7 +300,7 @@ public interface HuskSync extends Task.Supplier, EventDispatcher {
|
||||
}
|
||||
|
||||
default void checkForUpdates() {
|
||||
if (getSettings().doCheckForUpdates()) {
|
||||
if (getSettings().isCheckForUpdates()) {
|
||||
getUpdateChecker().check().thenAccept(checked -> {
|
||||
if (!checked.isUpToDate()) {
|
||||
log(Level.WARNING, String.format(
|
||||
@@ -305,12 +312,31 @@ public interface HuskSync extends Task.Supplier, EventDispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the set of UUIDs of "locked players", for which events will be canceled.
|
||||
* </p>
|
||||
* Players are locked while their items are being set (on join) or saved (on quit)
|
||||
*/
|
||||
@NotNull
|
||||
Set<UUID> getLockedPlayers();
|
||||
|
||||
default boolean isLocked(@NotNull UUID uuid) {
|
||||
return getLockedPlayers().contains(uuid);
|
||||
}
|
||||
|
||||
default void lockPlayer(@NotNull UUID uuid) {
|
||||
getLockedPlayers().add(uuid);
|
||||
}
|
||||
|
||||
default void unlockPlayer(@NotNull UUID uuid) {
|
||||
getLockedPlayers().remove(uuid);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Gson getGson();
|
||||
|
||||
boolean isDisabling();
|
||||
|
||||
@NotNull
|
||||
default Gson createGson() {
|
||||
return Converters.registerOffsetDateTime(new GsonBuilder()).create();
|
||||
@@ -324,12 +350,12 @@ public interface HuskSync extends Task.Supplier, EventDispatcher {
|
||||
private static final String FORMAT = """
|
||||
HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized.
|
||||
Please make sure the plugin has been setup correctly (https://william278.net/docs/husksync/setup):
|
||||
|
||||
1) Make sure you've entered your MySQL or MariaDB database details correctly in config.yml
|
||||
|
||||
1) Make sure you've entered your MySQL, MariaDB or MongoDB database details correctly in config.yml
|
||||
2) Make sure your Redis server details are also correct in config.yml
|
||||
3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-files)
|
||||
3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-file)
|
||||
4) Check the error below for more details
|
||||
|
||||
|
||||
Caused by: %s""";
|
||||
|
||||
FailedToLoadException(@NotNull String message, @NotNull Throwable cause) {
|
||||
|
||||
@@ -26,25 +26,31 @@ import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.Serializer;
|
||||
import net.william278.husksync.sync.DataSyncer;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* The base implementation of the HuskSync API, containing cross-platform API calls.
|
||||
* The common implementation of the HuskSync API, containing cross-platform API calls.
|
||||
* </p>
|
||||
* This class should not be used directly, but rather through platform-specific extending API classes.
|
||||
* Retrieve an instance of the API class via {@link #getInstance()}.
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class HuskSyncAPI {
|
||||
public class HuskSyncAPI {
|
||||
|
||||
// Instance of the plugin
|
||||
protected static HuskSyncAPI instance;
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Instance of the implementing plugin.
|
||||
@@ -59,6 +65,28 @@ public abstract class HuskSyncAPI {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint to the HuskSync API on the common platform - returns an instance of the API
|
||||
*
|
||||
* @return instance of the HuskSync API
|
||||
* @since 3.3
|
||||
*/
|
||||
@NotNull
|
||||
public static HuskSyncAPI getInstance() {
|
||||
if (instance == null) {
|
||||
throw new NotRegisteredException();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Unregister the API for this platform.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public static void unregister() {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link User} by their UUID
|
||||
*
|
||||
@@ -146,6 +174,7 @@ public abstract class HuskSyncAPI {
|
||||
public void editCurrentData(@NotNull User user, @NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
getCurrentData(user).thenAccept(optional -> optional.ifPresent(data -> {
|
||||
editor.accept(data);
|
||||
data.setId(UUID.randomUUID());
|
||||
setCurrentData(user, data);
|
||||
}));
|
||||
}
|
||||
@@ -236,13 +265,32 @@ public abstract class HuskSyncAPI {
|
||||
*
|
||||
* @param user The user to save the data for
|
||||
* @param snapshot The snapshot to save
|
||||
* @param callback A callback to run after the data has been saved (if the DataSaveEvent was not canceled)
|
||||
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will be fired unless the
|
||||
* {@link DataSnapshot.SaveCause#fireDataSaveEvent()} is {@code false}
|
||||
* @since 3.3.2
|
||||
*/
|
||||
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot,
|
||||
@Nullable BiConsumer<User, DataSnapshot.Packed> callback) {
|
||||
plugin.runAsync(() -> plugin.getDataSyncer().saveData(
|
||||
user,
|
||||
snapshot instanceof DataSnapshot.Unpacked unpacked
|
||||
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot,
|
||||
callback
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a data snapshot to the database
|
||||
*
|
||||
* @param user The user to save the data for
|
||||
* @param snapshot The snapshot to save
|
||||
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will be fired unless the
|
||||
* {@link DataSnapshot.SaveCause#fireDataSaveEvent()} is {@code false}
|
||||
* @since 3.0
|
||||
*/
|
||||
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().addSnapshot(
|
||||
user, snapshot instanceof DataSnapshot.Unpacked unpacked
|
||||
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot
|
||||
));
|
||||
this.addSnapshot(user, snapshot, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -404,6 +452,7 @@ public abstract class HuskSyncAPI {
|
||||
* @param <T> The type of the element
|
||||
* @return The deserialized element
|
||||
* @throws Serializer.DeserializationException If the element could not be deserialized
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public <T extends Adaptable> T deserializeData(@NotNull String serialized, Class<T> type)
|
||||
@@ -418,6 +467,7 @@ public abstract class HuskSyncAPI {
|
||||
* @param <T> The type of the element
|
||||
* @return The serialized JSON string
|
||||
* @throws Serializer.SerializationException If the element could not be serialized
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public <T extends Adaptable> String serializeData(@NotNull T element)
|
||||
@@ -425,6 +475,16 @@ public abstract class HuskSyncAPI {
|
||||
return plugin.getDataAdapter().toJson(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link DataSyncer} to be used to sync data
|
||||
*
|
||||
* @param syncer The data syncer to use for synchronizing user data
|
||||
* @since 3.1
|
||||
*/
|
||||
public void setDataSyncer(@NotNull DataSyncer syncer) {
|
||||
plugin.setDataSyncer(syncer);
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Get the plugin instance
|
||||
*
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -36,7 +36,7 @@ public abstract class Command extends Node {
|
||||
@NotNull HuskSync plugin) {
|
||||
super(name, aliases, plugin);
|
||||
this.usage = usage;
|
||||
this.additionalPermissions = new HashMap<>();
|
||||
this.additionalPermissions = Maps.newHashMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,11 +23,14 @@ import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.redis.RedisKeyType;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -49,7 +52,8 @@ public class EnderChestCommand extends ItemsCommand {
|
||||
|
||||
// Display opening message
|
||||
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(),
|
||||
snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("dd/MM/yyyy, HH:mm")))
|
||||
snapshot.getTimestamp().format(DateTimeFormatter
|
||||
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
|
||||
// Show GUI
|
||||
@@ -70,8 +74,8 @@ public class EnderChestCommand extends ItemsCommand {
|
||||
|
||||
// Creates a new snapshot with the updated enderChest
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User user) {
|
||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(user);
|
||||
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
|
||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
|
||||
if (latestData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
@@ -81,12 +85,19 @@ public class EnderChestCommand extends ItemsCommand {
|
||||
// Create and pack the snapshot with the updated enderChest
|
||||
final DataSnapshot.Packed snapshot = latestData.get().copy();
|
||||
snapshot.edit(plugin, (data) -> {
|
||||
data.setSaveCause(DataSnapshot.SaveCause.ENDERCHEST_COMMAND);
|
||||
data.setPinned(plugin.getSettings().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND));
|
||||
data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items));
|
||||
data.setSaveCause(DataSnapshot.SaveCause.ENDERCHEST_COMMAND);
|
||||
data.setPinned(
|
||||
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND)
|
||||
);
|
||||
});
|
||||
|
||||
// Save data
|
||||
final RedisManager redis = plugin.getRedisManager();
|
||||
plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
|
||||
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
|
||||
redis.sendUserDataUpdate(user, data);
|
||||
});
|
||||
plugin.getDatabase().addSnapshot(user, snapshot);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, snapshot);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,17 +21,23 @@ package net.william278.husksync.command;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.JoinConfiguration;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.william278.desertwell.about.AboutMenu;
|
||||
import net.william278.desertwell.util.UpdateChecker;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.apache.commons.text.WordUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -39,6 +45,7 @@ public class HuskSyncCommand extends Command implements TabProvider {
|
||||
|
||||
private static final Map<String, Boolean> SUB_COMMANDS = Map.of(
|
||||
"about", false,
|
||||
"status", true,
|
||||
"reload", true,
|
||||
"migrate", true,
|
||||
"update", true
|
||||
@@ -60,7 +67,8 @@ public class HuskSyncCommand extends Command implements TabProvider {
|
||||
AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net"))
|
||||
.credits("Contributors",
|
||||
AboutMenu.Credit.of("HarvelsX").description("Code"),
|
||||
AboutMenu.Credit.of("HookWoods").description("Code"))
|
||||
AboutMenu.Credit.of("HookWoods").description("Code"),
|
||||
AboutMenu.Credit.of("Preva1l").description("Code"))
|
||||
.credits("Translators",
|
||||
AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"),
|
||||
AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"),
|
||||
@@ -72,7 +80,10 @@ public class HuskSyncCommand extends Command implements TabProvider {
|
||||
AboutMenu.Credit.of("Ghost-chu").description("Simplified Chinese (zh-cn)"),
|
||||
AboutMenu.Credit.of("DJelly4K").description("Simplified Chinese (zh-cn)"),
|
||||
AboutMenu.Credit.of("Thourgard").description("Ukrainian (uk-ua)"),
|
||||
AboutMenu.Credit.of("xF3d3").description("Italian (it-it)"))
|
||||
AboutMenu.Credit.of("xF3d3").description("Italian (it-it)"),
|
||||
AboutMenu.Credit.of("cada3141").description("Korean (ko-kr)"),
|
||||
AboutMenu.Credit.of("Wirayuda5620").description("Indonesian (id-id)"),
|
||||
AboutMenu.Credit.of("WinTone01").description("Turkish (tr-tr)"))
|
||||
.buttons(
|
||||
AboutMenu.Link.of("https://william278.net/docs/husksync").text("Documentation").icon("⛏"),
|
||||
AboutMenu.Link.of("https://github.com/WiIIiam278/HuskSync/issues").text("Issues").icon("❌").color(TextColor.color(0xff9f0f)),
|
||||
@@ -91,9 +102,18 @@ public class HuskSyncCommand extends Command implements TabProvider {
|
||||
|
||||
switch (subCommand) {
|
||||
case "about" -> executor.sendMessage(aboutMenu.toComponent());
|
||||
case "status" -> {
|
||||
getPlugin().getLocales().getLocale("system_status_header").ifPresent(executor::sendMessage);
|
||||
executor.sendMessage(Component.join(
|
||||
JoinConfiguration.newlines(),
|
||||
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList()
|
||||
));
|
||||
}
|
||||
case "reload" -> {
|
||||
try {
|
||||
plugin.loadConfigs();
|
||||
plugin.loadSettings();
|
||||
plugin.loadLocales();
|
||||
plugin.loadServer();
|
||||
plugin.getLocales().getLocale("reload_complete").ifPresent(executor::sendMessage);
|
||||
} catch (Throwable e) {
|
||||
executor.sendMessage(new MineDown(
|
||||
@@ -181,4 +201,80 @@ public class HuskSyncCommand extends Command implements TabProvider {
|
||||
};
|
||||
}
|
||||
|
||||
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 -> {
|
||||
boolean enabled = plugin.getSettings().getSynchronization().isFeatureEnabled(i);
|
||||
return Component.textOfChildren(Component
|
||||
.text(i.toString()).appendSpace().append(Component.text(enabled ? '✔' : '❌')))
|
||||
.color(enabled ? NamedTextColor.GREEN : NamedTextColor.RED)
|
||||
.hoverEvent(HoverEvent.showText(Component.text(enabled ? "Enabled" : "Disabled")));
|
||||
}).toList()
|
||||
));
|
||||
|
||||
private final Function<HuskSync, Component> supplier;
|
||||
|
||||
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,11 +23,14 @@ import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.redis.RedisKeyType;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -49,7 +52,8 @@ public class InventoryCommand extends ItemsCommand {
|
||||
|
||||
// Display opening message
|
||||
plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(),
|
||||
snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("dd/MM/yyyy, HH:mm")))
|
||||
snapshot.getTimestamp().format(DateTimeFormatter
|
||||
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
|
||||
// Show GUI
|
||||
@@ -70,8 +74,8 @@ public class InventoryCommand extends ItemsCommand {
|
||||
|
||||
// Creates a new snapshot with the updated inventory
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User user) {
|
||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(user);
|
||||
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
|
||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
|
||||
if (latestData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
@@ -81,12 +85,19 @@ public class InventoryCommand extends ItemsCommand {
|
||||
// Create and pack the snapshot with the updated inventory
|
||||
final DataSnapshot.Packed snapshot = latestData.get().copy();
|
||||
snapshot.edit(plugin, (data) -> {
|
||||
data.setSaveCause(DataSnapshot.SaveCause.INVENTORY_COMMAND);
|
||||
data.setPinned(plugin.getSettings().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND));
|
||||
data.getInventory().ifPresent(inventory -> inventory.setContents(items));
|
||||
data.setSaveCause(DataSnapshot.SaveCause.INVENTORY_COMMAND);
|
||||
data.setPinned(
|
||||
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND)
|
||||
);
|
||||
});
|
||||
|
||||
// Save data
|
||||
final RedisManager redis = plugin.getRedisManager();
|
||||
plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
|
||||
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
|
||||
redis.sendUserDataUpdate(user, data);
|
||||
});
|
||||
plugin.getDatabase().addSnapshot(user, snapshot);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, snapshot);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -71,26 +71,43 @@ public abstract class ItemsCommand extends Command implements TabProvider {
|
||||
private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) {
|
||||
plugin.getRedisManager().getUserData(user.getUuid(), user).thenAccept(data -> data
|
||||
.or(() -> plugin.getDatabase().getLatestSnapshot(user))
|
||||
.ifPresentOrElse(
|
||||
snapshot -> this.showItems(
|
||||
viewer, snapshot.unpack(plugin), user,
|
||||
viewer.hasPermission(getPermission("edit"))
|
||||
),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage)
|
||||
));
|
||||
.or(() -> {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return Optional.empty();
|
||||
})
|
||||
.flatMap(packed -> {
|
||||
if (packed.isInvalid()) {
|
||||
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(packed.unpack(plugin));
|
||||
})
|
||||
.ifPresent(snapshot -> this.showItems(
|
||||
viewer, snapshot, user, viewer.hasPermission(getPermission("edit"))
|
||||
)));
|
||||
}
|
||||
|
||||
// View a specific version of the user data
|
||||
private void showSnapshotItems(@NotNull OnlineUser viewer, @NotNull User user, @NotNull UUID version) {
|
||||
plugin.getDatabase().getSnapshot(user, version)
|
||||
.ifPresentOrElse(
|
||||
snapshot -> this.showItems(
|
||||
viewer, snapshot.unpack(plugin), user, false
|
||||
),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(viewer::sendMessage)
|
||||
);
|
||||
.or(() -> {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return Optional.empty();
|
||||
})
|
||||
.flatMap(packed -> {
|
||||
if (packed.isInvalid()) {
|
||||
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(packed.unpack(plugin));
|
||||
})
|
||||
.ifPresent(snapshot -> this.showItems(
|
||||
viewer, snapshot, user, false
|
||||
));
|
||||
}
|
||||
|
||||
// Show a GUI menu with the correct item data from the snapshot
|
||||
|
||||
@@ -21,6 +21,8 @@ package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.redis.RedisKeyType;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import net.william278.husksync.util.DataDumper;
|
||||
@@ -59,7 +61,7 @@ public class UserDataCommand extends Command implements TabProvider {
|
||||
.or(() -> parseStringArg(args, 0).flatMap(name -> plugin.getDatabase().getUserByName(name)))
|
||||
.or(() -> args.length < 2 && executor instanceof User userExecutor
|
||||
? Optional.of(userExecutor) : Optional.empty());
|
||||
final Optional<UUID> optionalUuid = parseUUIDArg(args, 2).or(() -> parseUUIDArg(args, 1));
|
||||
final Optional<UUID> uuid = parseUUIDArg(args, 2).or(() -> parseUUIDArg(args, 1));
|
||||
if (optionalUser.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(executor::sendMessage);
|
||||
@@ -68,158 +70,179 @@ public class UserDataCommand extends Command implements TabProvider {
|
||||
|
||||
final User user = optionalUser.get();
|
||||
switch (subCommand) {
|
||||
case "view" -> optionalUuid.ifPresentOrElse(
|
||||
// Show the specified snapshot
|
||||
version -> plugin.getDatabase().getSnapshot(user, version).ifPresentOrElse(
|
||||
data -> DataSnapshotOverview.of(
|
||||
data.unpack(plugin), data.getFileSize(plugin), user, plugin
|
||||
).show(executor),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage)),
|
||||
|
||||
// Show the latest snapshot
|
||||
() -> plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse(
|
||||
data -> DataSnapshotOverview.of(
|
||||
data.unpack(plugin), data.getFileSize(plugin), user, plugin
|
||||
).show(executor),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(executor::sendMessage))
|
||||
case "view" -> uuid.ifPresentOrElse(
|
||||
version -> viewSnapshot(executor, user, version),
|
||||
() -> viewLatestSnapshot(executor, user)
|
||||
);
|
||||
|
||||
case "list" -> {
|
||||
// Check if there is data to display
|
||||
final List<DataSnapshot.Packed> dataList = plugin.getDatabase().getAllSnapshots(user);
|
||||
if (dataList.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the list to the player
|
||||
DataSnapshotList.create(dataList, user, plugin).displayPage(
|
||||
executor,
|
||||
parseIntArg(args, 2).or(() -> parseIntArg(args, 1)).orElse(1)
|
||||
);
|
||||
}
|
||||
|
||||
case "delete" -> {
|
||||
if (optionalUuid.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
case "list" -> listSnapshots(
|
||||
executor, user, parseIntArg(args, 2).or(() -> parseIntArg(args, 1)).orElse(1)
|
||||
);
|
||||
case "delete" -> uuid.ifPresentOrElse(
|
||||
version -> deleteSnapshot(executor, user, version),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata delete <username> <version_uuid>")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete user data by specified UUID
|
||||
final UUID version = optionalUuid.get();
|
||||
if (!plugin.getDatabase().deleteSnapshot(user, version)) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getLocales().getLocale("data_deleted",
|
||||
version.toString().split("-")[0],
|
||||
version.toString(),
|
||||
user.getUsername(),
|
||||
user.getUuid().toString())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
|
||||
case "restore" -> {
|
||||
if (optionalUuid.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
.ifPresent(executor::sendMessage)
|
||||
);
|
||||
case "restore" -> uuid.ifPresentOrElse(
|
||||
version -> restoreSnapshot(executor, user, version),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata restore <username> <version_uuid>")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore user data by specified UUID
|
||||
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, optionalUuid.get());
|
||||
if (optionalData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore users with a minimum of one health (prevent restoring players with <=0 health)
|
||||
final DataSnapshot.Packed data = optionalData.get().copy();
|
||||
data.edit(plugin, (unpacked -> {
|
||||
unpacked.getHealth().ifPresent(status -> status.setHealth(Math.max(1, status.getHealth())));
|
||||
unpacked.setSaveCause(DataSnapshot.SaveCause.BACKUP_RESTORE);
|
||||
unpacked.setPinned(plugin.getSettings().doAutoPin(DataSnapshot.SaveCause.BACKUP_RESTORE));
|
||||
}));
|
||||
|
||||
// Set the user's data and send a message
|
||||
plugin.getDatabase().addSnapshot(user, data);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, data);
|
||||
plugin.getLocales().getLocale("data_restored", user.getUsername(), user.getUuid().toString(),
|
||||
data.getShortId(), data.getId().toString()).ifPresent(executor::sendMessage);
|
||||
}
|
||||
|
||||
case "pin" -> {
|
||||
if (optionalUuid.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
.ifPresent(executor::sendMessage)
|
||||
);
|
||||
case "pin" -> uuid.ifPresentOrElse(
|
||||
version -> pinSnapshot(executor, user, version),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata pin <username> <version_uuid>")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that the data exists
|
||||
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, optionalUuid.get());
|
||||
if (optionalData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pin or unpin the data
|
||||
final DataSnapshot.Packed data = optionalData.get();
|
||||
if (data.isPinned()) {
|
||||
plugin.getDatabase().unpinSnapshot(user, data.getId());
|
||||
} else {
|
||||
plugin.getDatabase().pinSnapshot(user, data.getId());
|
||||
}
|
||||
plugin.getLocales().getLocale(data.isPinned() ? "data_unpinned" : "data_pinned", data.getShortId(),
|
||||
data.getId().toString(), user.getUsername(), user.getUuid().toString())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
|
||||
case "dump" -> {
|
||||
if (optionalUuid.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata dump <username> <version_uuid>")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine dump type
|
||||
final boolean webDump = parseStringArg(args, 3)
|
||||
.map(arg -> arg.equalsIgnoreCase("web"))
|
||||
.orElse(false);
|
||||
final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, optionalUuid.get());
|
||||
if (data.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Dump the data
|
||||
final DataSnapshot.Packed userData = data.get();
|
||||
final DataDumper dumper = DataDumper.create(userData, user, plugin);
|
||||
try {
|
||||
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getUsername(),
|
||||
(webDump ? dumper.toWeb() : dumper.toFile())).ifPresent(executor::sendMessage);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "Failed to dump user data", e);
|
||||
}
|
||||
}
|
||||
|
||||
.ifPresent(executor::sendMessage)
|
||||
);
|
||||
case "dump" -> uuid.ifPresentOrElse(
|
||||
version -> dumpSnapshot(executor, user, version, parseStringArg(args, 3)
|
||||
.map(arg -> arg.equalsIgnoreCase("web")).orElse(false)),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata dump <web/file> <username> <version_uuid>")
|
||||
.ifPresent(executor::sendMessage)
|
||||
);
|
||||
default -> plugin.getLocales().getLocale("error_invalid_syntax", getUsage())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Show the latest snapshot
|
||||
private void viewLatestSnapshot(@NotNull CommandUser executor, @NotNull User user) {
|
||||
plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse(
|
||||
data -> {
|
||||
if (data.isInvalid()) {
|
||||
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
DataSnapshotOverview.of(data.unpack(plugin), data.getFileSize(plugin), user, plugin)
|
||||
.show(executor);
|
||||
},
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(executor::sendMessage)
|
||||
);
|
||||
}
|
||||
|
||||
// Show the specified snapshot
|
||||
private void viewSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
||||
plugin.getDatabase().getSnapshot(user, version).ifPresentOrElse(
|
||||
data -> {
|
||||
if (data.isInvalid()) {
|
||||
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
DataSnapshotOverview.of(data.unpack(plugin), data.getFileSize(plugin), user, plugin)
|
||||
.show(executor);
|
||||
},
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage)
|
||||
);
|
||||
}
|
||||
|
||||
// View a list of snapshots
|
||||
private void listSnapshots(@NotNull CommandUser executor, @NotNull User user, int page) {
|
||||
final List<DataSnapshot.Packed> dataList = plugin.getDatabase().getAllSnapshots(user);
|
||||
if (dataList.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
DataSnapshotList.create(dataList, user, plugin).displayPage(executor, page);
|
||||
}
|
||||
|
||||
// Delete a snapshot
|
||||
private void deleteSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
||||
if (!plugin.getDatabase().deleteSnapshot(user, version)) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getRedisManager().clearUserData(user);
|
||||
plugin.getLocales().getLocale("data_deleted",
|
||||
version.toString().split("-")[0],
|
||||
version.toString(),
|
||||
user.getUsername(),
|
||||
user.getUuid().toString())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
|
||||
// Restore a snapshot
|
||||
private void restoreSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
||||
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, version);
|
||||
if (optionalData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore users with a minimum of one health (prevent restoring players with <= 0 health)
|
||||
final DataSnapshot.Packed data = optionalData.get().copy();
|
||||
if (data.isInvalid()) {
|
||||
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
data.edit(plugin, (unpacked -> {
|
||||
unpacked.getHealth().ifPresent(status -> status.setHealth(Math.max(1, status.getHealth())));
|
||||
unpacked.setSaveCause(DataSnapshot.SaveCause.BACKUP_RESTORE);
|
||||
unpacked.setPinned(
|
||||
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.BACKUP_RESTORE)
|
||||
);
|
||||
}));
|
||||
|
||||
// Save data
|
||||
final RedisManager redis = plugin.getRedisManager();
|
||||
plugin.getDataSyncer().saveData(user, data, (u, s) -> {
|
||||
redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s, RedisKeyType.TTL_1_YEAR));
|
||||
redis.sendUserDataUpdate(u, s);
|
||||
plugin.getLocales().getLocale("data_restored", u.getUsername(), u.getUuid().toString(),
|
||||
s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage);
|
||||
});
|
||||
}
|
||||
|
||||
// Pin a snapshot
|
||||
private void pinSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
||||
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, version);
|
||||
if (optionalData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pin or unpin the data
|
||||
final DataSnapshot.Packed data = optionalData.get();
|
||||
if (data.isPinned()) {
|
||||
plugin.getDatabase().unpinSnapshot(user, data.getId());
|
||||
} else {
|
||||
plugin.getDatabase().pinSnapshot(user, data.getId());
|
||||
}
|
||||
plugin.getLocales().getLocale(data.isPinned() ? "data_unpinned" : "data_pinned", data.getShortId(),
|
||||
data.getId().toString(), user.getUsername(), user.getUuid().toString())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
|
||||
// Dump a snapshot
|
||||
private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version, boolean webDump) {
|
||||
final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, version);
|
||||
if (data.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Dump the data
|
||||
final DataSnapshot.Packed userData = data.get();
|
||||
final DataDumper dumper = DataDumper.create(userData, user, plugin);
|
||||
try {
|
||||
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getUsername(),
|
||||
(webDump ? dumper.toWeb() : dumper.toFile())).ifPresent(executor::sendMessage);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "Failed to dump user data", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> suggest(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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.config;
|
||||
|
||||
|
||||
import de.exlll.configlib.NameFormatters;
|
||||
import de.exlll.configlib.YamlConfigurationProperties;
|
||||
import de.exlll.configlib.YamlConfigurationStore;
|
||||
import de.exlll.configlib.YamlConfigurations;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Interface for getting and setting data from plugin configuration files
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public interface ConfigProvider {
|
||||
|
||||
@NotNull
|
||||
YamlConfigurationProperties.Builder<?> YAML_CONFIGURATION_PROPERTIES = YamlConfigurationProperties.newBuilder()
|
||||
.charset(StandardCharsets.UTF_8)
|
||||
.setNameFormatter(NameFormatters.LOWER_UNDERSCORE);
|
||||
|
||||
/**
|
||||
* Get the plugin settings, read from the config file
|
||||
*
|
||||
* @return the plugin settings
|
||||
* @since 1.0
|
||||
*/
|
||||
@NotNull
|
||||
Settings getSettings();
|
||||
|
||||
/**
|
||||
* Set the plugin settings
|
||||
*
|
||||
* @param settings The settings to set
|
||||
* @since 1.0
|
||||
*/
|
||||
void setSettings(@NotNull Settings settings);
|
||||
|
||||
/**
|
||||
* Load the plugin settings from the config file
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
default void loadSettings() {
|
||||
setSettings(YamlConfigurations.update(
|
||||
getConfigDirectory().resolve("config.yml"),
|
||||
Settings.class,
|
||||
YAML_CONFIGURATION_PROPERTIES.header(Settings.CONFIG_HEADER).build()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the locales for the plugin
|
||||
*
|
||||
* @return the locales for the plugin
|
||||
* @since 1.0
|
||||
*/
|
||||
@NotNull
|
||||
Locales getLocales();
|
||||
|
||||
/**
|
||||
* Set the locales for the plugin
|
||||
*
|
||||
* @param locales The locales to set
|
||||
* @since 1.0
|
||||
*/
|
||||
void setLocales(@NotNull Locales locales);
|
||||
|
||||
/**
|
||||
* Load the locales from the config file
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
default void loadLocales() {
|
||||
final YamlConfigurationStore<Locales> store = new YamlConfigurationStore<>(
|
||||
Locales.class, YAML_CONFIGURATION_PROPERTIES.header(Locales.CONFIG_HEADER).build()
|
||||
);
|
||||
// Read existing locales if present
|
||||
final Path path = getConfigDirectory().resolve(String.format("messages-%s.yml", getSettings().getLanguage()));
|
||||
if (Files.exists(path)) {
|
||||
setLocales(store.load(path));
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, save and read the default locales
|
||||
try (InputStream input = getResource(String.format("locales/%s.yml", getSettings().getLanguage()))) {
|
||||
final Locales locales = store.read(input);
|
||||
store.save(locales, path);
|
||||
setLocales(locales);
|
||||
} catch (Throwable e) {
|
||||
getPlugin().log(Level.SEVERE, "An error occurred loading the locales (invalid lang code?)", e);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
String getServerName();
|
||||
|
||||
void setServerName(@NotNull Server server);
|
||||
|
||||
default void loadServer() {
|
||||
setServerName(YamlConfigurations.update(
|
||||
getConfigDirectory().resolve("server.yml"),
|
||||
Server.class,
|
||||
YAML_CONFIGURATION_PROPERTIES.header(Server.CONFIG_HEADER).build()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a plugin resource
|
||||
*
|
||||
* @param name The name of the resource
|
||||
* @return the resource, if found
|
||||
* @since 1.0
|
||||
*/
|
||||
InputStream getResource(@NotNull String name);
|
||||
|
||||
/**
|
||||
* Get the plugin config directory
|
||||
*
|
||||
* @return the plugin config directory
|
||||
* @since 1.0
|
||||
*/
|
||||
@NotNull
|
||||
Path getConfigDirectory();
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
@@ -19,53 +19,60 @@
|
||||
|
||||
package net.william278.husksync.config;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import de.exlll.configlib.Configuration;
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.annotaml.YamlFile;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.william278.paginedown.ListOptions;
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Loaded locales used by the plugin to display styled messages
|
||||
* Plugin locale configuration
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
@YamlFile(rootedMap = true, header = """
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ HuskSync Locales ┃
|
||||
┃ Developed by William278 ┃
|
||||
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
┣╸ See plugin about menu for international locale credits
|
||||
┣╸ Formatted in MineDown: https://github.com/Phoenix616/MineDown
|
||||
┗╸ Translate HuskSync: https://william278.net/docs/husksync/Translations""")
|
||||
@SuppressWarnings("FieldMayBeFinal")
|
||||
@Configuration
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class Locales {
|
||||
|
||||
/**
|
||||
* The raw set of locales loaded from yaml
|
||||
*/
|
||||
@NotNull
|
||||
public Map<String, String> rawLocales = new HashMap<>();
|
||||
static final String CONFIG_HEADER = """
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ HuskSync - Locales ┃
|
||||
┃ Developed by William278 ┃
|
||||
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
┣╸ See plugin about menu for international locale credits
|
||||
┣╸ Formatted in MineDown: https://github.com/Phoenix616/MineDown
|
||||
┗╸ Translate HuskSync: https://william278.net/docs/husksync/translations""";
|
||||
|
||||
protected static final String DEFAULT_LOCALE = "en-gb";
|
||||
|
||||
// The raw set of locales loaded from yaml
|
||||
Map<String, String> locales = Maps.newTreeMap();
|
||||
|
||||
/**
|
||||
* Returns a raw, unformatted locale loaded from the Locales file
|
||||
* Returns a raw, unformatted locale loaded from the locale file
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @return An {@link Optional} containing the locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<String> getRawLocale(@NotNull String localeId) {
|
||||
return Optional.ofNullable(rawLocales.get(localeId)).map(StringEscapeUtils::unescapeJava);
|
||||
return Optional.ofNullable(locales.get(localeId)).map(StringEscapeUtils::unescapeJava);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a raw, unformatted locale loaded from the Locales file, with replacements applied
|
||||
* Returns a raw, un-formatted locale loaded from the locales file, with replacements applied
|
||||
* <p>
|
||||
* Note that replacements will not be MineDown-escaped; use {@link #escapeMineDown(String)} to escape replacements
|
||||
* Note that replacements will not be MineDown-escaped; use {@link #escapeText(String)} to escape replacements
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @param replacements An ordered array of replacement strings to fill in placeholders with
|
||||
* @param replacements Ordered array of replacement strings to fill in placeholders with
|
||||
* @return An {@link Optional} containing the replacement-applied locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<String> getRawLocale(@NotNull String localeId, @NotNull String... replacements) {
|
||||
@@ -73,34 +80,45 @@ public class Locales {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a MineDown-formatted locale from the Locales file
|
||||
* Returns a MineDown-formatted locale from the locales file
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @return An {@link Optional} containing the formatted locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<MineDown> getLocale(@NotNull String localeId) {
|
||||
return getRawLocale(localeId).map(MineDown::new);
|
||||
return getRawLocale(localeId).map(this::format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a MineDown-formatted locale from the Locales file, with replacements applied
|
||||
* Returns a MineDown-formatted locale from the locales file, with replacements applied
|
||||
* <p>
|
||||
* Note that replacements will be MineDown-escaped before application
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @param replacements An ordered array of replacement strings to fill in placeholders with
|
||||
* @param replacements Ordered array of replacement strings to fill in placeholders with
|
||||
* @return An {@link Optional} containing the replacement-applied, formatted locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<MineDown> getLocale(@NotNull String localeId, @NotNull String... replacements) {
|
||||
return getRawLocale(localeId, Arrays.stream(replacements).map(Locales::escapeMineDown)
|
||||
.toArray(String[]::new)).map(MineDown::new);
|
||||
return getRawLocale(localeId, Arrays.stream(replacements).map(Locales::escapeText)
|
||||
.toArray(String[]::new)).map(this::format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a MineDown-formatted string
|
||||
*
|
||||
* @param text The text to format
|
||||
* @return A {@link MineDown} object containing the formatted text
|
||||
*/
|
||||
@NotNull
|
||||
public MineDown format(@NotNull String text) {
|
||||
return new MineDown(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply placeholder replacements to a raw locale
|
||||
*
|
||||
* @param rawLocale The raw, unparsed locale
|
||||
* @param replacements An ordered array of replacement strings to fill in placeholders with
|
||||
* @param replacements Ordered array of replacement strings to fill in placeholders with
|
||||
* @return the raw locale, with inserted placeholders
|
||||
*/
|
||||
@NotNull
|
||||
@@ -116,15 +134,12 @@ public class Locales {
|
||||
|
||||
/**
|
||||
* Escape a string from {@link MineDown} formatting for use in a MineDown-formatted locale
|
||||
* <p>
|
||||
* Although MineDown provides {@link MineDown#escape(String)}, that method fails to escape events
|
||||
* properly when using the escaped string in a replacement, so this is used instead
|
||||
*
|
||||
* @param string The string to escape
|
||||
* @return The escaped string
|
||||
*/
|
||||
@NotNull
|
||||
public static String escapeMineDown(@NotNull String string) {
|
||||
public static String escapeText(@NotNull String string) {
|
||||
final StringBuilder value = new StringBuilder();
|
||||
for (int i = 0; i < string.length(); ++i) {
|
||||
char c = string.charAt(i);
|
||||
@@ -140,21 +155,6 @@ public class Locales {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates a String to a specified length, and appends an ellipsis if it is longer than the specified length
|
||||
*
|
||||
* @param string The string to truncate
|
||||
* @param length The maximum length of the string
|
||||
* @return The truncated string
|
||||
*/
|
||||
@NotNull
|
||||
public static String truncate(@NotNull String string, int length) {
|
||||
if (string.length() > length) {
|
||||
return string.substring(0, length) + "…";
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base list options to use for a paginated chat list
|
||||
*
|
||||
@@ -185,10 +185,6 @@ public class Locales {
|
||||
.setSpaceBeforeFooter(false);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Locales() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the slot a system notification should be displayed in
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.config;
|
||||
|
||||
import de.exlll.configlib.Configuration;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Getter
|
||||
@Configuration
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class Server {
|
||||
|
||||
static final String CONFIG_HEADER = """
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ HuskSync - Server ID ┃
|
||||
┃ Developed by William278 ┃
|
||||
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
┣╸ This file should contain the ID of this server as defined in your proxy config.
|
||||
┗╸ If you join it using /server alpha, then set it to 'alpha' (case-sensitive)""";
|
||||
|
||||
private String name = getDefault();
|
||||
|
||||
@NotNull
|
||||
public static Server of(@NotNull String name) {
|
||||
return new Server(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a sensible default name for the server name property
|
||||
*/
|
||||
@NotNull
|
||||
private static String getDefault() {
|
||||
final String serverFolder = System.getProperty("user.dir");
|
||||
return serverFolder == null ? "server" : Path.of(serverFolder).getFileName().toString().trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@NotNull Object other) {
|
||||
// If the name of this server matches another, the servers are the same.
|
||||
if (other instanceof Server server) {
|
||||
return server.getName().equalsIgnoreCase(this.getName());
|
||||
}
|
||||
return super.equals(other);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,398 +19,281 @@
|
||||
|
||||
package net.william278.husksync.config;
|
||||
|
||||
import net.william278.annotaml.YamlComment;
|
||||
import net.william278.annotaml.YamlFile;
|
||||
import net.william278.annotaml.YamlKey;
|
||||
import com.google.common.collect.Lists;
|
||||
import de.exlll.configlib.Comment;
|
||||
import de.exlll.configlib.Configuration;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.listener.EventListener;
|
||||
import net.william278.husksync.sync.DataSyncer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Plugin settings, read from config.yml
|
||||
*/
|
||||
@YamlFile(header = """
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ HuskSync Config ┃
|
||||
┃ Developed by William278 ┃
|
||||
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
┣╸ Information: https://william278.net/project/husksync
|
||||
┣╸ Config Help: https://william278.net/docs/husksync/config-file/
|
||||
┗╸ Documentation: https://william278.net/docs/husksync""")
|
||||
@SuppressWarnings("FieldMayBeFinal")
|
||||
@Getter
|
||||
@Configuration
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class Settings {
|
||||
|
||||
// Top-level settings
|
||||
@YamlComment("Locale of the default language file to use. Docs: https://william278.net/docs/huskhomes/translations")
|
||||
@YamlKey("language")
|
||||
private String language = "en-gb";
|
||||
protected static final String CONFIG_HEADER = """
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ HuskSync Config ┃
|
||||
┃ Developed by William278 ┃
|
||||
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
┣╸ Information: https://william278.net/project/husksync
|
||||
┣╸ Config Help: https://william278.net/docs/husksync/config-file/
|
||||
┗╸ Documentation: https://william278.net/docs/husksync""";
|
||||
|
||||
@YamlComment("Whether to automatically check for plugin updates on startup")
|
||||
@YamlKey("check_for_updates")
|
||||
// Top-level settings
|
||||
@Comment({"Locale of the default language file to use.", "Docs: https://william278.net/docs/husksync/translations"})
|
||||
private String language = Locales.DEFAULT_LOCALE;
|
||||
|
||||
@Comment("Whether to automatically check for plugin updates on startup")
|
||||
private boolean checkForUpdates = true;
|
||||
|
||||
@YamlComment("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!")
|
||||
@YamlKey("cluster_id")
|
||||
private String clusterId = "";
|
||||
|
||||
@YamlComment("Enable development debug logging")
|
||||
@YamlKey("debug_logging")
|
||||
@Comment("Enable development debug logging")
|
||||
private boolean debugLogging = false;
|
||||
|
||||
@YamlComment("Whether to provide modern, rich TAB suggestions for commands (if available)")
|
||||
@YamlKey("brigadier_tab_completion")
|
||||
@Comment("Whether to provide modern, rich TAB suggestions for commands (if available)")
|
||||
private boolean brigadierTabCompletion = false;
|
||||
|
||||
@YamlComment("Whether to enable the Player Analytics hook. Docs: https://william278.net/docs/husksync/plan-hook")
|
||||
@YamlKey("enable_plan_hook")
|
||||
@Comment({"Whether to enable the Player Analytics hook.", "Docs: https://william278.net/docs/husksync/plan-hook"})
|
||||
private boolean enablePlanHook = true;
|
||||
|
||||
@Comment("Whether to cancel game event packets directly when handling locked players if ProtocolLib is installed")
|
||||
private boolean cancelPackets = true;
|
||||
|
||||
|
||||
// Database settings
|
||||
@YamlComment("Type of database to use (MYSQL, MARIADB)")
|
||||
@YamlKey("database.type")
|
||||
private Database.Type databaseType = Database.Type.MYSQL;
|
||||
@Comment("Database settings")
|
||||
private DatabaseSettings database = new DatabaseSettings();
|
||||
|
||||
@YamlComment("Specify credentials here for your MYSQL or MARIADB database")
|
||||
@YamlKey("database.credentials.host")
|
||||
private String mySqlHost = "localhost";
|
||||
@Getter
|
||||
@Configuration
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class DatabaseSettings {
|
||||
|
||||
@YamlKey("database.credentials.port")
|
||||
private int mySqlPort = 3306;
|
||||
@Comment("Type of database to use (MYSQL, MARIADB, POSTGRES, MONGO)")
|
||||
private Database.Type type = Database.Type.MYSQL;
|
||||
|
||||
@YamlKey("database.credentials.database")
|
||||
private String mySqlDatabase = "HuskSync";
|
||||
@Comment("Specify credentials here for your MYSQL, MARIADB, POSTGRES OR MONGO database")
|
||||
private DatabaseCredentials credentials = new DatabaseCredentials();
|
||||
|
||||
@YamlKey("database.credentials.username")
|
||||
private String mySqlUsername = "root";
|
||||
@Getter
|
||||
@Configuration
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class DatabaseCredentials {
|
||||
private String host = "localhost";
|
||||
private int port = 3306;
|
||||
private String database = "HuskSync";
|
||||
private String username = "root";
|
||||
private String password = "pa55w0rd";
|
||||
@Comment("Only change this if you're using MARIADB or POSTGRES")
|
||||
private String parameters = String.join("&",
|
||||
"?autoReconnect=true", "useSSL=false",
|
||||
"useUnicode=true", "characterEncoding=UTF-8");
|
||||
}
|
||||
|
||||
@YamlKey("database.credentials.password")
|
||||
private String mySqlPassword = "pa55w0rd";
|
||||
@Comment("MYSQL, MARIADB, POSTGRES database Hikari connection pool properties. Don't modify this unless you know what you're doing!")
|
||||
private PoolSettings connectionPool = new PoolSettings();
|
||||
|
||||
@YamlKey("database.credentials.parameters")
|
||||
private String mySqlConnectionParameters = "?autoReconnect=true"
|
||||
+ "&useSSL=false"
|
||||
+ "&useUnicode=true"
|
||||
+ "&characterEncoding=UTF-8";
|
||||
@Getter
|
||||
@Configuration
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class PoolSettings {
|
||||
private int maximumPoolSize = 10;
|
||||
private int minimumIdle = 10;
|
||||
private long maximumLifetime = 1800000;
|
||||
private long keepaliveTime = 0;
|
||||
private long connectionTimeout = 5000;
|
||||
}
|
||||
|
||||
@YamlComment("MYSQL / MARIADB database Hikari connection pool properties. "
|
||||
+ "Don't modify this unless you know what you're doing!")
|
||||
@YamlKey("database.connection_pool.maximum_pool_size")
|
||||
private int mySqlConnectionPoolSize = 10;
|
||||
@Comment("Advanced MongoDB settings. Don't modify unless you know what you're doing!")
|
||||
private MongoSettings mongoSettings = new MongoSettings();
|
||||
|
||||
@YamlKey("database.connection_pool.minimum_idle")
|
||||
private int mySqlConnectionPoolIdle = 10;
|
||||
@Getter
|
||||
@Configuration
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class MongoSettings {
|
||||
private boolean usingAtlas = false;
|
||||
private String parameters = String.join("&",
|
||||
"?retryWrites=true", "w=majority",
|
||||
"authSource=HuskSync");
|
||||
}
|
||||
|
||||
@YamlKey("database.connection_pool.maximum_lifetime")
|
||||
private long mySqlConnectionPoolLifetime = 1800000;
|
||||
@Comment("Names of tables to use on your database. Don't modify this unless you know what you're doing!")
|
||||
@Getter(AccessLevel.NONE)
|
||||
private Map<String, String> tableNames = Database.TableName.getDefaults();
|
||||
|
||||
@YamlKey("database.connection_pool.keepalive_time")
|
||||
private long mySqlConnectionPoolKeepAlive = 0;
|
||||
@NotNull
|
||||
public String getTableName(@NotNull Database.TableName tableName) {
|
||||
return tableNames.getOrDefault(tableName.name().toLowerCase(Locale.ENGLISH), tableName.getDefaultName());
|
||||
}
|
||||
}
|
||||
|
||||
@YamlKey("database.connection_pool.connection_timeout")
|
||||
private long mySqlConnectionPoolTimeout = 5000;
|
||||
// 𝓡𝓮𝓭𝓲𝓼 settings
|
||||
@Comment("Redis settings")
|
||||
private RedisSettings redis = new RedisSettings();
|
||||
|
||||
@YamlComment("Names of tables to use on your database. Don't modify this unless you know what you're doing!")
|
||||
@YamlKey("database.table_names")
|
||||
private Map<String, String> tableNames = TableName.getDefaults();
|
||||
@Getter
|
||||
@Configuration
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class RedisSettings {
|
||||
|
||||
@Comment("Specify the credentials of your Redis database here. Set \"password\" to '' if you don't have one")
|
||||
private RedisCredentials credentials = new RedisCredentials();
|
||||
|
||||
// Redis settings
|
||||
@YamlComment("Specify the credentials of your Redis database here. Set \"password\" to '' if you don't have one")
|
||||
@YamlKey("redis.credentials.host")
|
||||
private String redisHost = "localhost";
|
||||
@Getter
|
||||
@Configuration
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class RedisCredentials {
|
||||
private String host = "localhost";
|
||||
private int port = 6379;
|
||||
private String password = "";
|
||||
private boolean useSsl = false;
|
||||
}
|
||||
|
||||
@YamlKey("redis.credentials.port")
|
||||
private int redisPort = 6379;
|
||||
@Comment("Options for if you're using Redis sentinel. Don't modify this unless you know what you're doing!")
|
||||
private RedisSentinel sentinel = new RedisSentinel();
|
||||
|
||||
@YamlKey("redis.credentials.password")
|
||||
private String redisPassword = "";
|
||||
|
||||
@YamlKey("redis.use_ssl")
|
||||
private boolean redisUseSsl = false;
|
||||
@Getter
|
||||
@Configuration
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class RedisSentinel {
|
||||
@Comment("The master set name for the Redis sentinel.")
|
||||
private String master = "";
|
||||
@Comment("List of host:port pairs")
|
||||
private List<String> nodes = Lists.newArrayList();
|
||||
private String password = "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Synchronization settings
|
||||
@YamlComment("The number of data snapshot backups that should be kept at once per user")
|
||||
@YamlKey("synchronization.max_user_data_snapshots")
|
||||
private int maxUserDataSnapshots = 16;
|
||||
@Comment("Redis settings")
|
||||
private SynchronizationSettings synchronization = new SynchronizationSettings();
|
||||
|
||||
@YamlComment("Number of hours between new snapshots being saved as backups (Use \"0\" to backup all snapshots)")
|
||||
@YamlKey("synchronization.snapshot_backup_frequency")
|
||||
private int snapshotBackupFrequency = 4;
|
||||
@Getter
|
||||
@Configuration
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class SynchronizationSettings {
|
||||
|
||||
@YamlComment("List of save cause IDs for which a snapshot will be automatically pinned (so it won't be rotated)."
|
||||
+ " Docs: https://william278.net/docs/husksync/data-rotation#save-causes")
|
||||
@YamlKey("synchronization.auto_pinned_save_causes")
|
||||
private List<String> autoPinnedSaveCauses = List.of(
|
||||
DataSnapshot.SaveCause.INVENTORY_COMMAND.name(),
|
||||
DataSnapshot.SaveCause.ENDERCHEST_COMMAND.name(),
|
||||
DataSnapshot.SaveCause.BACKUP_RESTORE.name(),
|
||||
DataSnapshot.SaveCause.CONVERTED_FROM_V2.name(),
|
||||
DataSnapshot.SaveCause.LEGACY_MIGRATION.name(),
|
||||
DataSnapshot.SaveCause.MPDB_MIGRATION.name()
|
||||
);
|
||||
@Comment({"The data synchronization mode to use (LOCKSTEP or DELAY). LOCKSTEP is recommended for most networks.",
|
||||
"Docs: https://william278.net/docs/husksync/sync-modes"})
|
||||
private DataSyncer.Mode mode = DataSyncer.Mode.LOCKSTEP;
|
||||
|
||||
@YamlComment("Whether to create a snapshot for users on a world when the server saves that world")
|
||||
@YamlKey("synchronization.save_on_world_save")
|
||||
private boolean saveOnWorldSave = true;
|
||||
@Comment("The number of data snapshot backups that should be kept at once per user")
|
||||
private int maxUserDataSnapshots = 16;
|
||||
|
||||
@YamlComment("Whether to create a snapshot for users when they die (containing their death drops)")
|
||||
@YamlKey("synchronization.save_on_death")
|
||||
private boolean saveOnDeath = false;
|
||||
@Comment("Number of hours between new snapshots being saved as backups (Use \"0\" to backup all snapshots)")
|
||||
private int snapshotBackupFrequency = 4;
|
||||
|
||||
@YamlComment("Whether to save empty death drops for users when they die")
|
||||
@YamlKey("synchronization.save_empty_drops_on_death")
|
||||
private boolean saveEmptyDropsOnDeath = true;
|
||||
@Comment({"List of save cause IDs for which a snapshot will be automatically pinned (so it won't be rotated).",
|
||||
"Docs: https://william278.net/docs/husksync/data-rotation#save-causes"})
|
||||
@Getter(AccessLevel.NONE)
|
||||
private List<String> autoPinnedSaveCauses = List.of(
|
||||
DataSnapshot.SaveCause.INVENTORY_COMMAND.name(),
|
||||
DataSnapshot.SaveCause.ENDERCHEST_COMMAND.name(),
|
||||
DataSnapshot.SaveCause.BACKUP_RESTORE.name(),
|
||||
DataSnapshot.SaveCause.LEGACY_MIGRATION.name(),
|
||||
DataSnapshot.SaveCause.MPDB_MIGRATION.name()
|
||||
);
|
||||
|
||||
@YamlComment("Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing")
|
||||
@YamlKey("synchronization.compress_data")
|
||||
private boolean compressData = true;
|
||||
@Comment("Whether to create a snapshot for users on a world when the server saves that world")
|
||||
private boolean saveOnWorldSave = true;
|
||||
|
||||
@YamlComment("Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)")
|
||||
@YamlKey("synchronization.notification_display_slot")
|
||||
private Locales.NotificationSlot notificationSlot = Locales.NotificationSlot.ACTION_BAR;
|
||||
@Comment("Configuration for how and when to sync player data when they die")
|
||||
private SaveOnDeathSettings saveOnDeath = new SaveOnDeathSettings();
|
||||
|
||||
@YamlComment("(Experimental) Persist Cartography Table locked maps to let them be viewed on any server")
|
||||
@YamlKey("synchronization.persist_locked_maps")
|
||||
private boolean persistLockedMaps = true;
|
||||
@Getter
|
||||
@Configuration
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class SaveOnDeathSettings {
|
||||
@Comment("Whether to create a snapshot for users when they die (containing their death drops)")
|
||||
private boolean enabled = false;
|
||||
|
||||
@YamlComment("Whether to synchronize player max health (requires health syncing to be enabled)")
|
||||
@YamlKey("synchronization.synchronize_max_health")
|
||||
private boolean synchronizeMaxHealth = true;
|
||||
@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.")
|
||||
private DeathItemsMode itemsToSave = DeathItemsMode.DROPS;
|
||||
|
||||
@YamlComment("Whether dead players who log out and log in to a different server should have their items saved. "
|
||||
+ "You may need to modify this if you're using the keepInventory gamerule.")
|
||||
@YamlKey("synchronization.synchronize_dead_players_changing_server")
|
||||
private boolean synchronizeDeadPlayersChangingServer = true;
|
||||
@Comment("Should a death snapshot still be created even if the items to save on the player's death are empty?")
|
||||
private boolean saveEmptyItems = true;
|
||||
|
||||
@YamlComment("How long, in milliseconds, this server should wait for a response from the redis server before "
|
||||
+ "pulling data from the database instead (i.e., if the user did not change servers).")
|
||||
@YamlKey("synchronization.network_latency_milliseconds")
|
||||
private int networkLatencyMilliseconds = 500;
|
||||
@Comment("Whether dead players who log out and log in to a different server should have their items saved.")
|
||||
private boolean syncDeadPlayersChangingServer = true;
|
||||
|
||||
@YamlComment("Which data types to synchronize (Docs: https://william278.net/docs/husksync/sync-features)")
|
||||
@YamlKey("synchronization.features")
|
||||
private Map<String, Boolean> synchronizationFeatures = Identifier.getConfigMap();
|
||||
|
||||
@YamlComment("Commands which should be blocked before a player has finished syncing (Use * to block all commands)")
|
||||
@YamlKey("synchronization.blacklisted_commands_while_locked")
|
||||
private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*"));
|
||||
|
||||
@YamlComment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts")
|
||||
@YamlKey("synchronization.event_priorities")
|
||||
private Map<String, String> syncEventPriorities = EventListener.ListenerType.getDefaults();
|
||||
|
||||
|
||||
// Zero-args constructor for instantiation via Annotaml
|
||||
@SuppressWarnings("unused")
|
||||
public Settings() {
|
||||
}
|
||||
|
||||
|
||||
@NotNull
|
||||
public String getLanguage() {
|
||||
return language;
|
||||
}
|
||||
|
||||
public boolean doCheckForUpdates() {
|
||||
return checkForUpdates;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getClusterId() {
|
||||
return clusterId;
|
||||
}
|
||||
|
||||
public boolean doDebugLogging() {
|
||||
return debugLogging;
|
||||
}
|
||||
|
||||
public boolean doBrigadierTabCompletion() {
|
||||
return brigadierTabCompletion;
|
||||
}
|
||||
|
||||
public boolean usePlanHook() {
|
||||
return enablePlanHook;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Database.Type getDatabaseType() {
|
||||
return databaseType;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getMySqlHost() {
|
||||
return mySqlHost;
|
||||
}
|
||||
|
||||
public int getMySqlPort() {
|
||||
return mySqlPort;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getMySqlDatabase() {
|
||||
return mySqlDatabase;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getMySqlUsername() {
|
||||
return mySqlUsername;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getMySqlPassword() {
|
||||
return mySqlPassword;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getMySqlConnectionParameters() {
|
||||
return mySqlConnectionParameters;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getTableName(@NotNull TableName tableName) {
|
||||
return tableNames.getOrDefault(tableName.name().toLowerCase(Locale.ENGLISH), tableName.defaultName);
|
||||
}
|
||||
|
||||
public int getMySqlConnectionPoolSize() {
|
||||
return mySqlConnectionPoolSize;
|
||||
}
|
||||
|
||||
public int getMySqlConnectionPoolIdle() {
|
||||
return mySqlConnectionPoolIdle;
|
||||
}
|
||||
|
||||
public long getMySqlConnectionPoolLifetime() {
|
||||
return mySqlConnectionPoolLifetime;
|
||||
}
|
||||
|
||||
public long getMySqlConnectionPoolKeepAlive() {
|
||||
return mySqlConnectionPoolKeepAlive;
|
||||
}
|
||||
|
||||
public long getMySqlConnectionPoolTimeout() {
|
||||
return mySqlConnectionPoolTimeout;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getRedisHost() {
|
||||
return redisHost;
|
||||
}
|
||||
|
||||
public int getRedisPort() {
|
||||
return redisPort;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getRedisPassword() {
|
||||
return redisPassword;
|
||||
}
|
||||
|
||||
public boolean redisUseSsl() {
|
||||
return redisUseSsl;
|
||||
}
|
||||
|
||||
public int getMaxUserDataSnapshots() {
|
||||
return maxUserDataSnapshots;
|
||||
}
|
||||
|
||||
public int getBackupFrequency() {
|
||||
return snapshotBackupFrequency;
|
||||
}
|
||||
|
||||
public boolean doSaveOnWorldSave() {
|
||||
return saveOnWorldSave;
|
||||
}
|
||||
|
||||
public boolean doSaveOnDeath() {
|
||||
return saveOnDeath;
|
||||
}
|
||||
|
||||
public boolean doSaveEmptyDropsOnDeath() {
|
||||
return saveEmptyDropsOnDeath;
|
||||
}
|
||||
|
||||
public boolean doCompressData() {
|
||||
return compressData;
|
||||
}
|
||||
|
||||
public boolean doAutoPin(@NotNull DataSnapshot.SaveCause cause) {
|
||||
return autoPinnedSaveCauses.contains(cause.name());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Locales.NotificationSlot getNotificationDisplaySlot() {
|
||||
return notificationSlot;
|
||||
}
|
||||
|
||||
public boolean doPersistLockedMaps() {
|
||||
return persistLockedMaps;
|
||||
}
|
||||
|
||||
public boolean doSynchronizeDeadPlayersChangingServer() {
|
||||
return synchronizeDeadPlayersChangingServer;
|
||||
}
|
||||
|
||||
public boolean doSynchronizeMaxHealth() {
|
||||
return synchronizeMaxHealth;
|
||||
}
|
||||
|
||||
public int getNetworkLatencyMilliseconds() {
|
||||
return networkLatencyMilliseconds;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Map<String, Boolean> getSynchronizationFeatures() {
|
||||
return synchronizationFeatures;
|
||||
}
|
||||
|
||||
public boolean isSyncFeatureEnabled(@NotNull Identifier id) {
|
||||
return id.isCustom() || getSynchronizationFeatures().getOrDefault(id.getKeyValue(), id.isEnabledByDefault());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<String> getBlacklistedCommandsWhileLocked() {
|
||||
return blacklistedCommandsWhileLocked;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) {
|
||||
try {
|
||||
return EventListener.Priority.valueOf(syncEventPriorities.get(type.name().toLowerCase(Locale.ENGLISH)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return EventListener.Priority.NORMAL;
|
||||
/**
|
||||
* Represents the mode of saving items on death
|
||||
*/
|
||||
public enum DeathItemsMode {
|
||||
DROPS,
|
||||
ITEMS_TO_KEEP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the names of tables in the database
|
||||
*/
|
||||
public enum TableName {
|
||||
USERS("husksync_users"),
|
||||
USER_DATA("husksync_user_data");
|
||||
@Comment("Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing")
|
||||
private boolean compressData = true;
|
||||
|
||||
private final String defaultName;
|
||||
@Comment("Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)")
|
||||
private Locales.NotificationSlot notificationDisplaySlot = Locales.NotificationSlot.ACTION_BAR;
|
||||
|
||||
TableName(@NotNull String defaultName) {
|
||||
this.defaultName = defaultName;
|
||||
@Comment("Persist maps locked in a Cartography Table to let them be viewed on any server")
|
||||
private boolean persistLockedMaps = true;
|
||||
|
||||
@Comment("If using the DELAY sync method, how long should this server listen for Redis key data updates before "
|
||||
+ "pulling data from the database instead (i.e., if the user did not change servers).")
|
||||
private int networkLatencyMilliseconds = 500;
|
||||
|
||||
@Comment({"Which data types to synchronize.", "Docs: https://william278.net/docs/husksync/sync-features"})
|
||||
@Getter(AccessLevel.NONE)
|
||||
private Map<String, Boolean> features = Identifier.getConfigMap();
|
||||
|
||||
@Comment("Commands which should be blocked before a player has finished syncing (Use * to block all commands)")
|
||||
private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*"));
|
||||
|
||||
@Comment({"For attribute syncing, which attributes should be ignored/skipped when syncing",
|
||||
"(e.g. ['minecraft:generic.max_health', 'minecraft:generic.attack_damage'])"})
|
||||
@Getter(AccessLevel.NONE)
|
||||
private List<String> ignoredAttributes = new ArrayList<>(List.of(""));
|
||||
|
||||
@Comment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts")
|
||||
@Getter(AccessLevel.NONE)
|
||||
private Map<String, String> eventPriorities = EventListener.ListenerType.getDefaults();
|
||||
|
||||
public boolean doAutoPin(@NotNull DataSnapshot.SaveCause cause) {
|
||||
return autoPinnedSaveCauses.contains(cause.name());
|
||||
}
|
||||
|
||||
public boolean isFeatureEnabled(@NotNull Identifier id) {
|
||||
return id.isCustom() || features.getOrDefault(id.getKeyValue(), id.isEnabledByDefault());
|
||||
}
|
||||
|
||||
public boolean isIgnoredAttribute(@NotNull String attribute) {
|
||||
return ignoredAttributes.contains(attribute);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map.Entry<String, String> toEntry() {
|
||||
return Map.entry(name().toLowerCase(Locale.ENGLISH), defaultName);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@NotNull
|
||||
private static Map<String, String> getDefaults() {
|
||||
return Map.ofEntries(Arrays.stream(values())
|
||||
.map(TableName::toEntry)
|
||||
.toArray(Map.Entry[]::new));
|
||||
public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) {
|
||||
try {
|
||||
return EventListener.Priority.valueOf(eventPriorities.get(type.name().toLowerCase(Locale.ENGLISH)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return EventListener.Priority.NORMAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -51,8 +53,8 @@ public interface Data {
|
||||
*/
|
||||
interface Items extends Data {
|
||||
|
||||
@NotNull
|
||||
Stack[] getStack();
|
||||
@Nullable
|
||||
Stack @NotNull [] getStack();
|
||||
|
||||
default int getSlotCount() {
|
||||
return getStack().length;
|
||||
@@ -76,6 +78,9 @@ public interface Data {
|
||||
*/
|
||||
interface Inventory extends Items {
|
||||
|
||||
String ITEMS_TAG = "items";
|
||||
String HELD_ITEM_SLOT_TAG = "held_item_slot";
|
||||
|
||||
int getHeldItemSlot();
|
||||
|
||||
void setHeldItemSlot(int heldItemSlot) throws IllegalArgumentException;
|
||||
@@ -283,15 +288,97 @@ public interface Data {
|
||||
|
||||
void setHealth(double health);
|
||||
|
||||
double getMaxHealth();
|
||||
/**
|
||||
* @deprecated Use {@link Attributes#getMaxHealth()} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
default double getMaxHealth() {
|
||||
return getHealth();
|
||||
}
|
||||
|
||||
void setMaxHealth(double maxHealth);
|
||||
/**
|
||||
* @deprecated Use {@link Attributes#setMaxHealth(double)} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
default void setMaxHealth(double maxHealth) {
|
||||
}
|
||||
|
||||
double getHealthScale();
|
||||
|
||||
void setHealthScale(double healthScale);
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding player attribute data
|
||||
*/
|
||||
interface Attributes extends Data {
|
||||
|
||||
Key MAX_HEALTH_KEY = Key.key("generic.max_health");
|
||||
|
||||
List<Attribute> getAttributes();
|
||||
|
||||
record Attribute(
|
||||
@NotNull String name,
|
||||
double baseValue,
|
||||
@NotNull Set<Modifier> modifiers
|
||||
) {
|
||||
|
||||
public double getValue() {
|
||||
double value = baseValue;
|
||||
for (Modifier modifier : modifiers) {
|
||||
value = modifier.modify(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
record Modifier(
|
||||
@NotNull UUID uuid,
|
||||
@NotNull String name,
|
||||
double amount,
|
||||
@SerializedName("operation") int operationType,
|
||||
@SerializedName("equipment_slot") int equipmentSlot
|
||||
) {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof Modifier modifier && modifier.uuid.equals(uuid);
|
||||
}
|
||||
|
||||
public double modify(double value) {
|
||||
return switch (operationType) {
|
||||
case 0 -> value + amount;
|
||||
case 1 -> value * amount;
|
||||
case 2 -> value * (1 + amount);
|
||||
default -> value;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
default Optional<Attribute> getAttribute(@NotNull Key key) {
|
||||
return getAttributes().stream()
|
||||
.filter(attribute -> attribute.name().equals(key.asString()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
default void removeAttribute(@NotNull Key key) {
|
||||
getAttributes().removeIf(attribute -> attribute.name().equals(key.asString()));
|
||||
}
|
||||
|
||||
default double getMaxHealth() {
|
||||
return getAttribute(MAX_HEALTH_KEY)
|
||||
.map(Attribute::getValue)
|
||||
.orElse(20.0);
|
||||
}
|
||||
|
||||
default void setMaxHealth(double maxHealth) {
|
||||
removeAttribute(MAX_HEALTH_KEY);
|
||||
getAttributes().add(new Attribute(MAX_HEALTH_KEY.asString(), maxHealth, Sets.newHashSet()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
@@ -341,12 +428,7 @@ public interface Data {
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
* <li>Game mode</li>
|
||||
* <li>Allow flight</li>
|
||||
* <li>Is flying</li>
|
||||
* </ul>
|
||||
* Data container holding data for the player's current game mode
|
||||
*/
|
||||
interface GameMode extends Data {
|
||||
|
||||
@@ -355,13 +437,65 @@ public interface Data {
|
||||
|
||||
void setGameMode(@NotNull String gameMode);
|
||||
|
||||
boolean getAllowFlight();
|
||||
/**
|
||||
* Get if the player can fly.
|
||||
*
|
||||
* @return {@code false} since v3.5
|
||||
* @deprecated Moved to its own data type. This will always return {@code false}.
|
||||
* Use {@link FlightStatus#isAllowFlight()} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
default boolean getAllowFlight() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if the player can fly.
|
||||
*
|
||||
* @deprecated Moved to its own data type.
|
||||
* Use {@link FlightStatus#setAllowFlight(boolean)} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
default void setAllowFlight(boolean allowFlight) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if the player is flying.
|
||||
*
|
||||
* @return {@code false} since v3.5
|
||||
* @deprecated Moved to its own data type. This will always return {@code false}.
|
||||
* Use {@link FlightStatus#isFlying()} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
default boolean getIsFlying() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if the player is flying.
|
||||
*
|
||||
* @deprecated Moved to its own data type.
|
||||
* Use {@link FlightStatus#setFlying(boolean)} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
default void setIsFlying(boolean isFlying) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for the player's flight status
|
||||
*
|
||||
* @since 3.5
|
||||
*/
|
||||
interface FlightStatus extends Data {
|
||||
boolean isAllowFlight();
|
||||
|
||||
void setAllowFlight(boolean allowFlight);
|
||||
|
||||
boolean getIsFlying();
|
||||
boolean isFlying();
|
||||
|
||||
void setIsFlying(boolean isFlying);
|
||||
void setFlying(boolean isFlying);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* An exception related to {@link DataSnapshot} formatting, thrown if an exception occurs when unpacking a snapshot
|
||||
*/
|
||||
@Getter
|
||||
public class DataException extends IllegalStateException {
|
||||
|
||||
private final Reason reason;
|
||||
|
||||
private DataException(@NotNull DataException.Reason reason, @NotNull DataSnapshot data, @NotNull HuskSync plugin) {
|
||||
super(reason.getMessage(plugin, data));
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reasons why {@link DataException}s were thrown
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public enum Reason {
|
||||
INVALID_MINECRAFT_VERSION((plugin, snapshot) -> String.format("The Minecraft version of the snapshot (%s) is " +
|
||||
"newer than the server's version (%s). Ensure each server is on the same version of Minecraft.",
|
||||
snapshot.getMinecraftVersion(), plugin.getMinecraftVersion())),
|
||||
INVALID_FORMAT_VERSION((plugin, snapshot) -> String.format("The format version of the snapshot (%s) is newer " +
|
||||
"than the server's version (%s). Ensure each server is running the same version of HuskSync.",
|
||||
snapshot.getFormatVersion(), DataSnapshot.CURRENT_FORMAT_VERSION)),
|
||||
INVALID_PLATFORM_TYPE((plugin, snapshot) -> String.format("The platform type of the snapshot (%s) does " +
|
||||
"not match the server's platform type (%s). Ensure each server has the same platform type.",
|
||||
snapshot.getPlatformType(), plugin.getPlatformType())),
|
||||
NO_LEGACY_CONVERTER((plugin, snapshot) -> String.format("No legacy converter to convert format version: %s",
|
||||
snapshot.getFormatVersion()));
|
||||
|
||||
private final BiFunction<HuskSync, DataSnapshot, String> exception;
|
||||
|
||||
@NotNull
|
||||
String getMessage(@NotNull HuskSync plugin, @NotNull DataSnapshot data) {
|
||||
return exception.apply(plugin, data);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
DataException toException(@NotNull DataSnapshot data, @NotNull HuskSync plugin) {
|
||||
return new DataException(this, data, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -110,6 +110,15 @@ public interface DataHolder {
|
||||
getData().put(Identifier.HUNGER, hunger);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Attributes> getAttributes() {
|
||||
return Optional.ofNullable((Data.Attributes) getData().get(Identifier.ATTRIBUTES));
|
||||
}
|
||||
|
||||
default void setAttributes(@NotNull Data.Attributes attributes) {
|
||||
getData().put(Identifier.ATTRIBUTES, attributes);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Experience> getExperience() {
|
||||
return Optional.ofNullable((Data.Experience) getData().get(Identifier.EXPERIENCE));
|
||||
@@ -128,6 +137,15 @@ public interface DataHolder {
|
||||
getData().put(Identifier.GAME_MODE, gameMode);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.FlightStatus> getFlightStatus() {
|
||||
return Optional.ofNullable((Data.FlightStatus) getData().get(Identifier.FLIGHT_STATUS));
|
||||
}
|
||||
|
||||
default void setFlightStatus(@NotNull Data.FlightStatus flightStatus) {
|
||||
getData().put(Identifier.FLIGHT_STATUS, flightStatus);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.PersistentData> getPersistentData() {
|
||||
return Optional.ofNullable((Data.PersistentData) getData().get(Identifier.PERSISTENT_DATA));
|
||||
|
||||
@@ -19,14 +19,20 @@
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import org.apache.commons.text.WordUtils;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -41,13 +47,15 @@ import java.util.stream.Collectors;
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
@SuppressWarnings({"LombokSetterMayBeUsed", "LombokGetterMayBeUsed"})
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class DataSnapshot {
|
||||
|
||||
/*
|
||||
* Current version of the snapshot data format.
|
||||
* HuskSync v3.0 uses v4; HuskSync v2.0 uses v1-v3
|
||||
* HuskSync v3.1 uses v5, v3.0 uses v4; v2.0 uses v1-v3
|
||||
*/
|
||||
protected static final int CURRENT_FORMAT_VERSION = 4;
|
||||
protected static final int CURRENT_FORMAT_VERSION = 5;
|
||||
|
||||
@SerializedName("id")
|
||||
protected UUID id;
|
||||
@@ -59,7 +67,10 @@ public class DataSnapshot {
|
||||
protected OffsetDateTime timestamp;
|
||||
|
||||
@SerializedName("save_cause")
|
||||
protected SaveCause saveCause;
|
||||
protected String saveCause;
|
||||
|
||||
@SerializedName("server_name")
|
||||
protected String serverName;
|
||||
|
||||
@SerializedName("minecraft_version")
|
||||
protected String minecraftVersion;
|
||||
@@ -73,23 +84,25 @@ public class DataSnapshot {
|
||||
@SerializedName("data")
|
||||
protected Map<String, String> data;
|
||||
|
||||
// If the snapshot is invalid, this will be set to the validation exception
|
||||
@Nullable
|
||||
@Expose(serialize = false, deserialize = false)
|
||||
transient DataException.Reason exception = null;
|
||||
|
||||
private DataSnapshot(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull Map<String, String> data,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
this.id = id;
|
||||
this.pinned = pinned;
|
||||
this.timestamp = timestamp;
|
||||
this.saveCause = saveCause;
|
||||
this.serverName = serverName;
|
||||
this.data = data;
|
||||
this.minecraftVersion = minecraftVersion.toStringWithoutMetadata();
|
||||
this.platformType = platformType;
|
||||
this.formatVersion = formatVersion;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private DataSnapshot() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public static DataSnapshot.Builder builder(@NotNull HuskSync plugin) {
|
||||
@@ -100,37 +113,25 @@ public class DataSnapshot {
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public static DataSnapshot.Packed deserialize(@NotNull HuskSync plugin, byte[] data, @Nullable UUID id,
|
||||
@Nullable OffsetDateTime timestamp) throws IllegalStateException {
|
||||
@Nullable OffsetDateTime timestamp) {
|
||||
final DataSnapshot.Packed snapshot = plugin.getDataAdapter().fromBytes(data, DataSnapshot.Packed.class);
|
||||
if (snapshot.getMinecraftVersion().compareTo(plugin.getMinecraftVersion()) > 0) {
|
||||
throw new IllegalStateException(String.format("Cannot set data for user because the Minecraft version of " +
|
||||
"their user data (%s) is newer than the server's Minecraft version (%s)." +
|
||||
"Please ensure each server is running the same version of Minecraft.",
|
||||
snapshot.getMinecraftVersion(), plugin.getMinecraftVersion()));
|
||||
return snapshot.invalid(DataException.Reason.INVALID_MINECRAFT_VERSION);
|
||||
}
|
||||
if (snapshot.getFormatVersion() > CURRENT_FORMAT_VERSION) {
|
||||
throw new IllegalStateException(String.format("Cannot set data for user because the format version of " +
|
||||
"their user data (%s) is newer than the current format version (%s). " +
|
||||
"Please ensure each server is running the latest version of HuskSync.",
|
||||
snapshot.getFormatVersion(), CURRENT_FORMAT_VERSION));
|
||||
return snapshot.invalid(DataException.Reason.INVALID_FORMAT_VERSION);
|
||||
}
|
||||
if (snapshot.getFormatVersion() < CURRENT_FORMAT_VERSION) {
|
||||
if (snapshot.getFormatVersion() < 4) {
|
||||
if (plugin.getLegacyConverter().isPresent()) {
|
||||
return plugin.getLegacyConverter().get().convert(
|
||||
data,
|
||||
Objects.requireNonNull(id, "Attempted legacy conversion with null UUID!"),
|
||||
data, Objects.requireNonNull(id, "Attempted legacy conversion with null UUID!"),
|
||||
Objects.requireNonNull(timestamp, "Attempted legacy conversion with null timestamp!")
|
||||
);
|
||||
}
|
||||
throw new IllegalStateException(String.format(
|
||||
"No legacy converter to convert format version: %s", snapshot.getFormatVersion()
|
||||
));
|
||||
return snapshot.invalid(DataException.Reason.NO_LEGACY_CONVERTER);
|
||||
}
|
||||
if (!snapshot.getPlatformType().equalsIgnoreCase(plugin.getPlatformType())) {
|
||||
throw new IllegalStateException(String.format("Cannot set data for user because the platform type of " +
|
||||
"their user data (%s) is different to the server platform type (%s). " +
|
||||
"Please ensure each server is running the same platform type.",
|
||||
snapshot.getPlatformType(), plugin.getPlatformType()));
|
||||
return snapshot.invalid(DataException.Reason.INVALID_PLATFORM_TYPE);
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
@@ -153,6 +154,17 @@ public class DataSnapshot {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>Internal use only</b> Set the ID of the snapshot
|
||||
*
|
||||
* @param id The snapshot ID
|
||||
* @since 3.0
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public void setId(@NotNull UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the short display ID of the snapshot
|
||||
*
|
||||
@@ -192,7 +204,20 @@ public class DataSnapshot {
|
||||
*/
|
||||
@NotNull
|
||||
public SaveCause getSaveCause() {
|
||||
return saveCause;
|
||||
return SaveCause.of(saveCause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the server the snapshot was created on.
|
||||
* <p>
|
||||
* Note that snapshots generated before v3.1 will return {@code "N/A"}
|
||||
*
|
||||
* @return The server name
|
||||
* @since 3.1
|
||||
*/
|
||||
@NotNull
|
||||
public String getServerName() {
|
||||
return Optional.ofNullable(serverName).orElse("N/A");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,8 +226,8 @@ public class DataSnapshot {
|
||||
* @param saveCause The {@link SaveCause data save cause} of the snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setSaveCause(SaveCause saveCause) {
|
||||
this.saveCause = saveCause;
|
||||
public void setSaveCause(@NotNull SaveCause saveCause) {
|
||||
this.saveCause = saveCause.name();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,16 +278,39 @@ public class DataSnapshot {
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class Packed extends DataSnapshot implements Adaptable {
|
||||
|
||||
protected Packed(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull Map<String, String> data,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
super(id, pinned, timestamp, saveCause, data, minecraftVersion, platformType, formatVersion);
|
||||
super(id, pinned, timestamp, saveCause, serverName, data, minecraftVersion, platformType, formatVersion);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private Packed() {
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
DataSnapshot.Packed invalid(@NotNull DataException.Reason reason) {
|
||||
this.exception = reason;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isInvalid() {
|
||||
return exception != null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getInvalidReason(@NotNull HuskSync plugin) {
|
||||
if (exception == null) {
|
||||
throw new IllegalStateException("Attempted to get an invalid reason for a valid snapshot!");
|
||||
}
|
||||
return exception.getMessage(plugin, this);
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
void validate(@NotNull HuskSync plugin) throws DataException {
|
||||
if (exception != null) {
|
||||
throw exception.toException(this, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
@@ -270,7 +318,7 @@ public class DataSnapshot {
|
||||
final Unpacked data = unpack(plugin);
|
||||
editor.accept(data);
|
||||
this.pinned = data.isPinned();
|
||||
this.saveCause = data.getSaveCause();
|
||||
this.saveCause = data.getSaveCause().name();
|
||||
this.data = data.serializeData(plugin);
|
||||
}
|
||||
|
||||
@@ -282,12 +330,11 @@ public class DataSnapshot {
|
||||
@NotNull
|
||||
public Packed copy() {
|
||||
return new Packed(
|
||||
UUID.randomUUID(), pinned, OffsetDateTime.now(), saveCause, data,
|
||||
getMinecraftVersion(), platformType, formatVersion
|
||||
UUID.randomUUID(), pinned, OffsetDateTime.now(), saveCause, serverName,
|
||||
data, getMinecraftVersion(), platformType, formatVersion
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public byte[] asBytes(@NotNull HuskSync plugin) throws DataAdapter.AdaptionException {
|
||||
return plugin.getDataAdapter().toBytes(this);
|
||||
@@ -305,9 +352,10 @@ public class DataSnapshot {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public DataSnapshot.Unpacked unpack(@NotNull HuskSync plugin) {
|
||||
public DataSnapshot.Unpacked unpack(@NotNull HuskSync plugin) throws DataException {
|
||||
this.validate(plugin);
|
||||
return new Unpacked(
|
||||
id, pinned, timestamp, saveCause, data,
|
||||
id, pinned, timestamp, saveCause, serverName, data,
|
||||
getMinecraftVersion(), platformType, formatVersion, plugin
|
||||
);
|
||||
}
|
||||
@@ -325,17 +373,17 @@ public class DataSnapshot {
|
||||
private final Map<Identifier, Data> deserialized;
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull Map<String, String> data,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion,
|
||||
@NotNull HuskSync plugin) {
|
||||
super(id, pinned, timestamp, saveCause, data, minecraftVersion, platformType, formatVersion);
|
||||
super(id, pinned, timestamp, saveCause, serverName, data, minecraftVersion, platformType, formatVersion);
|
||||
this.deserialized = deserializeData(plugin);
|
||||
}
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull Map<Identifier, Data> data,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<Identifier, Data> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
super(id, pinned, timestamp, saveCause, Map.of(), minecraftVersion, platformType, formatVersion);
|
||||
super(id, pinned, timestamp, saveCause, serverName, Map.of(), minecraftVersion, platformType, formatVersion);
|
||||
this.deserialized = data;
|
||||
}
|
||||
|
||||
@@ -384,7 +432,7 @@ public class DataSnapshot {
|
||||
@ApiStatus.Internal
|
||||
public DataSnapshot.Packed pack(@NotNull HuskSync plugin) {
|
||||
return new DataSnapshot.Packed(
|
||||
id, pinned, timestamp, saveCause, serializeData(plugin),
|
||||
id, pinned, timestamp, saveCause, serverName, serializeData(plugin),
|
||||
getMinecraftVersion(), platformType, formatVersion
|
||||
);
|
||||
}
|
||||
@@ -402,6 +450,7 @@ public class DataSnapshot {
|
||||
private final HuskSync plugin;
|
||||
private UUID id;
|
||||
private SaveCause saveCause;
|
||||
private String serverName;
|
||||
private boolean pinned;
|
||||
private OffsetDateTime timestamp;
|
||||
private final Map<Identifier, Data> data;
|
||||
@@ -409,9 +458,10 @@ public class DataSnapshot {
|
||||
private Builder(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
this.pinned = false;
|
||||
this.data = new HashMap<>();
|
||||
this.data = Maps.newHashMap();
|
||||
this.timestamp = OffsetDateTime.now();
|
||||
this.id = UUID.randomUUID();
|
||||
this.serverName = plugin.getServerName();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -441,6 +491,19 @@ public class DataSnapshot {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of the server where this snapshot was created
|
||||
*
|
||||
* @param serverName The server name
|
||||
* @return The builder
|
||||
* @since 3.1
|
||||
*/
|
||||
@NotNull
|
||||
public Builder serverName(@NotNull String serverName) {
|
||||
this.serverName = serverName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the data should be pinned
|
||||
*
|
||||
@@ -471,7 +534,10 @@ public class DataSnapshot {
|
||||
@NotNull
|
||||
public Builder timestamp(@NotNull OffsetDateTime timestamp) {
|
||||
if (timestamp.isAfter(OffsetDateTime.now())) {
|
||||
throw new IllegalArgumentException("Data snapshots cannot have a timestamp set in the future");
|
||||
throw new IllegalArgumentException("Data snapshots cannot have a timestamp set in the future! "
|
||||
+ "Make sure your database server time matches the server time.\n"
|
||||
+ "Current game server timestamp: " + OffsetDateTime.now() + " / "
|
||||
+ "Snapshot timestamp: " + timestamp);
|
||||
}
|
||||
this.timestamp = timestamp;
|
||||
return this;
|
||||
@@ -624,6 +690,21 @@ public class DataSnapshot {
|
||||
return data(Identifier.HUNGER, hunger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the attributes of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.ATTRIBUTES, attributes)}
|
||||
* </p>
|
||||
*
|
||||
* @param attributes The user's attributes
|
||||
* @return The builder
|
||||
* @since 3.5
|
||||
*/
|
||||
@NotNull
|
||||
public Builder attributes(@NotNull Data.Attributes attributes) {
|
||||
return data(Identifier.ATTRIBUTES, attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the experience of the snapshot
|
||||
* <p>
|
||||
@@ -654,6 +735,21 @@ public class DataSnapshot {
|
||||
return data(Identifier.GAME_MODE, gameMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the flight status of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.FLIGHT_STATUS, flightStatus)}
|
||||
* </p>
|
||||
*
|
||||
* @param flightStatus The flight status
|
||||
* @return The builder
|
||||
* @since 3.5
|
||||
*/
|
||||
@NotNull
|
||||
public Builder flightStatus(@NotNull Data.FlightStatus flightStatus) {
|
||||
return data(Identifier.FLIGHT_STATUS, flightStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the persistent data container of the snapshot
|
||||
* <p>
|
||||
@@ -683,9 +779,10 @@ public class DataSnapshot {
|
||||
}
|
||||
return new Unpacked(
|
||||
id,
|
||||
pinned || plugin.getSettings().doAutoPin(saveCause),
|
||||
pinned || plugin.getSettings().getSynchronization().doAutoPin(saveCause),
|
||||
timestamp,
|
||||
saveCause,
|
||||
saveCause.name(),
|
||||
serverName,
|
||||
data,
|
||||
plugin.getMinecraftVersion(),
|
||||
plugin.getPlatformType(),
|
||||
@@ -707,134 +804,267 @@ public class DataSnapshot {
|
||||
|
||||
}
|
||||
|
||||
public interface Cause {
|
||||
|
||||
@NotNull
|
||||
String name();
|
||||
|
||||
/**
|
||||
* Returns the capitalized display name of the cause.
|
||||
*
|
||||
* @return the cause display name
|
||||
*/
|
||||
@NotNull
|
||||
default String getDisplayName() {
|
||||
return WordUtils.capitalizeFully(name().replaceAll("_", " "));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the cause of a player data save.
|
||||
*
|
||||
* @implNote This enum is saved in the database.
|
||||
* A string wrapper, for identifying the cause of a player data save.
|
||||
* </p>
|
||||
* Cause names have a max length of 32 characters.
|
||||
*/
|
||||
public enum SaveCause {
|
||||
@Accessors(fluent = true)
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class SaveCause implements Cause {
|
||||
|
||||
/**
|
||||
* Indicates data saved when a player disconnected from the server (either to change servers, or to log off)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
DISCONNECT,
|
||||
public static final SaveCause DISCONNECT = of("DISCONNECT");
|
||||
|
||||
/**
|
||||
* Indicates data saved when the world saved
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
WORLD_SAVE,
|
||||
public static final SaveCause WORLD_SAVE = of("WORLD_SAVE");
|
||||
|
||||
/**
|
||||
* Indicates data saved when the user died
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
DEATH,
|
||||
public static final SaveCause DEATH = of("DEATH");
|
||||
|
||||
/**
|
||||
* Indicates data saved when the server shut down
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
SERVER_SHUTDOWN,
|
||||
public static final SaveCause SERVER_SHUTDOWN = of("SERVER_SHUTDOWN", false);
|
||||
|
||||
/**
|
||||
* Indicates data was saved by editing inventory contents via the {@code /inventory} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
INVENTORY_COMMAND,
|
||||
public static final SaveCause INVENTORY_COMMAND = of("INVENTORY_COMMAND");
|
||||
|
||||
/**
|
||||
* Indicates data was saved by editing Ender Chest contents via the {@code /enderchest} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
ENDERCHEST_COMMAND,
|
||||
public static final SaveCause ENDERCHEST_COMMAND = of("ENDERCHEST_COMMAND");
|
||||
|
||||
/**
|
||||
* Indicates data was saved by restoring it from a previous version
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
BACKUP_RESTORE,
|
||||
public static final SaveCause BACKUP_RESTORE = of("BACKUP_RESTORE");
|
||||
|
||||
/**
|
||||
* Indicates data was saved by an API call
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
API,
|
||||
public static final SaveCause API = of("API");
|
||||
|
||||
/**
|
||||
* Indicates data was saved from being imported from MySQLPlayerDataBridge
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
MPDB_MIGRATION,
|
||||
public static final SaveCause MPDB_MIGRATION = of("MPDB_MIGRATION", false);
|
||||
|
||||
/**
|
||||
* Indicates data was saved from being imported from a legacy version (v1.x -> v2.x)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
LEGACY_MIGRATION,
|
||||
public static final SaveCause LEGACY_MIGRATION = of("LEGACY_MIGRATION", false);
|
||||
|
||||
/**
|
||||
* Indicates data was saved from being imported from a legacy version (v2.x -> v3.x)
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
CONVERTED_FROM_V2;
|
||||
public static final SaveCause CONVERTED_FROM_V2 = of("CONVERTED_FROM_V2", false);
|
||||
|
||||
@NotNull
|
||||
public String getDisplayName() {
|
||||
return Locales.truncate(name().toLowerCase(Locale.ENGLISH)
|
||||
.replaceAll("_", " "), 18);
|
||||
private final String name;
|
||||
|
||||
private final boolean fireDataSaveEvent;
|
||||
|
||||
/**
|
||||
* Get or create a {@link SaveCause} from a name
|
||||
*
|
||||
* @param name the name to be displayed
|
||||
* @return the cause
|
||||
*/
|
||||
@NotNull
|
||||
public static SaveCause of(@NotNull String name) {
|
||||
return new SaveCause(name.length() > 32 ? name.substring(0, 31) : name, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a {@link SaveCause} from a name and whether it should fire a save event
|
||||
*
|
||||
* @param name the name to be displayed
|
||||
* @param firesSaveEvent whether the cause should fire a save event
|
||||
* @return the cause
|
||||
*/
|
||||
@NotNull
|
||||
public static SaveCause of(@NotNull String name, boolean firesSaveEvent) {
|
||||
return new SaveCause(name.length() > 32 ? name.substring(0, 31) : name, firesSaveEvent);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getLocale(@NotNull HuskSync plugin) {
|
||||
return plugin.getLocales()
|
||||
.getRawLocale("save_cause_%s".formatted(name().toLowerCase(Locale.ENGLISH)))
|
||||
.orElse(getDisplayName());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static SaveCause[] values() {
|
||||
return new SaveCause[]{
|
||||
DISCONNECT, WORLD_SAVE, DEATH, SERVER_SHUTDOWN, INVENTORY_COMMAND, ENDERCHEST_COMMAND,
|
||||
BACKUP_RESTORE, API, MPDB_MIGRATION, LEGACY_MIGRATION, CONVERTED_FROM_V2
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the cause of a player having their data updated.
|
||||
* A string wrapper, for identifying the cause of a player data update.
|
||||
* </p>
|
||||
* Cause names have a max length of 32 characters.
|
||||
*/
|
||||
public enum UpdateCause {
|
||||
@Accessors(fluent = true)
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class UpdateCause implements Cause {
|
||||
|
||||
/**
|
||||
* Indicates the data was updated by a synchronization process
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
SYNCHRONIZED("synchronization_complete", "synchronization_failed"),
|
||||
public static final UpdateCause SYNCHRONIZED = of("SYNCHRONIZED",
|
||||
"synchronization_complete", "synchronization_failed"
|
||||
);
|
||||
|
||||
/**
|
||||
* Indicates the data was updated by a user joining the server
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
NEW_USER("user_registration_complete", null),
|
||||
public static final UpdateCause NEW_USER = of("NEW_USER",
|
||||
"user_registration_complete", null
|
||||
);
|
||||
|
||||
/**
|
||||
* Indicates the data was updated by a data update process (management command, API, etc.)
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
UPDATED("data_update_complete", "data_update_failed");
|
||||
public static final UpdateCause UPDATED = of("UPDATED",
|
||||
"data_update_complete", "data_update_failed"
|
||||
);
|
||||
|
||||
private final String completedLocale;
|
||||
private final String failureLocale;
|
||||
/**
|
||||
* Indicates data was saved by an API call
|
||||
*
|
||||
* @since 3.3.3
|
||||
*/
|
||||
public static final UpdateCause API = of("API");
|
||||
|
||||
UpdateCause(@Nullable String completedLocale, @Nullable String failureLocale) {
|
||||
this.completedLocale = completedLocale;
|
||||
this.failureLocale = failureLocale;
|
||||
@NotNull
|
||||
private final String name;
|
||||
@Nullable
|
||||
private String completedLocale;
|
||||
@Nullable
|
||||
private String failureLocale;
|
||||
|
||||
/**
|
||||
* Get or create a {@link UpdateCause} from a name and completed/failure locales
|
||||
*
|
||||
* @param name the name to be displayed
|
||||
* @param completedLocale the locale to be displayed on successful update,
|
||||
* or {@code null} if none is to be shown
|
||||
* @param failureLocale the locale to be displayed on a failed update,
|
||||
* or {@code null} if none is to be shown
|
||||
* @return the cause
|
||||
*/
|
||||
public static UpdateCause of(@NotNull String name, @Nullable String completedLocale,
|
||||
@Nullable String failureLocale) {
|
||||
return new UpdateCause(
|
||||
name.length() > 32 ? name.substring(0, 31) : name,
|
||||
completedLocale, failureLocale
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a {@link UpdateCause} from a name
|
||||
*
|
||||
* @param name the name to be displayed
|
||||
* @return the cause
|
||||
*/
|
||||
@NotNull
|
||||
public static UpdateCause of(@NotNull String name) {
|
||||
return of(name, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message to be displayed when a user's data is successfully updated.
|
||||
*
|
||||
* @param plugin plugin instance
|
||||
* @return the message
|
||||
*/
|
||||
public Optional<MineDown> getCompletedLocale(@NotNull HuskSync plugin) {
|
||||
if (completedLocale != null) {
|
||||
return plugin.getLocales().getLocale(completedLocale);
|
||||
if (completedLocale() != null) {
|
||||
return Optional.of(plugin.getLocales().getLocale(completedLocale())
|
||||
.orElse(plugin.getLocales().format(getDisplayName())));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message to be displayed when a user's data fails to update.
|
||||
*
|
||||
* @param plugin plugin instance
|
||||
* @return the message
|
||||
*/
|
||||
public Optional<MineDown> getFailedLocale(@NotNull HuskSync plugin) {
|
||||
if (failureLocale != null) {
|
||||
return plugin.getLocales().getLocale(failureLocale);
|
||||
if (failureLocale() != null) {
|
||||
return Optional.of(plugin.getLocales().getLocale(failureLocale())
|
||||
.orElse(plugin.getLocales().format(failureLocale())));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static UpdateCause[] values() {
|
||||
return new UpdateCause[]{
|
||||
SYNCHRONIZED, NEW_USER, UPDATED
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,8 +41,10 @@ public class Identifier {
|
||||
public static Identifier STATISTICS = huskSync("statistics", true);
|
||||
public static Identifier HEALTH = huskSync("health", true);
|
||||
public static Identifier HUNGER = huskSync("hunger", true);
|
||||
public static Identifier ATTRIBUTES = huskSync("attributes", true);
|
||||
public static Identifier EXPERIENCE = huskSync("experience", true);
|
||||
public static Identifier GAME_MODE = huskSync("game_mode", true);
|
||||
public static Identifier FLIGHT_STATUS = huskSync("flight_status", true);
|
||||
public static Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
|
||||
|
||||
private final Key key;
|
||||
@@ -113,8 +115,8 @@ public class Identifier {
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, Boolean> getConfigMap() {
|
||||
return Map.ofEntries(Stream.of(
|
||||
INVENTORY, ENDER_CHEST, POTION_EFFECTS, ADVANCEMENTS, LOCATION,
|
||||
STATISTICS, HEALTH, HUNGER, EXPERIENCE, GAME_MODE, PERSISTENT_DATA
|
||||
INVENTORY, ENDER_CHEST, POTION_EFFECTS, ADVANCEMENTS, LOCATION, STATISTICS,
|
||||
HEALTH, HUNGER, ATTRIBUTES, EXPERIENCE, GAME_MODE, FLIGHT_STATUS, PERSISTENT_DATA
|
||||
)
|
||||
.map(Identifier::getConfigEntry)
|
||||
.toArray(Map.Entry[]::new));
|
||||
|
||||
@@ -43,7 +43,7 @@ public interface UserDataHolder extends DataHolder {
|
||||
@NotNull
|
||||
default Map<Identifier, Data> getData() {
|
||||
return getPlugin().getRegisteredDataTypes().stream()
|
||||
.filter(type -> type.isCustom() || getPlugin().getSettings().isSyncFeatureEnabled(type))
|
||||
.filter(type -> type.isCustom() || getPlugin().getSettings().getSynchronization().isFeatureEnabled(type))
|
||||
.map(id -> Map.entry(id, getData(id)))
|
||||
.filter(data -> data.getValue().isPresent())
|
||||
.collect(HashMap::new, (map, data) -> map.put(data.getKey(), data.getValue().get()), HashMap::putAll);
|
||||
@@ -60,7 +60,7 @@ public interface UserDataHolder extends DataHolder {
|
||||
*/
|
||||
@Override
|
||||
default void setData(@NotNull Identifier identifier, @NotNull Data data) {
|
||||
getPlugin().runSync(() -> data.apply(this, getPlugin()));
|
||||
getPlugin().runSync(() -> data.apply(this, getPlugin()), this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,6 +97,7 @@ public interface UserDataHolder extends DataHolder {
|
||||
unpacked = snapshot.unpack(plugin);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, String.format("Failed to unpack data snapshot for %s", getUsername()), e);
|
||||
runAfter.accept(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,7 +106,7 @@ public interface UserDataHolder extends DataHolder {
|
||||
try {
|
||||
for (Map.Entry<Identifier, Data> entry : unpacked.getData().entrySet()) {
|
||||
final Identifier identifier = entry.getKey();
|
||||
if (plugin.getSettings().isSyncFeatureEnabled(identifier)) {
|
||||
if (plugin.getSettings().getSynchronization().isFeatureEnabled(identifier)) {
|
||||
if (identifier.isCustom()) {
|
||||
getCustomDataStore().put(identifier, entry.getValue());
|
||||
}
|
||||
@@ -118,7 +119,7 @@ public interface UserDataHolder extends DataHolder {
|
||||
return;
|
||||
}
|
||||
plugin.runAsync(() -> runAfter.accept(true));
|
||||
});
|
||||
}, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -171,6 +172,11 @@ public interface UserDataHolder extends DataHolder {
|
||||
this.setData(Identifier.GAME_MODE, gameMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setFlightStatus(@NotNull Data.FlightStatus flightStatus) {
|
||||
this.setData(Identifier.FLIGHT_STATUS, flightStatus);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setPersistentData(@NotNull Data.PersistentData persistentData) {
|
||||
this.setData(Identifier.PERSISTENT_DATA, persistentData);
|
||||
|
||||
@@ -19,11 +19,10 @@
|
||||
|
||||
package net.william278.husksync.database;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.DataSnapshot.SaveCause;
|
||||
import net.william278.husksync.data.UserDataHolder;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -31,10 +30,8 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* An abstract representation of the plugin database, storing player data.
|
||||
@@ -71,8 +68,9 @@ public abstract class Database {
|
||||
*/
|
||||
@NotNull
|
||||
protected final String formatStatementTables(@NotNull String sql) {
|
||||
return sql.replaceAll("%users_table%", plugin.getSettings().getTableName(Settings.TableName.USERS))
|
||||
.replaceAll("%user_data_table%", plugin.getSettings().getTableName(Settings.TableName.USER_DATA));
|
||||
final Settings.DatabaseSettings settings = plugin.getSettings().getDatabase();
|
||||
return sql.replaceAll("%users_table%", settings.getTableName(TableName.USERS))
|
||||
.replaceAll("%user_data_table%", settings.getTableName(TableName.USER_DATA));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,43 +155,24 @@ public abstract class Database {
|
||||
@Blocking
|
||||
public abstract boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid);
|
||||
|
||||
/**
|
||||
* Save user data to the database
|
||||
* </p>
|
||||
* This will remove the oldest data for the user if the amount of data exceeds the limit as configured
|
||||
*
|
||||
* @param user The user to add data for
|
||||
* @param snapshot The {@link DataSnapshot} to set.
|
||||
* The implementation should version it with a random UUID and the current timestamp during insertion.
|
||||
* @see UserDataHolder#createSnapshot(SaveCause)
|
||||
*/
|
||||
@Blocking
|
||||
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot) {
|
||||
if (snapshot.getSaveCause() != SaveCause.SERVER_SHUTDOWN) {
|
||||
plugin.fireEvent(
|
||||
plugin.getDataSaveEvent(user, snapshot),
|
||||
(event) -> this.addAndRotateSnapshot(user, snapshot)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.addAndRotateSnapshot(user, snapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>Internal</b> - Save user data to the database. This will:
|
||||
* Save user data to the database, doing the following (in order):
|
||||
* <ol>
|
||||
* <li>Delete their most recent snapshot, if it was created before the backup frequency time</li>
|
||||
* <li>Create the snapshot</li>
|
||||
* <li>Rotate snapshot backups</li>
|
||||
* </ol>
|
||||
* This is an expensive blocking method and should be run off the main thread.
|
||||
*
|
||||
* @param user The user to add data for
|
||||
* @param snapshot The {@link DataSnapshot} to set.
|
||||
* @apiNote Prefer {@link net.william278.husksync.sync.DataSyncer#saveData(User, DataSnapshot.Packed, BiConsumer)}.
|
||||
* </p>This method will not fire the {@link net.william278.husksync.event.DataSaveEvent}
|
||||
*/
|
||||
@Blocking
|
||||
private void addAndRotateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot) {
|
||||
final int backupFrequency = plugin.getSettings().getBackupFrequency();
|
||||
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot) {
|
||||
final int backupFrequency = plugin.getSettings().getSynchronization().getSnapshotBackupFrequency();
|
||||
if (!snapshot.isPinned() && backupFrequency > 0) {
|
||||
this.rotateLatestSnapshot(user, snapshot.getTimestamp().minusHours(backupFrequency));
|
||||
}
|
||||
@@ -274,9 +253,12 @@ public abstract class Database {
|
||||
/**
|
||||
* Identifies types of databases
|
||||
*/
|
||||
@Getter
|
||||
public enum Type {
|
||||
MYSQL("MySQL", "mysql"),
|
||||
MARIADB("MariaDB", "mariadb");
|
||||
MARIADB("MariaDB", "mariadb"),
|
||||
POSTGRES("PostgreSQL", "postgresql"),
|
||||
MONGO("MongoDB", "mongo");
|
||||
|
||||
private final String displayName;
|
||||
private final String protocol;
|
||||
@@ -285,16 +267,33 @@ public abstract class Database {
|
||||
this.displayName = displayName;
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the names of tables in the database
|
||||
*/
|
||||
@Getter
|
||||
public enum TableName {
|
||||
USERS("husksync_users"),
|
||||
USER_DATA("husksync_user_data");
|
||||
|
||||
private final String defaultName;
|
||||
|
||||
TableName(@NotNull String defaultName) {
|
||||
this.defaultName = defaultName;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map.Entry<String, String> toEntry() {
|
||||
return Map.entry(name().toLowerCase(Locale.ENGLISH), defaultName);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@NotNull
|
||||
public static Map<String, String> getDefaults() {
|
||||
return Map.ofEntries(Arrays.stream(values())
|
||||
.map(TableName::toEntry)
|
||||
.toArray(Map.Entry[]::new));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.database;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.mongodb.ConnectionString;
|
||||
import com.mongodb.MongoException;
|
||||
import com.mongodb.client.FindIterable;
|
||||
import com.mongodb.client.model.Updates;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.database.mongo.MongoCollectionHelper;
|
||||
import net.william278.husksync.database.mongo.MongoConnectionHandler;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.bson.types.Binary;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.TimeZone;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class MongoDbDatabase extends Database {
|
||||
private MongoConnectionHandler mongoConnectionHandler;
|
||||
private MongoCollectionHelper mongoCollectionHelper;
|
||||
|
||||
private final String usersTable;
|
||||
private final String userDataTable;
|
||||
public MongoDbDatabase(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
this.usersTable = plugin.getSettings().getDatabase().getTableName(TableName.USERS);
|
||||
this.userDataTable = plugin.getSettings().getDatabase().getTableName(TableName.USER_DATA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the database and ensure tables are present; create tables if they do not exist.
|
||||
*
|
||||
* @throws IllegalStateException if the database could not be initialized
|
||||
*/
|
||||
@Override
|
||||
public void initialize() throws IllegalStateException {
|
||||
final Settings.DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
|
||||
try {
|
||||
ConnectionString URI = createConnectionURI(credentials);
|
||||
mongoConnectionHandler = new MongoConnectionHandler(URI, credentials.getDatabase());
|
||||
mongoCollectionHelper = new MongoCollectionHelper(mongoConnectionHandler);
|
||||
if (mongoCollectionHelper.getCollection(usersTable) == null) {
|
||||
mongoCollectionHelper.createCollection(usersTable);
|
||||
}
|
||||
if (mongoCollectionHelper.getCollection(userDataTable) == null) {
|
||||
mongoCollectionHelper.createCollection(userDataTable);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " +
|
||||
"Please check the supplied database credentials in the config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ConnectionString createConnectionURI(Settings.DatabaseSettings.DatabaseCredentials credentials) {
|
||||
String baseURI = plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ?
|
||||
"mongodb+srv://{0}:{1}@{2}/{4}{5}" : "mongodb://{0}:{1}@{2}:{3}/{4}{5}";
|
||||
baseURI = baseURI.replace("{0}", credentials.getUsername());
|
||||
baseURI = baseURI.replace("{1}", credentials.getPassword());
|
||||
baseURI = baseURI.replace("{2}", credentials.getHost());
|
||||
baseURI = baseURI.replace("{3}", String.valueOf(credentials.getPort()));
|
||||
baseURI = baseURI.replace("{4}", credentials.getDatabase());
|
||||
baseURI = baseURI.replace("{5}", plugin.getSettings().getDatabase().getMongoSettings().getParameters());
|
||||
return new ConnectionString(baseURI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a {@link User} has an entry in the database and that their username is up-to-date
|
||||
*
|
||||
* @param user The {@link User} to ensure
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public void ensureUser(@NotNull User user) {
|
||||
try {
|
||||
getUser(user.getUuid()).ifPresentOrElse(
|
||||
existingUser -> {
|
||||
if (!existingUser.getUsername().equals(user.getUsername())) {
|
||||
// Update a user's name if it has changed in the database
|
||||
try {
|
||||
Document filter = new Document("uuid", existingUser.getUuid().toString());
|
||||
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
||||
if (doc == null) {
|
||||
throw new MongoException("User document returned null!");
|
||||
}
|
||||
|
||||
Bson updates = Updates.set("uuid", user.getUuid().toString());
|
||||
mongoCollectionHelper.updateDocument(usersTable, doc, updates);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
// Insert new player data into the database
|
||||
try {
|
||||
Document doc = new Document("uuid", user.getUuid().toString()).append("username", user.getUsername());
|
||||
mongoCollectionHelper.insertDocument(usersTable, doc);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to ensure user data is in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a player by their Minecraft account {@link UUID}
|
||||
*
|
||||
* @param uuid Minecraft account {@link UUID} of the {@link User} to get
|
||||
* @return An optional with the {@link User} present if they exist
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<User> getUser(@NotNull UUID uuid) {
|
||||
try {
|
||||
Document filter = new Document("uuid", uuid);
|
||||
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
||||
if (doc != null) {
|
||||
return Optional.of(new User(UUID.fromString(doc.getString("uuid")),
|
||||
doc.getString("username")));
|
||||
}
|
||||
return Optional.empty();
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get user data from the database", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by their username (<i>case-insensitive</i>)
|
||||
*
|
||||
* @param username Username of the {@link User} to get (<i>case-insensitive</i>)
|
||||
* @return An optional with the {@link User} present if they exist
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<User> getUserByName(@NotNull String username) {
|
||||
try {
|
||||
Document filter = new Document("username", username);
|
||||
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
||||
if (doc != null) {
|
||||
return Optional.of(new User(UUID.fromString(doc.getString("uuid")),
|
||||
doc.getString("username")));
|
||||
}
|
||||
return Optional.empty();
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get user data from the database", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest data snapshot for a user.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @return an optional containing the {@link DataSnapshot}, if it exists, or an empty optional if it does not
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
||||
try {
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString());
|
||||
Document sort = new Document("timestamp", -1); // -1 = Descending
|
||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
||||
Document doc = iterable.first();
|
||||
if (doc != null) {
|
||||
final UUID versionUuid = UUID.fromString(doc.getString("version_uuid"));
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
|
||||
final Binary bin = doc.get("data", Binary.class);
|
||||
final byte[] dataByteArray = bin.getData();
|
||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
return Optional.empty();
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get latest snapshot from the database", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all {@link DataSnapshot} entries for a user from the database.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @return The list of a user's {@link DataSnapshot} entries
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
@NotNull
|
||||
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
|
||||
try {
|
||||
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString());
|
||||
Document sort = new Document("timestamp", -1); // -1 = Descending
|
||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
||||
for (Document doc : iterable) {
|
||||
final UUID versionUuid = UUID.fromString(doc.getString("version_uuid"));
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
|
||||
final Binary bin = doc.get("data", Binary.class);
|
||||
final byte[] dataByteArray = bin.getData();
|
||||
retrievedData.add(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
return retrievedData;
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get all snapshots from the database", e);
|
||||
return Lists.newArrayList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @param versionUuid The UUID of the {@link DataSnapshot} entry to get
|
||||
* @return An optional containing the {@link DataSnapshot}, if it exists
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
try {
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString()).append("version_uuid", versionUuid.toString());
|
||||
Document sort = new Document("timestamp", -1); // -1 = Descending
|
||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
||||
Document doc = iterable.first();
|
||||
if (doc != null) {
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
|
||||
final Binary bin = doc.get("data", Binary.class);
|
||||
final byte[] dataByteArray = bin.getData();
|
||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
return Optional.empty();
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get snapshot from the database", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal)</b> Prune user data for a given user to the maximum value as configured.
|
||||
*
|
||||
* @param user The user to prune data for
|
||||
* @implNote Data snapshots marked as {@code pinned} are exempt from rotation
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateSnapshots(@NotNull User user) {
|
||||
try {
|
||||
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
|
||||
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
|
||||
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
|
||||
if (unpinnedUserData.size() > maxSnapshots) {
|
||||
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString()).append("pinned", false);
|
||||
Document sort = new Document("timestamp", 1); // 1 = Ascending
|
||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable)
|
||||
.find(filter)
|
||||
.sort(sort)
|
||||
.limit(unpinnedUserData.size() - maxSnapshots);
|
||||
|
||||
for (Document doc : iterable) {
|
||||
mongoCollectionHelper.deleteDocument(userDataTable, doc);
|
||||
}
|
||||
}
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to rotate snapshots", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @param versionUuid The UUID of the {@link DataSnapshot} entry to delete
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
try {
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString()).append("version_uuid", versionUuid.toString());
|
||||
Document doc = mongoCollectionHelper.getCollection(userDataTable).find(filter).first();
|
||||
if (doc == null) {
|
||||
return false;
|
||||
}
|
||||
mongoCollectionHelper.deleteDocument(userDataTable, doc);
|
||||
return true;
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to delete specific user data from the database", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the most recent data snapshot by the given {@link User user}
|
||||
* The snapshot must have been created after {@link OffsetDateTime time} and NOT be pinned
|
||||
* Facilities the backup frequency feature, reducing redundant snapshots from being saved longer than needed
|
||||
*
|
||||
* @param user The user to delete a snapshot for
|
||||
* @param within The time to delete a snapshot after
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
|
||||
try {
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString()).append("pinned", false);
|
||||
Document sort = new Document("timestamp", 1); // 1 = Ascending
|
||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable)
|
||||
.find(filter)
|
||||
.sort(sort);
|
||||
|
||||
for (Document doc : iterable) {
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(
|
||||
Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId()
|
||||
);
|
||||
if (timestamp.isAfter(within)) {
|
||||
mongoCollectionHelper.deleteDocument(userDataTable, doc);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to rotate latest snapshot from the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>Internal</b> - Create user data in the database
|
||||
*
|
||||
* @param user The user to add data for
|
||||
* @param data The {@link DataSnapshot} to set.
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try {
|
||||
Document doc = new Document("player_uuid", user.getUuid().toString())
|
||||
.append("version_uuid", data.getId().toString())
|
||||
.append("timestamp", data.getTimestamp().toInstant().toEpochMilli())
|
||||
.append("save_cause", data.getSaveCause().name())
|
||||
.append("pinned", data.isPinned())
|
||||
.append("data", new Binary(data.asBytes(plugin)));
|
||||
mongoCollectionHelper.insertDocument(userDataTable, doc);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to set user data in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a saved {@link DataSnapshot} by given version UUID
|
||||
*
|
||||
* @param user The user whose data snapshot
|
||||
* @param data The {@link DataSnapshot} to update
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try {
|
||||
Document doc = new Document("player_uuid", user.getUuid().toString()).append("version_uuid", data.getId().toString());
|
||||
Bson updates = Updates.combine(
|
||||
Updates.set("save_cause", data.getSaveCause().name()),
|
||||
Updates.set("pinned", data.isPinned()),
|
||||
Updates.set("data", new Binary(data.asBytes(plugin)))
|
||||
);
|
||||
mongoCollectionHelper.updateDocument(userDataTable, doc, updates);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to update snapshot in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipes <b>all</b> {@link User} entries from the database.
|
||||
* <b>This should only be used when preparing tables for a data migration.</b>
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public void wipeDatabase() {
|
||||
try {
|
||||
mongoCollectionHelper.deleteCollection(usersTable);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to wipe the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
@Override
|
||||
public void terminate() {
|
||||
if (mongoConnectionHandler != null) {
|
||||
mongoConnectionHandler.closeConnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
package net.william278.husksync.database;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
@@ -34,6 +35,8 @@ import java.time.OffsetDateTime;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static net.william278.husksync.config.Settings.DatabaseSettings;
|
||||
|
||||
public class MySqlDatabase extends Database {
|
||||
|
||||
private static final String DATA_POOL_NAME = "HuskSyncHikariPool";
|
||||
@@ -43,9 +46,10 @@ public class MySqlDatabase extends Database {
|
||||
|
||||
public MySqlDatabase(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
this.flavor = plugin.getSettings().getDatabaseType().getProtocol();
|
||||
this.driverClass = plugin.getSettings().getDatabaseType() == Type.MARIADB
|
||||
? "org.mariadb.jdbc.Driver" : "com.mysql.cj.jdbc.Driver";
|
||||
|
||||
final Type type = plugin.getSettings().getDatabase().getType();
|
||||
this.flavor = type.getProtocol();
|
||||
this.driverClass = type == Type.MARIADB ? "org.mariadb.jdbc.Driver" : "com.mysql.cj.jdbc.Driver";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,26 +71,28 @@ public class MySqlDatabase extends Database {
|
||||
@Override
|
||||
public void initialize() throws IllegalStateException {
|
||||
// Initialize the Hikari pooled connection
|
||||
final DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
|
||||
dataSource = new HikariDataSource();
|
||||
dataSource.setDriverClassName(driverClass);
|
||||
dataSource.setJdbcUrl(String.format("jdbc:%s://%s:%s/%s%s",
|
||||
flavor,
|
||||
plugin.getSettings().getMySqlHost(),
|
||||
plugin.getSettings().getMySqlPort(),
|
||||
plugin.getSettings().getMySqlDatabase(),
|
||||
plugin.getSettings().getMySqlConnectionParameters()
|
||||
credentials.getHost(),
|
||||
credentials.getPort(),
|
||||
credentials.getDatabase(),
|
||||
credentials.getParameters()
|
||||
));
|
||||
|
||||
// Authenticate with the database
|
||||
dataSource.setUsername(plugin.getSettings().getMySqlUsername());
|
||||
dataSource.setPassword(plugin.getSettings().getMySqlPassword());
|
||||
dataSource.setUsername(credentials.getUsername());
|
||||
dataSource.setPassword(credentials.getPassword());
|
||||
|
||||
// Set connection pool options
|
||||
dataSource.setMaximumPoolSize(plugin.getSettings().getMySqlConnectionPoolSize());
|
||||
dataSource.setMinimumIdle(plugin.getSettings().getMySqlConnectionPoolIdle());
|
||||
dataSource.setMaxLifetime(plugin.getSettings().getMySqlConnectionPoolLifetime());
|
||||
dataSource.setKeepaliveTime(plugin.getSettings().getMySqlConnectionPoolKeepAlive());
|
||||
dataSource.setConnectionTimeout(plugin.getSettings().getMySqlConnectionPoolTimeout());
|
||||
final DatabaseSettings.PoolSettings pool = plugin.getSettings().getDatabase().getConnectionPool();
|
||||
dataSource.setMaximumPoolSize(pool.getMaximumPoolSize());
|
||||
dataSource.setMinimumIdle(pool.getMinimumIdle());
|
||||
dataSource.setMaxLifetime(pool.getMaximumLifetime());
|
||||
dataSource.setKeepaliveTime(pool.getKeepaliveTime());
|
||||
dataSource.setConnectionTimeout(pool.getConnectionTimeout());
|
||||
dataSource.setPoolName(DATA_POOL_NAME);
|
||||
|
||||
// Set additional connection pool properties
|
||||
@@ -245,7 +251,7 @@ public class MySqlDatabase extends Database {
|
||||
@Override
|
||||
@NotNull
|
||||
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
|
||||
final List<DataSnapshot.Packed> retrievedData = new ArrayList<>();
|
||||
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `data`
|
||||
@@ -306,7 +312,8 @@ public class MySqlDatabase extends Database {
|
||||
protected void rotateSnapshots(@NotNull User user) {
|
||||
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
|
||||
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
|
||||
if (unpinnedUserData.size() > plugin.getSettings().getMaxUserDataSnapshots()) {
|
||||
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
|
||||
if (unpinnedUserData.size() > maxSnapshots) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM `%user_data_table%`
|
||||
@@ -314,7 +321,7 @@ public class MySqlDatabase extends Database {
|
||||
AND `pinned` IS FALSE
|
||||
ORDER BY `timestamp` ASC
|
||||
LIMIT %entry_count%;""".replace("%entry_count%",
|
||||
Integer.toString(unpinnedUserData.size() - plugin.getSettings().getMaxUserDataSnapshots()))))) {
|
||||
Integer.toString(unpinnedUserData.size() - maxSnapshots))))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.database;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.*;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static net.william278.husksync.config.Settings.DatabaseSettings;
|
||||
|
||||
public class PostgresDatabase extends Database {
|
||||
|
||||
private static final String DATA_POOL_NAME = "HuskSyncHikariPool";
|
||||
private final String flavor;
|
||||
private final String driverClass;
|
||||
private HikariDataSource dataSource;
|
||||
|
||||
public PostgresDatabase(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
|
||||
final Type type = plugin.getSettings().getDatabase().getType();
|
||||
this.flavor = type.getProtocol();
|
||||
this.driverClass = "org.postgresql.Driver";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the auto-closeable connection from the hikariDataSource
|
||||
*
|
||||
* @return The {@link Connection} to the MySQL database
|
||||
* @throws SQLException if the connection fails for some reason
|
||||
*/
|
||||
@Blocking
|
||||
@NotNull
|
||||
private Connection getConnection() throws SQLException {
|
||||
if (dataSource == null) {
|
||||
throw new IllegalStateException("The database has not been initialized");
|
||||
}
|
||||
return dataSource.getConnection();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public void initialize() throws IllegalStateException {
|
||||
// Initialize the Hikari pooled connection
|
||||
final DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
|
||||
dataSource = new HikariDataSource();
|
||||
dataSource.setDriverClassName(driverClass);
|
||||
dataSource.setJdbcUrl(String.format("jdbc:%s://%s:%s/%s%s",
|
||||
flavor,
|
||||
credentials.getHost(),
|
||||
credentials.getPort(),
|
||||
credentials.getDatabase(),
|
||||
credentials.getParameters()
|
||||
));
|
||||
|
||||
// Authenticate with the database
|
||||
dataSource.setUsername(credentials.getUsername());
|
||||
dataSource.setPassword(credentials.getPassword());
|
||||
|
||||
// Set connection pool options
|
||||
final DatabaseSettings.PoolSettings pool = plugin.getSettings().getDatabase().getConnectionPool();
|
||||
dataSource.setMaximumPoolSize(pool.getMaximumPoolSize());
|
||||
dataSource.setMinimumIdle(pool.getMinimumIdle());
|
||||
dataSource.setMaxLifetime(pool.getMaximumLifetime());
|
||||
dataSource.setKeepaliveTime(pool.getKeepaliveTime());
|
||||
dataSource.setConnectionTimeout(pool.getConnectionTimeout());
|
||||
dataSource.setPoolName(DATA_POOL_NAME);
|
||||
|
||||
// Set additional connection pool properties
|
||||
final Properties properties = new Properties();
|
||||
properties.putAll(
|
||||
Map.of("cachePrepStmts", "true",
|
||||
"prepStmtCacheSize", "250",
|
||||
"prepStmtCacheSqlLimit", "2048",
|
||||
"useServerPrepStmts", "true",
|
||||
"useLocalSessionState", "true",
|
||||
"useLocalTransactionState", "true"
|
||||
));
|
||||
properties.putAll(
|
||||
Map.of(
|
||||
"rewriteBatchedStatements", "true",
|
||||
"cacheResultSetMetadata", "true",
|
||||
"cacheServerConfiguration", "true",
|
||||
"elideSetAutoCommits", "true",
|
||||
"maintainTimeStats", "false")
|
||||
);
|
||||
dataSource.setDataSourceProperties(properties);
|
||||
|
||||
// Prepare database schema; make tables if they don't exist
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
final String[] databaseSchema = getSchemaStatements(String.format("database/%s_schema.sql", flavor));
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
for (String tableCreationStatement : databaseSchema) {
|
||||
statement.execute(tableCreationStatement);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new IllegalStateException("Failed to create database tables. Please ensure you are running PostgreSQL " +
|
||||
"and that your connecting user account has privileges to create tables.", e);
|
||||
}
|
||||
} catch (SQLException | IOException e) {
|
||||
throw new IllegalStateException("Failed to establish a connection to the PostgreSQL database. " +
|
||||
"Please check the supplied database credentials in the config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public void ensureUser(@NotNull User user) {
|
||||
getUser(user.getUuid()).ifPresentOrElse(
|
||||
existingUser -> {
|
||||
if (!existingUser.getUsername().equals(user.getUsername())) {
|
||||
// Update a user's name if it has changed in the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE "%users_table%"
|
||||
SET "username"=?
|
||||
WHERE "uuid"=?"""))) {
|
||||
|
||||
statement.setString(1, user.getUsername());
|
||||
statement.setObject(2, existingUser.getUuid());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
plugin.log(Level.INFO, "Updated " + user.getUsername() + "'s name in the database (" + existingUser.getUsername() + " -> " + user.getUsername() + ")");
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
// Insert new player data into the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO "%users_table%" ("uuid","username")
|
||||
VALUES (?,?);"""))) {
|
||||
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.setString(2, user.getUsername());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<User> getUser(@NotNull UUID uuid) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT "uuid", "username"
|
||||
FROM "%users_table%"
|
||||
WHERE "uuid"=?"""))) {
|
||||
|
||||
statement.setObject(1, uuid);
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return Optional.of(new User((UUID) resultSet.getObject("uuid"),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user from uuid from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<User> getUserByName(@NotNull String username) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT "uuid", "username"
|
||||
FROM "%users_table%"
|
||||
WHERE "username"=?"""))) {
|
||||
statement.setString(1, username);
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return Optional.of(new User((UUID) resultSet.getObject("uuid"),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user by name from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT "version_uuid", "timestamp", "data"
|
||||
FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=?
|
||||
ORDER BY "timestamp" DESC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final UUID versionUuid = (UUID) resultSet.getObject("version_uuid");
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(
|
||||
resultSet.getTimestamp("timestamp").toInstant(), TimeZone.getDefault().toZoneId()
|
||||
);
|
||||
final byte[] dataByteArray = resultSet.getBytes("data");
|
||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
@NotNull
|
||||
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
|
||||
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT "version_uuid", "timestamp", "data"
|
||||
FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=?
|
||||
ORDER BY "timestamp" DESC;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
final UUID versionUuid = (UUID) resultSet.getObject("version_uuid");
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(
|
||||
resultSet.getTimestamp("timestamp").toInstant(), TimeZone.getDefault().toZoneId()
|
||||
);
|
||||
final byte[] dataByteArray = resultSet.getBytes("data");
|
||||
retrievedData.add(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
return retrievedData;
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
}
|
||||
return retrievedData;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT "version_uuid", "timestamp", "data"
|
||||
FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=? AND "version_uuid"=?
|
||||
ORDER BY "timestamp" DESC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.setObject(2, versionUuid);
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(
|
||||
resultSet.getTimestamp("timestamp").toInstant(), TimeZone.getDefault().toZoneId()
|
||||
);
|
||||
final byte[] dataByteArray = resultSet.getBytes("data");
|
||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch specific user data by UUID from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateSnapshots(@NotNull User user) {
|
||||
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
|
||||
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
|
||||
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
|
||||
if (unpinnedUserData.size() > maxSnapshots) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=?
|
||||
AND "pinned" = FALSE
|
||||
ORDER BY "timestamp" ASC
|
||||
LIMIT %entry_count%;""".replace("%entry_count%",
|
||||
Integer.toString(unpinnedUserData.size() - maxSnapshots))))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to prune user data from the database", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=? AND "version_uuid"=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
return statement.executeUpdate() > 0;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to delete specific user data from the database", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=? AND "timestamp" = (
|
||||
SELECT "timestamp"
|
||||
FROM "%user_data_table%"
|
||||
WHERE "player_uuid"=? AND "timestamp" > ? AND "pinned" = FALSE
|
||||
ORDER BY "timestamp" ASC
|
||||
LIMIT 1
|
||||
);"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.setObject(2, user.getUuid());
|
||||
statement.setTimestamp(3, Timestamp.from(within.toInstant()));
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to delete a user's data from the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO "%user_data_table%"
|
||||
("player_uuid","version_uuid","timestamp","save_cause","pinned","data")
|
||||
VALUES (?,?,?,?,?,?);"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.setObject(2, data.getId());
|
||||
statement.setTimestamp(3, Timestamp.from(data.getTimestamp().toInstant()));
|
||||
statement.setString(4, data.getSaveCause().name());
|
||||
statement.setBoolean(5, data.isPinned());
|
||||
statement.setBytes(6, data.asBytes(plugin));
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to set user data in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE "%user_data_table%"
|
||||
SET "save_cause"=?,"pinned"=?,"data"=?
|
||||
WHERE "player_uuid"=? AND "version_uuid"=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, data.getSaveCause().name());
|
||||
statement.setBoolean(2, data.isPinned());
|
||||
statement.setBytes(3, data.asBytes(plugin));
|
||||
statement.setObject(4, user.getUuid());
|
||||
statement.setObject(5, data.getId());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to pin user data in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void wipeDatabase() {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
statement.executeUpdate(formatStatementTables("DELETE FROM \"%user_data_table%\";"));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to wipe the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
if (dataSource != null) {
|
||||
if (!dataSource.isClosed()) {
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.database.mongo;
|
||||
|
||||
import com.mongodb.client.MongoCollection;
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class MongoCollectionHelper {
|
||||
private final MongoConnectionHandler database;
|
||||
|
||||
/**
|
||||
* Initialize the collection helper
|
||||
* @param database Instance of {@link MongoConnectionHandler}
|
||||
*/
|
||||
public MongoCollectionHelper(@NotNull MongoConnectionHandler database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a collection
|
||||
* @param collectionName the collection name
|
||||
*/
|
||||
public void createCollection(@NotNull String collectionName) {
|
||||
database.getDatabase().createCollection(collectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a collection
|
||||
* @param collectionName the collection name
|
||||
*/
|
||||
public void deleteCollection(@NotNull String collectionName) {
|
||||
database.getDatabase().getCollection(collectionName).drop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a collection
|
||||
* @param collectionName the collection name
|
||||
* @return MongoCollection<Document>
|
||||
*/
|
||||
public MongoCollection<Document> getCollection(@NotNull String collectionName) {
|
||||
return database.getDatabase().getCollection(collectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a document to a collection
|
||||
* @param collectionName collection to add to
|
||||
* @param document Document to add
|
||||
*/
|
||||
public void insertDocument(@NotNull String collectionName, @NotNull Document document) {
|
||||
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
||||
collection.insertOne(document);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a document
|
||||
* @param collectionName collection the document is in
|
||||
* @param document filter of document
|
||||
* @param updates Bson of updates
|
||||
*/
|
||||
public void updateDocument(@NotNull String collectionName, @NotNull Document document, @NotNull Bson updates) {
|
||||
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
||||
collection.updateOne(document, updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a document
|
||||
* @param collectionName collection the document is in
|
||||
* @param document filter to remove
|
||||
*/
|
||||
public void deleteDocument(@NotNull String collectionName, @NotNull Document document) {
|
||||
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
||||
collection.deleteOne(document);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.database.mongo;
|
||||
|
||||
import com.mongodb.ConnectionString;
|
||||
import com.mongodb.MongoClientSettings;
|
||||
import com.mongodb.client.MongoClient;
|
||||
import com.mongodb.client.MongoClients;
|
||||
import com.mongodb.client.MongoDatabase;
|
||||
import lombok.Getter;
|
||||
import org.bson.UuidRepresentation;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Getter
|
||||
public class MongoConnectionHandler {
|
||||
private final MongoClient mongoClient;
|
||||
private final MongoDatabase database;
|
||||
|
||||
/**
|
||||
* Initiate a connection to a Mongo Server
|
||||
* @param uri The connection string
|
||||
*/
|
||||
public MongoConnectionHandler(@NotNull ConnectionString uri, @NotNull String databaseName) {
|
||||
try {
|
||||
final MongoClientSettings settings = MongoClientSettings.builder()
|
||||
.applyConnectionString(uri)
|
||||
.uuidRepresentation(UuidRepresentation.STANDARD)
|
||||
.build();
|
||||
|
||||
this.mongoClient = MongoClients.create(settings);
|
||||
this.database = mongoClient.getDatabase(databaseName);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " +
|
||||
"Please check the supplied database credentials in the config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the connection with the database
|
||||
*/
|
||||
public void closeConnection() {
|
||||
if (this.mongoClient != null) {
|
||||
this.mongoClient.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,7 +184,7 @@ public class PlanHook {
|
||||
public String getHealth(@NotNull UUID uuid) {
|
||||
return getLatestSnapshot(uuid)
|
||||
.flatMap(DataHolder::getHealth)
|
||||
.map(health -> String.format("%s / %s", health.getHealth(), health.getMaxHealth()))
|
||||
.map(health -> String.format("%s", health.getHealth()))
|
||||
.orElse(UNKNOWN_STRING);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,13 +23,13 @@ import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.util.Task;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Level;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static net.william278.husksync.config.Settings.SynchronizationSettings.SaveOnDeathSettings;
|
||||
|
||||
/**
|
||||
* Handles what should happen when events are fired
|
||||
@@ -39,22 +39,8 @@ public abstract class EventListener {
|
||||
// The plugin instance
|
||||
protected final HuskSync plugin;
|
||||
|
||||
/**
|
||||
* Set of UUIDs of "locked players", for which events will be canceled.
|
||||
* </p>
|
||||
* Players are locked while their items are being set (on join) or saved (on quit)
|
||||
*/
|
||||
private final Set<UUID> lockedPlayers;
|
||||
|
||||
/**
|
||||
* Whether the plugin is currently being disabled
|
||||
*/
|
||||
private boolean disabling;
|
||||
|
||||
protected EventListener(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
this.lockedPlayers = new HashSet<>();
|
||||
this.disabling = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,51 +52,8 @@ public abstract class EventListener {
|
||||
if (user.isNpc()) {
|
||||
return;
|
||||
}
|
||||
lockedPlayers.add(user.getUuid());
|
||||
|
||||
plugin.runAsyncDelayed(() -> {
|
||||
// Fetch from the database if the user isn't changing servers
|
||||
if (!plugin.getRedisManager().getUserServerSwitch(user)) {
|
||||
this.setUserFromDatabase(user);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the user as soon as the source server has set the data to redis
|
||||
final long MAX_ATTEMPTS = 16L;
|
||||
final AtomicLong timesRun = new AtomicLong(0L);
|
||||
final AtomicReference<Task.Repeating> task = new AtomicReference<>();
|
||||
final Runnable runnable = () -> {
|
||||
if (user.isOffline()) {
|
||||
task.get().cancel();
|
||||
return;
|
||||
}
|
||||
if (disabling || timesRun.getAndIncrement() > MAX_ATTEMPTS) {
|
||||
task.get().cancel();
|
||||
this.setUserFromDatabase(user);
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getRedisManager().getUserData(user).ifPresent(redisData -> {
|
||||
task.get().cancel();
|
||||
user.applySnapshot(redisData, DataSnapshot.UpdateCause.SYNCHRONIZED);
|
||||
});
|
||||
};
|
||||
task.set(plugin.getRepeatingTask(runnable, 10));
|
||||
task.get().run();
|
||||
|
||||
}, Math.max(0, plugin.getSettings().getNetworkLatencyMilliseconds() / 50L));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a user's data from the database
|
||||
*
|
||||
* @param user The user to set the data for
|
||||
*/
|
||||
private void setUserFromDatabase(@NotNull OnlineUser user) {
|
||||
plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse(
|
||||
snapshot -> user.applySnapshot(snapshot, DataSnapshot.UpdateCause.SYNCHRONIZED),
|
||||
() -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin)
|
||||
);
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
plugin.getDataSyncer().setUserData(user);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,27 +62,11 @@ public abstract class EventListener {
|
||||
* @param user The {@link OnlineUser} to handle
|
||||
*/
|
||||
protected final void handlePlayerQuit(@NotNull OnlineUser user) {
|
||||
// Players quitting have their data manually saved when the plugin is disabled
|
||||
if (disabling) {
|
||||
if (user.isNpc() || plugin.isDisabling() || plugin.isLocked(user.getUuid())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't sync players awaiting synchronization
|
||||
if (lockedPlayers.contains(user.getUuid()) || user.isNpc()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle disconnection
|
||||
try {
|
||||
lockedPlayers.add(user.getUuid());
|
||||
plugin.getRedisManager().setUserServerSwitch(user).thenRun(() -> {
|
||||
final DataSnapshot.Packed data = user.createSnapshot(DataSnapshot.SaveCause.DISCONNECT);
|
||||
plugin.getRedisManager().setUserData(user, data);
|
||||
plugin.getDatabase().addSnapshot(user, data);
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred handling a player disconnection", e);
|
||||
}
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
plugin.getDataSyncer().saveUserData(user);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,12 +75,12 @@ public abstract class EventListener {
|
||||
* @param usersInWorld a list of users in the world that is being saved
|
||||
*/
|
||||
protected final void saveOnWorldSave(@NotNull List<OnlineUser> usersInWorld) {
|
||||
if (disabling || !plugin.getSettings().doSaveOnWorldSave()) {
|
||||
if (plugin.isDisabling() || !plugin.getSettings().getSynchronization().isSaveOnWorldSave()) {
|
||||
return;
|
||||
}
|
||||
usersInWorld.stream()
|
||||
.filter(user -> !lockedPlayers.contains(user.getUuid()) && !user.isNpc())
|
||||
.forEach(user -> plugin.getDatabase().addSnapshot(
|
||||
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
|
||||
.forEach(user -> plugin.getDataSyncer().saveData(
|
||||
user, user.createSnapshot(DataSnapshot.SaveCause.WORLD_SAVE)
|
||||
));
|
||||
}
|
||||
@@ -162,41 +89,35 @@ public abstract class EventListener {
|
||||
* Handles the saving of data when a player dies
|
||||
*
|
||||
* @param user The user who died
|
||||
* @param drops The items that this user would have dropped
|
||||
* @param items The items that should be saved for this user on their death
|
||||
*/
|
||||
protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull Data.Items drops) {
|
||||
if (disabling || !plugin.getSettings().doSaveOnDeath() || lockedPlayers.contains(user.getUuid()) || user.isNpc()
|
||||
|| (!plugin.getSettings().doSaveEmptyDropsOnDeath() && drops.isEmpty())) {
|
||||
protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull Data.Items items) {
|
||||
final SaveOnDeathSettings settings = plugin.getSettings().getSynchronization().getSaveOnDeath();
|
||||
if (plugin.isDisabling() || !settings.isEnabled() || plugin.isLocked(user.getUuid())
|
||||
|| user.isNpc() || (!settings.isSaveEmptyItems() && items.isEmpty())) {
|
||||
return;
|
||||
}
|
||||
|
||||
final DataSnapshot.Packed snapshot = user.createSnapshot(DataSnapshot.SaveCause.DEATH);
|
||||
snapshot.edit(plugin, (data -> data.getInventory().ifPresent(inventory -> inventory.setContents(drops))));
|
||||
plugin.getDatabase().addSnapshot(user, snapshot);
|
||||
snapshot.edit(plugin, (data -> data.getInventory().ifPresent(inventory -> inventory.setContents(items))));
|
||||
plugin.getDataSyncer().saveData(user, snapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a player event should be canceled
|
||||
*
|
||||
* @param userUuid The UUID of the user to check
|
||||
* @return Whether the event should be canceled
|
||||
*/
|
||||
protected final boolean cancelPlayerEvent(@NotNull UUID userUuid) {
|
||||
return disabling || lockedPlayers.contains(userUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the plugin disabling
|
||||
*/
|
||||
public final void handlePluginDisable() {
|
||||
disabling = true;
|
||||
|
||||
// Save data for all online users
|
||||
// Save for all online players
|
||||
plugin.getOnlineUsers().stream()
|
||||
.filter(user -> !lockedPlayers.contains(user.getUuid()) && !user.isNpc())
|
||||
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
|
||||
.forEach(user -> {
|
||||
lockedPlayers.add(user.getUuid());
|
||||
plugin.getDatabase().addSnapshot(user, user.createSnapshot(DataSnapshot.SaveCause.SERVER_SHUTDOWN));
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
plugin.getDataSyncer().saveData(
|
||||
user,
|
||||
user.createSnapshot(DataSnapshot.SaveCause.SERVER_SHUTDOWN),
|
||||
(saved, data) -> plugin.getRedisManager().clearUserData(saved)
|
||||
);
|
||||
});
|
||||
|
||||
// Close outstanding connections
|
||||
@@ -204,10 +125,6 @@ public abstract class EventListener {
|
||||
plugin.getRedisManager().terminate();
|
||||
}
|
||||
|
||||
public final Set<UUID> getLockedPlayers() {
|
||||
return this.lockedPlayers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents priorities for events that HuskSync listens to
|
||||
*/
|
||||
@@ -245,7 +162,6 @@ public abstract class EventListener {
|
||||
return Map.entry(name().toLowerCase(), defaultPriority.name());
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@NotNull
|
||||
public static Map<String, String> getDefaults() {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Interface for doing stuff with locked users or when the plugin is disabled
|
||||
*/
|
||||
public interface LockedHandler {
|
||||
|
||||
/**
|
||||
* Get if a command should be disabled while the user is locked
|
||||
*/
|
||||
default boolean isCommandDisabled(@NotNull String label) {
|
||||
final List<String> blocked = getPlugin().getSettings().getSynchronization().getBlacklistedCommandsWhileLocked();
|
||||
return blocked.contains("*") || blocked.contains(label.toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a player event should be canceled
|
||||
*
|
||||
* @param userUuid The UUID of the user to check
|
||||
* @return Whether the event should be canceled
|
||||
*/
|
||||
default boolean cancelPlayerEvent(@NotNull UUID userUuid) {
|
||||
return getPlugin().isDisabling() || getPlugin().isLocked(userUuid);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
@@ -24,15 +24,13 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.util.Locale;
|
||||
|
||||
public enum RedisKeyType {
|
||||
CACHE(60 * 60 * 24),
|
||||
DATA_UPDATE(10),
|
||||
SERVER_SWITCH(10);
|
||||
|
||||
private final int timeToLive;
|
||||
LATEST_SNAPSHOT,
|
||||
SERVER_SWITCH,
|
||||
DATA_CHECKOUT;
|
||||
|
||||
RedisKeyType(int timeToLive) {
|
||||
this.timeToLive = timeToLive;
|
||||
}
|
||||
public static final int TTL_1_YEAR = 60 * 60 * 24 * 7 * 52; // 1 year
|
||||
public static final int TTL_10_SECONDS = 10; // 10 seconds
|
||||
|
||||
@NotNull
|
||||
public String getKeyPrefix(@NotNull String clusterId) {
|
||||
@@ -44,8 +42,4 @@ public enum RedisKeyType {
|
||||
);
|
||||
}
|
||||
|
||||
public int getTimeToLive() {
|
||||
return timeToLive;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,18 +20,16 @@
|
||||
package net.william278.husksync.redis;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.JedisPoolConfig;
|
||||
import redis.clients.jedis.JedisPubSub;
|
||||
import redis.clients.jedis.*;
|
||||
import redis.clients.jedis.exceptions.JedisException;
|
||||
import redis.clients.jedis.util.Pool;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -39,17 +37,21 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Manages the connection to the Redis server, handling the caching of user data
|
||||
* Manages the connection to Redis, handling the caching of user data
|
||||
*/
|
||||
public class RedisManager extends JedisPubSub {
|
||||
|
||||
protected static final String KEY_NAMESPACE = "husksync:";
|
||||
private static final int RECONNECTION_TIME = 8000;
|
||||
|
||||
private final HuskSync plugin;
|
||||
private final String clusterId;
|
||||
private JedisPool jedisPool;
|
||||
private Pool<Jedis> jedisPool;
|
||||
private final Map<UUID, CompletableFuture<Optional<DataSnapshot.Packed>>> pendingRequests;
|
||||
|
||||
private boolean enabled;
|
||||
private boolean reconnected;
|
||||
|
||||
public RedisManager(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
this.clusterId = plugin.getSettings().getClusterId();
|
||||
@@ -57,51 +59,98 @@ public class RedisManager extends JedisPubSub {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the redis connection pool
|
||||
* Initialize Redis connection pool
|
||||
*/
|
||||
@Blocking
|
||||
public void initialize() throws IllegalStateException {
|
||||
final String password = plugin.getSettings().getRedisPassword();
|
||||
final String host = plugin.getSettings().getRedisHost();
|
||||
final int port = plugin.getSettings().getRedisPort();
|
||||
final boolean useSSL = plugin.getSettings().redisUseSsl();
|
||||
final Settings.RedisSettings.RedisCredentials credentials = plugin.getSettings().getRedis().getCredentials();
|
||||
final String password = credentials.getPassword();
|
||||
final String host = credentials.getHost();
|
||||
final int port = credentials.getPort();
|
||||
final boolean useSSL = credentials.isUseSsl();
|
||||
|
||||
// Create the jedis pool
|
||||
final JedisPoolConfig config = new JedisPoolConfig();
|
||||
config.setMaxIdle(0);
|
||||
config.setTestOnBorrow(true);
|
||||
config.setTestOnReturn(true);
|
||||
this.jedisPool = password.isEmpty()
|
||||
? new JedisPool(config, host, port, 0, useSSL)
|
||||
: new JedisPool(config, host, port, 0, password, useSSL);
|
||||
|
||||
final Settings.RedisSettings.RedisSentinel sentinel = plugin.getSettings().getRedis().getSentinel();
|
||||
Set<String> redisSentinelNodes = new HashSet<>(sentinel.getNodes());
|
||||
if (redisSentinelNodes.isEmpty()) {
|
||||
this.jedisPool = password.isEmpty()
|
||||
? new JedisPool(config, host, port, 0, useSSL)
|
||||
: new JedisPool(config, host, port, 0, password, useSSL);
|
||||
} else {
|
||||
final String sentinelPassword = sentinel.getPassword();
|
||||
this.jedisPool = new JedisSentinelPool(sentinel.getMaster(), redisSentinelNodes, password.isEmpty()
|
||||
? null : password, sentinelPassword.isEmpty() ? null : sentinelPassword);
|
||||
}
|
||||
|
||||
// Ping the server to check the connection
|
||||
try {
|
||||
jedisPool.getResource().ping();
|
||||
} catch (JedisException e) {
|
||||
throw new IllegalStateException("Failed to establish connection with the Redis server. "
|
||||
throw new IllegalStateException("Failed to establish connection with Redis. "
|
||||
+ "Please check the supplied credentials in the config file", e);
|
||||
}
|
||||
|
||||
// Subscribe using a thread (rather than a task)
|
||||
enabled = true;
|
||||
new Thread(this::subscribe, "husksync:redis_subscriber").start();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
private void subscribe() {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.subscribe(
|
||||
this,
|
||||
Arrays.stream(RedisMessageType.values())
|
||||
.map(type -> type.getMessageChannel(clusterId))
|
||||
.toArray(String[]::new)
|
||||
);
|
||||
while (enabled && !Thread.interrupted() && jedisPool != null && !jedisPool.isClosed()) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
if (reconnected) {
|
||||
plugin.log(Level.INFO, "Redis connection is alive again");
|
||||
}
|
||||
// Subscribe channels and lock the thread
|
||||
jedis.subscribe(
|
||||
this,
|
||||
Arrays.stream(RedisMessage.Type.values())
|
||||
.map(type -> type.getMessageChannel(clusterId))
|
||||
.toArray(String[]::new)
|
||||
);
|
||||
} catch (Throwable t) {
|
||||
// Thread was unlocked due error
|
||||
onThreadUnlock(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onThreadUnlock(@NotNull Throwable t) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (reconnected) {
|
||||
plugin.log(Level.WARNING, "Redis Server connection lost. Attempting reconnect in %ss..."
|
||||
.formatted(RECONNECTION_TIME / 1000), t);
|
||||
}
|
||||
try {
|
||||
this.unsubscribe();
|
||||
} catch (Throwable ignored) {
|
||||
// empty catch
|
||||
}
|
||||
|
||||
// Make an instant subscribe if occurs any error on initialization
|
||||
if (!reconnected) {
|
||||
reconnected = true;
|
||||
} else {
|
||||
try {
|
||||
Thread.sleep(RECONNECTION_TIME);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(@NotNull String channel, @NotNull String message) {
|
||||
final RedisMessageType messageType = RedisMessageType.getTypeFromChannel(channel, clusterId).orElse(null);
|
||||
final RedisMessage.Type messageType = RedisMessage.Type.getTypeFromChannel(channel, clusterId).orElse(null);
|
||||
if (messageType == null) {
|
||||
return;
|
||||
}
|
||||
@@ -109,29 +158,50 @@ public class RedisManager extends JedisPubSub {
|
||||
final RedisMessage redisMessage = RedisMessage.fromJson(plugin, message);
|
||||
switch (messageType) {
|
||||
case UPDATE_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
|
||||
user -> user.applySnapshot(
|
||||
DataSnapshot.deserialize(plugin, redisMessage.getPayload()),
|
||||
DataSnapshot.UpdateCause.UPDATED
|
||||
)
|
||||
user -> {
|
||||
try {
|
||||
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload());
|
||||
user.applySnapshot(data, DataSnapshot.UpdateCause.UPDATED);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred updating user data from Redis", e);
|
||||
user.completeSync(false, DataSnapshot.UpdateCause.UPDATED, plugin);
|
||||
}
|
||||
}
|
||||
);
|
||||
case REQUEST_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
|
||||
user -> RedisMessage.create(
|
||||
UUID.fromString(new String(redisMessage.getPayload(), StandardCharsets.UTF_8)),
|
||||
user.createSnapshot(DataSnapshot.SaveCause.INVENTORY_COMMAND).asBytes(plugin)
|
||||
).dispatch(plugin, RedisMessageType.RETURN_USER_DATA)
|
||||
).dispatch(plugin, RedisMessage.Type.RETURN_USER_DATA)
|
||||
);
|
||||
case RETURN_USER_DATA -> {
|
||||
final CompletableFuture<Optional<DataSnapshot.Packed>> future = pendingRequests.get(
|
||||
redisMessage.getTargetUuid()
|
||||
);
|
||||
if (future != null) {
|
||||
future.complete(Optional.of(DataSnapshot.deserialize(plugin, redisMessage.getPayload())));
|
||||
try {
|
||||
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload());
|
||||
future.complete(Optional.of(data));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred returning user data from Redis", e);
|
||||
future.complete(Optional.empty());
|
||||
}
|
||||
pendingRequests.remove(redisMessage.getTargetUuid());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubscribe(String channel, int subscribedChannels) {
|
||||
plugin.log(Level.INFO, "Redis subscribed to channel '" + channel + "'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnsubscribe(String channel, int subscribedChannels) {
|
||||
plugin.log(Level.INFO, "Redis unsubscribed from channel '" + channel + "'");
|
||||
}
|
||||
|
||||
@Blocking
|
||||
protected void sendMessage(@NotNull String channel, @NotNull String message) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
@@ -142,7 +212,7 @@ public class RedisManager extends JedisPubSub {
|
||||
public void sendUserDataUpdate(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
plugin.runAsync(() -> {
|
||||
final RedisMessage redisMessage = RedisMessage.create(user.getUuid(), data.asBytes(plugin));
|
||||
redisMessage.dispatch(plugin, RedisMessageType.UPDATE_USER_DATA);
|
||||
redisMessage.dispatch(plugin, RedisMessage.Type.UPDATE_USER_DATA);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -162,10 +232,11 @@ public class RedisManager extends JedisPubSub {
|
||||
user.getUuid(),
|
||||
requestId.toString().getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
redisMessage.dispatch(plugin, RedisMessageType.REQUEST_USER_DATA);
|
||||
redisMessage.dispatch(plugin, RedisMessage.Type.REQUEST_USER_DATA);
|
||||
});
|
||||
return future.orTimeout(
|
||||
plugin.getSettings().getNetworkLatencyMilliseconds(),
|
||||
return future
|
||||
.orTimeout(
|
||||
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds(),
|
||||
TimeUnit.MILLISECONDS
|
||||
)
|
||||
.exceptionally(throwable -> {
|
||||
@@ -175,70 +246,132 @@ public class RedisManager extends JedisPubSub {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a user's data to the Redis server
|
||||
* Set a user's data to Redis
|
||||
*
|
||||
* @param user the user to set data for
|
||||
* @param data the user's data to set
|
||||
* @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
|
||||
*/
|
||||
public void setUserData(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
plugin.runAsync(() -> {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.setex(
|
||||
getKey(RedisKeyType.DATA_UPDATE, user.getUuid(), clusterId),
|
||||
RedisKeyType.DATA_UPDATE.getTimeToLive(),
|
||||
data.asBytes(plugin)
|
||||
@Blocking
|
||||
public void setUserData(@NotNull User user, @NotNull DataSnapshot.Packed data, int timeToLive) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.setex(
|
||||
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId),
|
||||
timeToLive,
|
||||
data.asBytes(plugin)
|
||||
);
|
||||
plugin.debug(String.format("[%s] Set %s key on Redis", user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred setting user data on Redis", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
public void clearUserData(@NotNull User user) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.del(
|
||||
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId)
|
||||
);
|
||||
plugin.debug(String.format("[%s] Cleared %s on Redis", user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred clearing user data on Redis", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
public void setUserCheckedOut(@NotNull User user, boolean checkedOut) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
if (checkedOut) {
|
||||
jedis.set(
|
||||
getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId),
|
||||
plugin.getServerName().getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
plugin.debug(String.format("[%s] Set %s key to redis at: %s", user.getUsername(),
|
||||
RedisKeyType.DATA_UPDATE.name(), new SimpleDateFormat("mm:ss.SSS").format(new Date())));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred setting a user's server switch", e);
|
||||
} else {
|
||||
jedis.del(getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId));
|
||||
}
|
||||
});
|
||||
plugin.debug(String.format("[%s] %s %s key to/from Redis", user.getUsername(),
|
||||
checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred setting checkout to", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
public Optional<String> getUserCheckedOut(@NotNull User user) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
final byte[] key = getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId);
|
||||
final byte[] readData = jedis.get(key);
|
||||
if (readData != null) {
|
||||
final String checkoutServer = new String(readData, StandardCharsets.UTF_8);
|
||||
plugin.debug(String.format("[%s] Waiting for %s %s key to be unset on Redis",
|
||||
user.getUsername(), checkoutServer, RedisKeyType.DATA_CHECKOUT));
|
||||
return Optional.of(checkoutServer);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred getting a user's checkout key from Redis", e);
|
||||
}
|
||||
plugin.debug(String.format("[%s] %s key not set on Redis", user.getUsername(),
|
||||
RedisKeyType.DATA_CHECKOUT));
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
public void clearUsersCheckedOutOnServer() {
|
||||
final String keyFormat = String.format("%s*", RedisKeyType.DATA_CHECKOUT.getKeyPrefix(clusterId));
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
final Set<String> keys = jedis.keys(keyFormat);
|
||||
if (keys == null) {
|
||||
plugin.log(Level.WARNING, "Checkout key returned null from Redis during clearing");
|
||||
return;
|
||||
}
|
||||
for (String key : keys) {
|
||||
if (jedis.get(key).equals(plugin.getServerName())) {
|
||||
jedis.del(key);
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred clearing this server's checkout keys on Redis", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a user's server switch to the Redis server
|
||||
* Set a user's server switch to Redis
|
||||
*
|
||||
* @param user the user to set the server switch for
|
||||
* @return a future returning void when complete
|
||||
*/
|
||||
public CompletableFuture<Void> setUserServerSwitch(@NotNull User user) {
|
||||
final CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
plugin.runAsync(() -> {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.setex(
|
||||
getKey(RedisKeyType.SERVER_SWITCH, user.getUuid(), clusterId),
|
||||
RedisKeyType.SERVER_SWITCH.getTimeToLive(), new byte[0]
|
||||
);
|
||||
future.complete(null);
|
||||
plugin.debug(String.format("[%s] Set %s key to redis at: %s", user.getUsername(),
|
||||
RedisKeyType.SERVER_SWITCH.name(), new SimpleDateFormat("mm:ss.SSS").format(new Date())));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred setting a user's server switch", e);
|
||||
}
|
||||
});
|
||||
return future;
|
||||
@Blocking
|
||||
public void setUserServerSwitch(@NotNull User user) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.setex(
|
||||
getKey(RedisKeyType.SERVER_SWITCH, user.getUuid(), clusterId),
|
||||
RedisKeyType.TTL_10_SECONDS,
|
||||
new byte[0]
|
||||
);
|
||||
plugin.debug(String.format("[%s] Set %s key to Redis",
|
||||
user.getUsername(), RedisKeyType.SERVER_SWITCH));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred setting a user's server switch key from Redis", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a user's data from the Redis server and consume the key if found
|
||||
* Fetch a user's data from Redis and consume the key if found
|
||||
*
|
||||
* @param user The user to fetch data for
|
||||
* @return The user's data, if it's present on the database. Otherwise, an empty optional.
|
||||
*/
|
||||
@Blocking
|
||||
public Optional<DataSnapshot.Packed> getUserData(@NotNull User user) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
final byte[] key = getKey(RedisKeyType.DATA_UPDATE, user.getUuid(), clusterId);
|
||||
final byte[] key = getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId);
|
||||
final byte[] dataByteArray = jedis.get(key);
|
||||
if (dataByteArray == null) {
|
||||
plugin.debug("[" + user.getUsername() + "] Could not read " +
|
||||
RedisKeyType.DATA_UPDATE.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
plugin.debug(String.format("[%s] Waiting for %s key from Redis",
|
||||
user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
|
||||
return Optional.empty();
|
||||
}
|
||||
plugin.debug("[" + user.getUsername() + "] Successfully read "
|
||||
+ RedisKeyType.DATA_UPDATE.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
plugin.debug(String.format("[%s] Read %s key from Redis",
|
||||
user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
|
||||
|
||||
// Consume the key (delete from redis)
|
||||
jedis.del(key);
|
||||
@@ -246,35 +379,36 @@ public class RedisManager extends JedisPubSub {
|
||||
// Use Snappy to decompress the json
|
||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred fetching a user's data from redis", e);
|
||||
plugin.log(Level.SEVERE, "An exception occurred getting a user's data from Redis", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
public boolean getUserServerSwitch(@NotNull User user) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
final byte[] key = getKey(RedisKeyType.SERVER_SWITCH, user.getUuid(), clusterId);
|
||||
final byte[] readData = jedis.get(key);
|
||||
if (readData == null) {
|
||||
plugin.debug("[" + user.getUsername() + "] Could not read " +
|
||||
RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
plugin.debug(String.format("[%s] Waiting for %s key from Redis",
|
||||
user.getUsername(), RedisKeyType.SERVER_SWITCH));
|
||||
return false;
|
||||
}
|
||||
plugin.debug("[" + user.getUsername() + "] Successfully read "
|
||||
+ RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
plugin.debug(String.format("[%s] Read %s key from Redis",
|
||||
user.getUsername(), RedisKeyType.SERVER_SWITCH));
|
||||
|
||||
// Consume the key (delete from redis)
|
||||
jedis.del(key);
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred fetching a user's server switch from redis", e);
|
||||
plugin.log(Level.SEVERE, "An exception occurred getting a user's server switch from Redis", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
public void terminate() {
|
||||
enabled = false;
|
||||
if (jedisPool != null) {
|
||||
if (!jedisPool.isClosed()) {
|
||||
jedisPool.close();
|
||||
|
||||
@@ -25,6 +25,9 @@ import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public class RedisMessage implements Adaptable {
|
||||
@@ -53,7 +56,7 @@ public class RedisMessage implements Adaptable {
|
||||
return plugin.getGson().fromJson(json, RedisMessage.class);
|
||||
}
|
||||
|
||||
public void dispatch(@NotNull HuskSync plugin, @NotNull RedisMessageType type) {
|
||||
public void dispatch(@NotNull HuskSync plugin, @NotNull Type type) {
|
||||
plugin.runAsync(() -> plugin.getRedisManager().sendMessage(
|
||||
type.getMessageChannel(plugin.getSettings().getClusterId()),
|
||||
plugin.getGson().toJson(this)
|
||||
@@ -77,4 +80,27 @@ public class RedisMessage implements Adaptable {
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
|
||||
UPDATE_USER_DATA,
|
||||
REQUEST_USER_DATA,
|
||||
RETURN_USER_DATA;
|
||||
|
||||
@NotNull
|
||||
public String getMessageChannel(@NotNull String clusterId) {
|
||||
return String.format(
|
||||
"%s:%s:%s",
|
||||
RedisManager.KEY_NAMESPACE.toLowerCase(Locale.ENGLISH),
|
||||
clusterId.toLowerCase(Locale.ENGLISH),
|
||||
name().toLowerCase(Locale.ENGLISH)
|
||||
);
|
||||
}
|
||||
|
||||
public static Optional<Type> getTypeFromChannel(@NotNull String channel, @NotNull String clusterId) {
|
||||
return Arrays.stream(values())
|
||||
.filter(messageType -> messageType.getMessageChannel(clusterId).equalsIgnoreCase(channel))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.redis;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
public enum RedisMessageType {
|
||||
|
||||
UPDATE_USER_DATA,
|
||||
REQUEST_USER_DATA,
|
||||
RETURN_USER_DATA;
|
||||
|
||||
@NotNull
|
||||
public String getMessageChannel(@NotNull String clusterId) {
|
||||
return String.format(
|
||||
"%s:%s:%s",
|
||||
RedisManager.KEY_NAMESPACE.toLowerCase(Locale.ENGLISH),
|
||||
clusterId.toLowerCase(Locale.ENGLISH),
|
||||
name().toLowerCase(Locale.ENGLISH)
|
||||
);
|
||||
}
|
||||
|
||||
public static Optional<RedisMessageType> getTypeFromChannel(@NotNull String channel, @NotNull String clusterId) {
|
||||
return Arrays.stream(values())
|
||||
.filter(messageType -> messageType.getMessageChannel(clusterId).equalsIgnoreCase(channel))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* 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.sync;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.api.HuskSyncAPI;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import net.william278.husksync.util.Task;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Handles the synchronization of data when a player changes servers or logs in
|
||||
*
|
||||
* @since 3.1
|
||||
*/
|
||||
public abstract class DataSyncer {
|
||||
private static final long BASE_LISTEN_ATTEMPTS = 16;
|
||||
private static final long LISTEN_DELAY = 10;
|
||||
|
||||
protected final HuskSync plugin;
|
||||
private final long maxListenAttempts;
|
||||
|
||||
@ApiStatus.Internal
|
||||
protected DataSyncer(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
this.maxListenAttempts = getMaxListenAttempts();
|
||||
}
|
||||
|
||||
/**
|
||||
* API-exposed constructor for a {@link DataSyncer}
|
||||
*
|
||||
* @param api instance of the {@link HuskSyncAPI}
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public DataSyncer(@NotNull HuskSyncAPI api) {
|
||||
this(api.getPlugin());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the plugin is enabled
|
||||
*/
|
||||
public void initialize() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the plugin is disabled
|
||||
*/
|
||||
public void terminate() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a user's data should be fetched and applied to them
|
||||
*
|
||||
* @param user the user to fetch data for
|
||||
*/
|
||||
public abstract void setUserData(@NotNull OnlineUser user);
|
||||
|
||||
/**
|
||||
* Called when a user's data should be serialized and saved
|
||||
*
|
||||
* @param user the user to save
|
||||
*/
|
||||
public abstract void saveUserData(@NotNull OnlineUser user);
|
||||
|
||||
/**
|
||||
* Save a {@link DataSnapshot.Packed user's data snapshot} to the database,
|
||||
* first firing the {@link net.william278.husksync.event.DataSaveEvent}. This will not update data on Redis.
|
||||
*
|
||||
* @param user the user to save the data for
|
||||
* @param data the data to save
|
||||
* @param after a consumer to run after data has been saved. Will be run async (off the main thread).
|
||||
* @apiNote Data will not be saved if the {@link net.william278.husksync.event.DataSaveEvent} is canceled.
|
||||
* Note that this method can also edit the data before saving it.
|
||||
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will <b>not</b> be fired if
|
||||
* {@link DataSnapshot.SaveCause#fireDataSaveEvent()} is {@code false} (e.g., with the SERVER_SHUTDOWN cause).
|
||||
* @since 3.3.2
|
||||
*/
|
||||
@Blocking
|
||||
public void saveData(@NotNull User user, @NotNull DataSnapshot.Packed data,
|
||||
@Nullable BiConsumer<User, DataSnapshot.Packed> after) {
|
||||
if (!data.getSaveCause().fireDataSaveEvent()) {
|
||||
addSnapshotToDatabase(user, data, after);
|
||||
return;
|
||||
}
|
||||
plugin.fireEvent(
|
||||
plugin.getDataSaveEvent(user, data),
|
||||
(event) -> addSnapshotToDatabase(user, data, after)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a {@link DataSnapshot.Packed user's data snapshot} to the database,
|
||||
* first firing the {@link net.william278.husksync.event.DataSaveEvent}. This will not update data on Redis.
|
||||
*
|
||||
* @param user the user to save the data for
|
||||
* @param data the data to save
|
||||
* @apiNote Data will not be saved if the {@link net.william278.husksync.event.DataSaveEvent} is canceled.
|
||||
* Note that this method can also edit the data before saving it.
|
||||
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will <b>not</b> be fired if
|
||||
* {@link DataSnapshot.SaveCause#fireDataSaveEvent()} is {@code false} (e.g., with the SERVER_SHUTDOWN cause).
|
||||
* @since 3.3.3
|
||||
*/
|
||||
public void saveData(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
saveData(user, data, null);
|
||||
}
|
||||
|
||||
// Adds a snapshot to the database and runs the after consumer
|
||||
@Blocking
|
||||
private void addSnapshotToDatabase(@NotNull User user, @NotNull DataSnapshot.Packed data,
|
||||
@Nullable BiConsumer<User, DataSnapshot.Packed> after) {
|
||||
getDatabase().addSnapshot(user, data);
|
||||
if (after != null) {
|
||||
after.accept(user, data);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculates the max attempts the system should listen for user data for based on the latency value
|
||||
private long getMaxListenAttempts() {
|
||||
return BASE_LISTEN_ATTEMPTS + (
|
||||
(Math.max(100, plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds()) / 1000)
|
||||
* 20 / LISTEN_DELAY
|
||||
);
|
||||
}
|
||||
|
||||
// Set a user's data from the database, or set them as a new user
|
||||
@ApiStatus.Internal
|
||||
protected void setUserFromDatabase(@NotNull OnlineUser user) {
|
||||
try {
|
||||
getDatabase().getLatestSnapshot(user).ifPresentOrElse(
|
||||
snapshot -> user.applySnapshot(snapshot, DataSnapshot.UpdateCause.SYNCHRONIZED),
|
||||
() -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin)
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.WARNING, "Failed to set %s's data from the database".formatted(user.getUsername()), e);
|
||||
user.completeSync(false, DataSnapshot.UpdateCause.SYNCHRONIZED, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
// Continuously listen for data from Redis
|
||||
@ApiStatus.Internal
|
||||
protected void listenForRedisData(@NotNull OnlineUser user, @NotNull Supplier<Boolean> completionSupplier) {
|
||||
final AtomicLong timesRun = new AtomicLong(0L);
|
||||
final AtomicReference<Task.Repeating> task = new AtomicReference<>();
|
||||
final AtomicBoolean processing = new AtomicBoolean(false);
|
||||
final Runnable runnable = () -> {
|
||||
if (user.isOffline()) {
|
||||
task.get().cancel();
|
||||
return;
|
||||
}
|
||||
// Ensure only one task is running at a time
|
||||
if (processing.getAndSet(true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Timeout if the plugin is disabling or the max attempts have been reached
|
||||
if (plugin.isDisabling() || timesRun.getAndIncrement() > maxListenAttempts) {
|
||||
task.get().cancel();
|
||||
plugin.debug(String.format("[%s] Redis timed out after %s attempts; setting from database",
|
||||
user.getUsername(), timesRun.get()));
|
||||
setUserFromDatabase(user);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fire the completion supplier
|
||||
if (completionSupplier.get()) {
|
||||
task.get().cancel();
|
||||
}
|
||||
processing.set(false);
|
||||
};
|
||||
task.set(plugin.getRepeatingTask(runnable, LISTEN_DELAY));
|
||||
task.get().run();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected RedisManager getRedis() {
|
||||
return plugin.getRedisManager();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected Database getDatabase() {
|
||||
return plugin.getDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the different available default modes of {@link DataSyncer}
|
||||
*
|
||||
* @since 3.1
|
||||
*/
|
||||
public enum Mode {
|
||||
LOCKSTEP(LockstepDataSyncer::new),
|
||||
DELAY(DelayDataSyncer::new);
|
||||
|
||||
private final Function<HuskSync, ? extends DataSyncer> supplier;
|
||||
|
||||
Mode(@NotNull Function<HuskSync, ? extends DataSyncer> supplier) {
|
||||
this.supplier = supplier;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public DataSyncer create(@NotNull HuskSync plugin) {
|
||||
return supplier.apply(plugin);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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.sync;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.redis.RedisKeyType;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A data syncer which applies a network delay before checking the presence of user data
|
||||
*/
|
||||
public class DelayDataSyncer extends DataSyncer {
|
||||
|
||||
public DelayDataSyncer(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserData(@NotNull OnlineUser user) {
|
||||
plugin.runAsyncDelayed(
|
||||
() -> {
|
||||
// Fetch from the database if the user isn't changing servers
|
||||
if (!getRedis().getUserServerSwitch(user)) {
|
||||
this.setUserFromDatabase(user);
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for the data to be updated
|
||||
this.listenForRedisData(
|
||||
user,
|
||||
() -> getRedis().getUserData(user).map(data -> {
|
||||
user.applySnapshot(data, DataSnapshot.UpdateCause.SYNCHRONIZED);
|
||||
return true;
|
||||
}).orElse(false)
|
||||
);
|
||||
},
|
||||
Math.max(0, plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() / 50L)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveUserData(@NotNull OnlineUser onlineUser) {
|
||||
plugin.runAsync(() -> {
|
||||
getRedis().setUserServerSwitch(onlineUser);
|
||||
saveData(
|
||||
onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
|
||||
(user, data) -> getRedis().setUserData(user, data, RedisKeyType.TTL_10_SECONDS)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.sync;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.redis.RedisKeyType;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class LockstepDataSyncer extends DataSyncer {
|
||||
|
||||
public LockstepDataSyncer(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
getRedis().clearUsersCheckedOutOnServer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
getRedis().clearUsersCheckedOutOnServer();
|
||||
}
|
||||
|
||||
// Consume their data when they are checked in
|
||||
@Override
|
||||
public void setUserData(@NotNull OnlineUser user) {
|
||||
this.listenForRedisData(user, () -> {
|
||||
if (getRedis().getUserCheckedOut(user).isPresent()) {
|
||||
return false;
|
||||
}
|
||||
getRedis().setUserCheckedOut(user, true);
|
||||
getRedis().getUserData(user).ifPresentOrElse(
|
||||
data -> user.applySnapshot(data, DataSnapshot.UpdateCause.SYNCHRONIZED),
|
||||
() -> this.setUserFromDatabase(user)
|
||||
);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveUserData(@NotNull OnlineUser onlineUser) {
|
||||
plugin.runAsync(() -> {
|
||||
getRedis().setUserServerSwitch(onlineUser);
|
||||
saveData(
|
||||
onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
|
||||
(user, data) -> {
|
||||
getRedis().setUserData(user, data, RedisKeyType.TTL_1_YEAR);
|
||||
getRedis().setUserCheckedOut(user, false);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
package net.william278.husksync.user;
|
||||
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.platform.AudienceProvider;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class ConsoleUser implements CommandUser {
|
||||
@@ -27,8 +28,8 @@ public final class ConsoleUser implements CommandUser {
|
||||
@NotNull
|
||||
private final Audience audience;
|
||||
|
||||
public ConsoleUser(@NotNull Audience console) {
|
||||
this.audience = console;
|
||||
public ConsoleUser(@NotNull AudienceProvider audiences) {
|
||||
this.audience = audiences.console();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
package net.william278.husksync.user;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import de.themoep.minedown.adventure.MineDownParser;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.william278.husksync.HuskSync;
|
||||
@@ -50,13 +49,11 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
||||
*/
|
||||
public abstract boolean isOffline();
|
||||
|
||||
/**
|
||||
* Get the player's adventure {@link Audience}
|
||||
*
|
||||
* @return the player's {@link Audience}
|
||||
*/
|
||||
@NotNull
|
||||
public abstract Audience getAudience();
|
||||
@Override
|
||||
public Audience getAudience() {
|
||||
return getPlugin().getAudience(getUuid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to this player
|
||||
@@ -73,9 +70,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
||||
* @param mineDown the parsed {@link MineDown} to send
|
||||
*/
|
||||
public void sendMessage(@NotNull MineDown mineDown) {
|
||||
sendMessage(mineDown
|
||||
.disable(MineDownParser.Option.SIMPLE_FORMATTING)
|
||||
.replace().toComponent());
|
||||
sendMessage(mineDown.toComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,9 +79,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
||||
* @param mineDown the parsed {@link MineDown} to send
|
||||
*/
|
||||
public void sendActionBar(@NotNull MineDown mineDown) {
|
||||
getAudience().sendActionBar(mineDown
|
||||
.disable(MineDownParser.Option.SIMPLE_FORMATTING)
|
||||
.replace().toComponent());
|
||||
getAudience().sendActionBar(mineDown.toComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,6 +124,9 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
||||
public void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull DataSnapshot.UpdateCause cause) {
|
||||
getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> {
|
||||
if (!isOffline()) {
|
||||
getPlugin().debug(String.format("Applying snapshot (%s) to %s (cause: %s)",
|
||||
snapshot.getShortId(), getUsername(), cause.getDisplayName()
|
||||
));
|
||||
UserDataHolder.super.applySnapshot(
|
||||
event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin())
|
||||
);
|
||||
@@ -146,7 +142,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
||||
*/
|
||||
public void completeSync(boolean succeeded, @NotNull DataSnapshot.UpdateCause cause, @NotNull HuskSync plugin) {
|
||||
if (succeeded) {
|
||||
switch (plugin.getSettings().getNotificationDisplaySlot()) {
|
||||
switch (plugin.getSettings().getSynchronization().getNotificationDisplaySlot()) {
|
||||
case CHAT -> cause.getCompletedLocale(plugin).ifPresent(this::sendMessage);
|
||||
case ACTION_BAR -> cause.getCompletedLocale(plugin).ifPresent(this::sendActionBar);
|
||||
case TOAST -> cause.getCompletedLocale(plugin)
|
||||
@@ -158,7 +154,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
||||
}
|
||||
plugin.fireEvent(
|
||||
plugin.getSyncCompleteEvent(this),
|
||||
(event) -> plugin.getLockedPlayers().remove(getUuid())
|
||||
(event) -> plugin.unlockPlayer(getUuid())
|
||||
);
|
||||
} else {
|
||||
cause.getFailedLocale(plugin).ifPresent(this::sendMessage);
|
||||
|
||||
@@ -115,7 +115,7 @@ public class DataDumper {
|
||||
} else {
|
||||
return "(Failed to upload to logs site, got: " + connection.getResponseCode() + ")";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "Failed to upload data to logs site", e);
|
||||
}
|
||||
return "(Failed to upload to logs site)";
|
||||
|
||||
@@ -27,6 +27,7 @@ import net.william278.paginedown.PaginatedList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@@ -46,18 +47,19 @@ public class DataSnapshotList {
|
||||
final AtomicInteger snapshotNumber = new AtomicInteger(1);
|
||||
this.paginatedList = PaginatedList.of(snapshots.stream()
|
||||
.map(snapshot -> plugin.getLocales()
|
||||
.getRawLocale("data_list_item",
|
||||
.getRawLocale(!snapshot.isInvalid() ? "data_list_item" : "data_list_item_invalid",
|
||||
getNumberIcon(snapshotNumber.getAndIncrement()),
|
||||
dataOwner.getUsername(),
|
||||
snapshot.getId().toString(),
|
||||
snapshot.getShortId(),
|
||||
snapshot.isPinned() ? "※" : " ",
|
||||
snapshot.getTimestamp().format(DateTimeFormatter
|
||||
.ofPattern("dd/MM/yyyy, HH:mm")),
|
||||
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)),
|
||||
snapshot.getTimestamp().format(DateTimeFormatter
|
||||
.ofPattern("MMM dd yyyy, HH:mm:ss.SSS")),
|
||||
snapshot.getSaveCause().getDisplayName(),
|
||||
String.format("%.2fKiB", snapshot.getFileSize(plugin) / 1024f))
|
||||
.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.MEDIUM)),
|
||||
snapshot.getSaveCause().getLocale(plugin),
|
||||
String.format("%.2fKiB", snapshot.getFileSize(plugin) / 1024f),
|
||||
snapshot.isInvalid() ? snapshot.getInvalidReason(plugin) : "")
|
||||
.orElse("• " + snapshot.getId())).toList(),
|
||||
plugin.getLocales().getBaseChatList(6)
|
||||
.setHeaderFormat(plugin.getLocales()
|
||||
|
||||
@@ -28,6 +28,7 @@ import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
@@ -61,28 +62,32 @@ public class DataSnapshotOverview {
|
||||
dataOwner.getUsername(), dataOwner.getUuid().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
locales.getLocale("data_manager_timestamp",
|
||||
snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("MMM dd yyyy, HH:mm:ss.SSS")),
|
||||
snapshot.getTimestamp().format(DateTimeFormatter
|
||||
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)),
|
||||
snapshot.getTimestamp().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
if (snapshot.isPinned()) {
|
||||
locales.getLocale("data_manager_pinned")
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
locales.getLocale("data_manager_cause", snapshot.getSaveCause().getDisplayName())
|
||||
locales.getLocale("data_manager_cause", snapshot.getSaveCause().getLocale(plugin))
|
||||
.ifPresent(user::sendMessage);
|
||||
locales.getLocale("data_manager_server", snapshot.getServerName())
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
// User status data, if present in the snapshot
|
||||
final Optional<Data.Health> health = snapshot.getHealth();
|
||||
final Optional<Data.Attributes> attributes = snapshot.getAttributes();
|
||||
final Optional<Data.Hunger> food = snapshot.getHunger();
|
||||
final Optional<Data.Experience> experience = snapshot.getExperience();
|
||||
final Optional<Data.GameMode> gameMode = snapshot.getGameMode();
|
||||
if (health.isPresent() && food.isPresent() && experience.isPresent() && gameMode.isPresent()) {
|
||||
final Optional<Data.Experience> exp = snapshot.getExperience();
|
||||
final Optional<Data.GameMode> mode = snapshot.getGameMode();
|
||||
if (health.isPresent() && attributes.isPresent() && food.isPresent() && exp.isPresent() && mode.isPresent()) {
|
||||
locales.getLocale("data_manager_status",
|
||||
Integer.toString((int) health.get().getHealth()),
|
||||
Integer.toString((int) health.get().getMaxHealth()),
|
||||
Integer.toString((int) attributes.get().getMaxHealth()),
|
||||
Integer.toString(food.get().getFoodLevel()),
|
||||
Integer.toString(experience.get().getExpLevel()),
|
||||
gameMode.get().getGameMode().toLowerCase(Locale.ENGLISH))
|
||||
Integer.toString(exp.get().getExpLevel()),
|
||||
mode.get().getGameMode().toLowerCase(Locale.ENGLISH))
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ public abstract class LegacyConverter {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public abstract DataSnapshot.Packed convert(@NotNull byte[] data, @NotNull UUID id,
|
||||
public abstract DataSnapshot.Packed convert(byte @NotNull [] data, @NotNull UUID id,
|
||||
@NotNull OffsetDateTime timestamp) throws DataAdapter.AdaptionException;
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.UserDataHolder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@@ -86,7 +88,7 @@ public interface Task extends Runnable {
|
||||
interface Supplier {
|
||||
|
||||
@NotNull
|
||||
Task.Sync getSyncTask(@NotNull Runnable runnable, long delayTicks);
|
||||
Task.Sync getSyncTask(@NotNull Runnable runnable, @Nullable UserDataHolder user, long delayTicks);
|
||||
|
||||
@NotNull
|
||||
Task.Async getAsyncTask(@NotNull Runnable runnable, long delayTicks);
|
||||
@@ -95,8 +97,8 @@ public interface Task extends Runnable {
|
||||
Task.Repeating getRepeatingTask(@NotNull Runnable runnable, long repeatingTicks);
|
||||
|
||||
@NotNull
|
||||
default Task.Sync runSyncDelayed(@NotNull Runnable runnable, long delayTicks) {
|
||||
final Task.Sync task = getSyncTask(runnable, delayTicks);
|
||||
default Task.Sync runSyncDelayed(@NotNull Runnable runnable, @Nullable UserDataHolder user, long delayTicks) {
|
||||
final Task.Sync task = getSyncTask(runnable, user, delayTicks);
|
||||
task.run();
|
||||
return task;
|
||||
}
|
||||
@@ -109,7 +111,12 @@ public interface Task extends Runnable {
|
||||
|
||||
@NotNull
|
||||
default Task.Sync runSync(@NotNull Runnable runnable) {
|
||||
return runSyncDelayed(runnable, 0);
|
||||
return runSyncDelayed(runnable, null, 0);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Task.Sync runSync(@NotNull Runnable runnable, @NotNull UserDataHolder user) {
|
||||
return runSyncDelayed(runnable, user, 0);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
22
common/src/main/resources/database/postgresql_schema.sql
Normal file
22
common/src/main/resources/database/postgresql_schema.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Create the users table if it does not exist
|
||||
CREATE TABLE IF NOT EXISTS "%users_table%"
|
||||
(
|
||||
uuid uuid NOT NULL UNIQUE,
|
||||
username varchar(16) NOT NULL,
|
||||
|
||||
PRIMARY KEY (uuid)
|
||||
);
|
||||
|
||||
-- Create the user data table if it does not exist
|
||||
CREATE TABLE IF NOT EXISTS "%user_data_table%"
|
||||
(
|
||||
version_uuid uuid NOT NULL UNIQUE,
|
||||
player_uuid uuid NOT NULL,
|
||||
timestamp timestamp NOT NULL,
|
||||
save_cause varchar(32) NOT NULL,
|
||||
pinned boolean NOT NULL DEFAULT FALSE,
|
||||
data longblob NOT NULL,
|
||||
|
||||
PRIMARY KEY (version_uuid, player_uuid),
|
||||
FOREIGN KEY (player_uuid) REFERENCES "%users_table%" (uuid) ON DELETE CASCADE
|
||||
);
|
||||
@@ -1,49 +1,65 @@
|
||||
synchronization_complete: '[⏵ Данните синхронизирани!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Провалихме се да синхронизираме Вашите данни! Моля свържете се с администратор.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0Инвентара на %1%'
|
||||
ender_chest_viewer_menu_title: '&0Ендър Сандъка на %1%'
|
||||
inventory_viewer_opened: '[Преглеждане снапшота на](#00fb9a) [%1%](#00fb9a bold) [ инвентар от ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Преглеждане снапшота на](#00fb9a) [%1%](#00fb9a bold) [ Ендър Сандък от ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Вашите данни бяха обновени!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Провалихме се да обновим Вашите данни! Моля свържете се с администратор.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
data_manager_title: '[Преглеждане потребителският снапшот](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%](#00fb9a bold show_text=&7UUID на Играча:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Клеймо на Версията:\n&8Когато данните са били запазени)'
|
||||
data_manager_pinned: '[※ Закачен снапшот](#d8ff2b show_text=&7Закачен:\n&8Снапшота на този потребител няма да бъде автоматично завъртан.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Причина на Запазване:\n&8Какво е накарало данните да бъдат запазени)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Точки кръв) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Точки глад) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Ниво опит) [🏹 %5%](dark_aqua show_text=&7Режим на игра)'
|
||||
data_manager_advancements_statistics: '[⭐ Напредъци: %1%](color=#ffc43b-#f5c962 show_text=&7Напредъци, в които имате прогрес:\n&8%2%) [⌛ Изиграно Време: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Изиграно време в играта\n&8⚠ Базирано на статистики от играта)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Инвентар…]](color=#a17b5f-#f5b98c show_text=&7Натиснете, за да прегледате run_command=/inventory %1% %2%) [[⌀ Ендър Сандък…]](#b649c4-#d254ff show_text=&7Натиснете, за да проверите run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Управление:](gray) [[❌ Изтрий…]](#ff3300 show_text=&7Натиснете, за да изтриете този снапшот от потребителски данни.\n&8Това няма да засегне текущите данни на потребителя.\n&#ff3300&⚠ Това не може да бъде отменено! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Възстанови…]](#00fb9a show_text=&7Натиснете, за да възстановите тези потребителски данни.\n&8Това ще зададе данните на потребителя на този снапшот.\n&#ff3300&⚠ Текущите данни на %1% ще бъдат пренаписани! suggest_command=/husksync:userdata restore %1% %2%) [[※ Закачи/Откачи…]](#d8ff2b show_text=&7Натиснете, за да закачите или откачите този снапшот с потребителски данни\n&8Закачените снапшоти няма да бъдат автоматично завъртани run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'и още %1%…'
|
||||
data_list_title: '[Лист от](#00fb9a) [снапшоти на данните на потребителя](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_deleted: '[❌ Успешно изтрихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
||||
data_restored: '[⏪ Успешно възстановихме](#00fb9a) [текущите потребителски данни за](#00fb9a) [%1%](#00fb9a show_text=&7UUID на Играча:\n&8%2%) [от снапшот](#00fb9a) [%3%.](#00fb9a show_text=&7Версия на UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Успешно закачихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
||||
data_unpinned: '[※ Успешно откачихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| You are running the latest version of HuskSync (v%1%).](#00fb9a)'
|
||||
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)'
|
||||
error_invalid_syntax: '[Грешка:](#ff3300) [Неправилен синтаксис. Използвайте:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Грешка:](#ff3300) [Не можахме да открием играч с това име.](#ff7e5e)'
|
||||
error_no_permission: '[Грешка:](#ff3300) [Нямате право да използвате тази команда](#ff7e5e)'
|
||||
error_console_command_only: '[Грешка:](#ff3300) [Тази команда може да бъде използвана единствено през конзолата](#ff7e5e)'
|
||||
error_in_game_command_only: 'Грешка: Тази команда може да бъде използвана само от играта.'
|
||||
error_no_data_to_display: '[Грешка:](#ff3300) [Не можахме да открием никакви данни за потребителя, които да покажем.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Грешка:](#ff3300) [Не можахме да открием никакви потребителски данни за тази версия на това UUID.](#ff7e5e)'
|
||||
husksync_command_description: 'Manage the HuskSync plugin'
|
||||
userdata_command_description: 'View, manage & restore player userdata'
|
||||
inventory_command_description: 'View & edit a player''s inventory'
|
||||
enderchest_command_description: 'View & edit a player''s Ender Chest'
|
||||
locales:
|
||||
synchronization_complete: '[⏵ Данните синхронизирани!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Провалихме се да синхронизираме Вашите данни! Моля свържете се с администратор.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0Инвентара на %1%'
|
||||
ender_chest_viewer_menu_title: '&0Ендър Сандъка на %1%'
|
||||
inventory_viewer_opened: '[Преглеждане снапшота на](#00fb9a) [%1%](#00fb9a bold) [ инвентар от ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Преглеждане снапшота на](#00fb9a) [%1%](#00fb9a bold) [ Ендър Сандък от ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Вашите данни бяха обновени!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Провалихме се да обновим Вашите данни! Моля свържете се с администратор.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
data_manager_title: '[Преглеждане потребителският снапшот](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%](#00fb9a bold show_text=&7UUID на Играча:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Клеймо на Версията:\n&8Когато данните са били запазени)'
|
||||
data_manager_pinned: '[※ Закачен снапшот](#d8ff2b show_text=&7Закачен:\n&8Снапшота на този потребител няма да бъде автоматично завъртан.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Причина на Запазване:\n&8Какво е накарало данните да бъдат запазени)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Точки кръв) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Точки глад) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Ниво опит) [🏹 %5%](dark_aqua show_text=&7Режим на игра)'
|
||||
data_manager_advancements_statistics: '[⭐ Напредъци: %1%](color=#ffc43b-#f5c962 show_text=&7Напредъци, в които имате прогрес:\n&8%2%) [⌛ Изиграно Време: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Изиграно време в играта\n&8⚠ Базирано на статистики от играта)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Инвентар…]](color=#a17b5f-#f5b98c show_text=&7Натиснете, за да прегледате run_command=/inventory %1% %2%) [[⌀ Ендър Сандък…]](#b649c4-#d254ff show_text=&7Натиснете, за да проверите run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Управление:](gray) [[❌ Изтрий…]](#ff3300 show_text=&7Натиснете, за да изтриете този снапшот от потребителски данни.\n&8Това няма да засегне текущите данни на потребителя.\n&#ff3300&⚠ Това не може да бъде отменено! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Възстанови…]](#00fb9a show_text=&7Натиснете, за да възстановите тези потребителски данни.\n&8Това ще зададе данните на потребителя на този снапшот.\n&#ff3300&⚠ Текущите данни на %1% ще бъдат пренаписани! suggest_command=/husksync:userdata restore %1% %2%) [[※ Закачи/Откачи…]](#d8ff2b show_text=&7Натиснете, за да закачите или откачите този снапшот с потребителски данни\n&8Закачените снапшоти няма да бъдат автоматично завъртани run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'и още %1%…'
|
||||
data_list_title: '[Лист от](#00fb9a) [снапшоти на данните на потребителя](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_deleted: '[❌ Успешно изтрихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
||||
data_restored: '[⏪ Успешно възстановихме](#00fb9a) [текущите потребителски данни за](#00fb9a) [%1%](#00fb9a show_text=&7UUID на Играча:\n&8%2%) [от снапшот](#00fb9a) [%3%.](#00fb9a show_text=&7Версия на UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Успешно закачихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
||||
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%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: 'disconnect'
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB migration'
|
||||
save_cause_legacy_migration: 'legacy migration'
|
||||
save_cause_converted_from_v2: 'converted from v2'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| You are running the latest version of HuskSync (v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Презаредихме конфигурацията и файловете със съобщения.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
error_invalid_syntax: '[Грешка:](#ff3300) [Неправилен синтаксис. Използвайте:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Грешка:](#ff3300) [Не можахме да открием играч с това име.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[Грешка:](#ff3300) [Нямате право да използвате тази команда](#ff7e5e)'
|
||||
error_console_command_only: '[Грешка:](#ff3300) [Тази команда може да бъде използвана единствено през конзолата](#ff7e5e)'
|
||||
error_in_game_command_only: 'Грешка: Тази команда може да бъде използвана само от играта.'
|
||||
error_no_data_to_display: '[Грешка:](#ff3300) [Не можахме да открием никакви данни за потребителя, които да покажем.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Грешка:](#ff3300) [Не можахме да открием никакви потребителски данни за тази версия на това UUID.](#ff7e5e)'
|
||||
husksync_command_description: 'Manage the HuskSync plugin'
|
||||
userdata_command_description: 'View, manage & restore player userdata'
|
||||
inventory_command_description: 'View & edit a player''s inventory'
|
||||
enderchest_command_description: 'View & edit a player''s Ender Chest'
|
||||
@@ -1,49 +1,65 @@
|
||||
synchronization_complete: '[⏵ Daten synchronisiert!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Ein Fehler ist beim Synchronisieren deiner Daten aufgetreten! Bitte kontaktiere einen Administrator.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0Inventar von %1%'
|
||||
ender_chest_viewer_menu_title: '&0Endertruhe von %1%'
|
||||
inventory_viewer_opened: '[Du siehst den Schnappschuss des Inventares von](#00fb9a) [%1%](#00fb9a bold) [von ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Du siehst den Schnappschuss der Endertruhe von](#00fb9a) [%1%](#00fb9a bold) [von ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Deine Daten wurden aktualisiert!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Ein Fehler ist beim Aktualisieren deiner Daten aufgetreten! Bitte kontaktiere einen Administrator.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
data_manager_title: '[Du siehst den Nutzerdaten-Schnappschuss](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für %3%](#00fb9a bold show_text=&7Spieler-UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Versions-Zeitstempel:\n&8Zeitpunkt der Speicherung der Daten)'
|
||||
data_manager_pinned: '[※ Schnappschuss angeheftet](#d8ff2b show_text=&7Angeheftet:\n&8Dieser Nutzerdaten-Schnappschuss wird nicht automatisch rotiert.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Speicherungsgrund:\n&8Der Grund für das Speichern der Daten)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Lebenspunkte) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hungerpunkte) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP-Level) [🏹 %5%](dark_aqua show_text=&7Spielmodus)'
|
||||
data_manager_advancements_statistics: '[⭐ Erfolge: %1%](color=#ffc43b-#f5c962 show_text=&7Erfolge in denen du Fortschritt gemacht hast:\n&8%2%) [⌛ Spielzeit: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Deine verbrachte Zeit im Spiel\n&8⚠ Basierend auf Spielstatistiken)\n'
|
||||
data_manager_item_buttons: '[Sehen:](gray) [[🪣 Inventar…]](color=#a17b5f-#f5b98c show_text=&7Klicke zum Ansehen run_command=/inventory %1% %2%) [[⌀ Endertruhe…]](#b649c4-#d254ff show_text=&7Klicke zum Ansehen run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Verwalten:](gray) [[❌ Löschen…]](#ff3300 show_text=&7Klicke, um diesen Nutzerdaten-Schnappschuss zu löschen.\n&8Dies betrifft nicht die aktuellen Nutzerdaten.\n&#ff3300&⚠ Dieser Schritt kann nicht rückgängig gemacht werden! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Wiederherstellen…]](#00fb9a show_text=&7Klicke, um die Nutzerdaten wiederherzustellen.\n&8Dies wird die Nutzerdaten auf den Stand des Schnappschusses setzen.\n&#ff3300&⚠ Die aktuellen Nutzerdaten von %1% werden überschrieben! suggest_command=/husksync:userdata restore %1% %2%) [[※ Anheften/Loslösen…]](#d8ff2b show_text=&7Klicke, um diesen Nutzerdaten-Schnappschuss anzuheften oder loszulösen\n&8Angeheftete Nutzerdaten-Schnappschüsse werden nicht automatisch rotiert run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ Daten-Dump…]](dark_gray show_text=&7Klicke, um diesen rohen Nutzerdaten-Schnappschuss in eine Datei zu speichern.\n&8Daten-Dumps können unter ~/plugins/HuskSync/dumps/ gefunden werden. run_command=/husksync:userdata dump %1% %2% file) [[☂ Web-Dump…]](dark_gray show_text=&7Klicke, um diesen rohen Nutzerdaten-Schnappschuss auf den mc-logs Service hochzuladen.\n&8Du erhältst dann eine URL, die die Daten enthält. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'und %1% weitere…'
|
||||
data_list_title: '[Nutzerdaten-Schnappschüsse von %1%:](#00fb9a) [(%2%-%3% von](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_deleted: '[❌ Nutzerdaten-Schnappschuss erfolgreich gelöscht](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Erfgreich wiederhergestellt](#00fb9a) [Aktuelle Nutzerdaten des Schnappschusses von %1%](#00fb9a show_text=&7Spieler-UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versions-UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Nutzerdaten-Schnappschuss erfolgreich angepinnt](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ Nutzerdaten-Schnappschuss erfolgreich losgelöst](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Nutzerdaten-Schnappschuss %1% für %2% erfolgreich gedumpt nach:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Seite](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7Siehe vorherige Seite run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7Siehe nächste Seite run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Springe zu Seite %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Die Konfigurations- und Sprachdateien wurden neu geladen.](#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)'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| You are running the latest version of HuskSync (v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
|
||||
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_no_permission: '[Fehler:](#ff3300) [Du hast nicht die benötigten Berechtigungen um diesen Befehl auszuführen](#ff7e5e)'
|
||||
error_console_command_only: '[Fehler:](#ff3300) [Dieser Befehl kann nur über die Konsole ausgeführt werden.](#ff7e5e)'
|
||||
error_in_game_command_only: 'Fehler: Dieser Befehl kann nur im Spiel genutzt werden.'
|
||||
error_no_data_to_display: '[Fehler:](#ff3300) [Es konnten keine Nutzerdaten zum Anzeigen gefunden werden.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Fehler:](#ff3300) [Es konnten keine Nutzerdaten für diese Versions-UUID gefunden werden.](#ff7e5e)'
|
||||
husksync_command_description: 'Manage the HuskSync plugin'
|
||||
userdata_command_description: 'View, manage & restore player userdata'
|
||||
inventory_command_description: 'View & edit a player''s inventory'
|
||||
enderchest_command_description: 'View & edit a player''s Ender Chest'
|
||||
locales:
|
||||
synchronization_complete: '[⏵ Daten synchronisiert!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Ein Fehler ist beim Synchronisieren deiner Daten aufgetreten! Bitte kontaktiere einen Administrator.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0Inventar von %1%'
|
||||
ender_chest_viewer_menu_title: '&0Endertruhe von %1%'
|
||||
inventory_viewer_opened: '[Du siehst den Schnappschuss des Inventares von](#00fb9a) [%1%](#00fb9a bold) [von ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Du siehst den Schnappschuss der Endertruhe von](#00fb9a) [%1%](#00fb9a bold) [von ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Deine Daten wurden aktualisiert!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Ein Fehler ist beim Aktualisieren deiner Daten aufgetreten! Bitte kontaktiere einen Administrator.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ Benutzer-Registrierung abgeschlossen!](#00fb9a)'
|
||||
data_manager_title: '[Du siehst den Nutzerdaten-Schnappschuss](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für %3%](#00fb9a bold show_text=&7Spieler-UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Versions-Zeitstempel:\n&8Zeitpunkt der Speicherung der Daten)'
|
||||
data_manager_pinned: '[※ Schnappschuss angeheftet](#d8ff2b show_text=&7Angeheftet:\n&8Dieser Nutzerdaten-Schnappschuss wird nicht automatisch rotiert.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Speicherungsgrund:\n&8Der Grund für das Speichern der Daten)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name des Servers, auf dem die Daten gespeichert wurden)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Schnappschuss-Größe:\n&8Geschätzte Dateigröße des Schnappschusses (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Lebenspunkte) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hungerpunkte) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP-Level) [🏹 %5%](dark_aqua show_text=&7Spielmodus)'
|
||||
data_manager_advancements_statistics: '[⭐ Erfolge: %1%](color=#ffc43b-#f5c962 show_text=&7Erfolge in denen du Fortschritt gemacht hast:\n&8%2%) [⌛ Spielzeit: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Deine verbrachte Zeit im Spiel\n&8⚠ Basierend auf Spielstatistiken)\n'
|
||||
data_manager_item_buttons: '[Sehen:](gray) [[🪣 Inventar…]](color=#a17b5f-#f5b98c show_text=&7Klicke zum Ansehen run_command=/inventory %1% %2%) [[⌀ Endertruhe…]](#b649c4-#d254ff show_text=&7Klicke zum Ansehen run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Verwalten:](gray) [[❌ Löschen…]](#ff3300 show_text=&7Klicke, um diesen Nutzerdaten-Schnappschuss zu löschen.\n&8Dies betrifft nicht die aktuellen Nutzerdaten.\n&#ff3300&⚠ Dieser Schritt kann nicht rückgängig gemacht werden! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Wiederherstellen…]](#00fb9a show_text=&7Klicke, um die Nutzerdaten wiederherzustellen.\n&8Dies wird die Nutzerdaten auf den Stand des Schnappschusses setzen.\n&#ff3300&⚠ Die aktuellen Nutzerdaten von %1% werden überschrieben! suggest_command=/husksync:userdata restore %1% %2%) [[※ Anheften/Loslösen…]](#d8ff2b show_text=&7Klicke, um diesen Nutzerdaten-Schnappschuss anzuheften oder loszulösen\n&8Angeheftete Nutzerdaten-Schnappschüsse werden nicht automatisch rotiert run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ Daten-Dump…]](dark_gray show_text=&7Klicke, um diesen rohen Nutzerdaten-Schnappschuss in eine Datei zu speichern.\n&8Daten-Dumps können unter ~/plugins/HuskSync/dumps/ gefunden werden. run_command=/husksync:userdata dump %1% %2% file) [[☂ Web-Dump…]](dark_gray show_text=&7Klicke, um diesen rohen Nutzerdaten-Schnappschuss auf den mc-logs Service hochzuladen.\n&8Du erhältst dann eine URL, die die Daten enthält. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'und %1% weitere…'
|
||||
data_list_title: '[Nutzerdaten-Schnappschüsse von %1%:](#00fb9a) [(%2%-%3% von](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Nutzerdaten-Schnappschuss für %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Angeheftet:\n&8Angeheftete Schnappschüsse werden nicht automatisch rotiert. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versions-Zeitstempel:&7\n&8Zeitpunkt der Speicherung der Daten\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Speicherungsgrund:\n&8Grund für das Speichern der Daten run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Schnappschuss-Größe:&7\n&8Geschätzte Dateigröße des Schnappschusses (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_deleted: '[❌ Nutzerdaten-Schnappschuss erfolgreich gelöscht](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Erfgreich wiederhergestellt](#00fb9a) [Aktuelle Nutzerdaten des Schnappschusses von %1%](#00fb9a show_text=&7Spieler-UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versions-UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Nutzerdaten-Schnappschuss erfolgreich angepinnt](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)'
|
||||
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%'
|
||||
list_footer: '\n%1%[Seite](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7Siehe vorherige Seite run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7Siehe nächste Seite run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Springe zu Seite %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: 'Server verlassen'
|
||||
save_cause_world_save: 'Welt gespeichert'
|
||||
save_cause_death: 'Tod'
|
||||
save_cause_server_shutdown: 'Server gestoppt'
|
||||
save_cause_inventory_command: 'Inventar Befehl'
|
||||
save_cause_enderchest_command: 'Enderchest Befehl'
|
||||
save_cause_backup_restore: 'Backup wiederhergestellt'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB Migration'
|
||||
save_cause_legacy_migration: 'Legacy Migration'
|
||||
save_cause_converted_from_v2: 'Import von v2'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Die Konfigurations- und Sprachdateien wurden neu geladen.](#00fb9a)\n[⚠ Stelle sicher, dass die Konfigurationsdateien auf allen Servern aktuell sind!](#00fb9a)\n[Ein Neustart wird benötigt, damit Konfigurations-Änderungen wirkbar werden.](#00fb9a italic)'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| Du verwendest die neuste Version von HuskSync (v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| Eine neue Version von HuskSync ist verfügbar: v%1% (Aktuelle Version: v%2%).](#ff7e5e)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
error_invalid_syntax: '[Fehler:](#ff3300) [Falsche Syntax. Nutze:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Fehler:](#ff3300) [Es konnte kein Spieler mit diesem Namen gefunden werden.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[Fehler:](#ff3300) [Du hast nicht die benötigten Berechtigungen um diesen Befehl auszuführen](#ff7e5e)'
|
||||
error_console_command_only: '[Fehler:](#ff3300) [Dieser Befehl kann nur über die Konsole ausgeführt werden.](#ff7e5e)'
|
||||
error_in_game_command_only: 'Fehler: Dieser Befehl kann nur im Spiel genutzt werden.'
|
||||
error_no_data_to_display: '[Fehler:](#ff3300) [Es konnten keine Nutzerdaten zum Anzeigen gefunden werden.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Fehler:](#ff3300) [Es konnten keine Nutzerdaten für diese Versions-UUID gefunden werden.](#ff7e5e)'
|
||||
husksync_command_description: 'Das HuskSync-Plugin verwalten'
|
||||
userdata_command_description: 'Nutzerdaten eines Spielers anzeigen, verwalten und wiederherstellen'
|
||||
inventory_command_description: 'Inventar eines Spielers ansehen und bearbeiten'
|
||||
enderchest_command_description: 'Endertruhe eines Spielers ansehen und bearbeiten'
|
||||
|
||||
@@ -1,49 +1,65 @@
|
||||
synchronization_complete: '[⏵ Data synchronized!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Failed to synchronize your data! Please contact an administrator.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1%''s Inventory'
|
||||
ender_chest_viewer_menu_title: '&0%1%''s Ender Chest'
|
||||
inventory_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold)[''s inventory as of ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold)[''s Ender Chest as of ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Your data has been updated!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Failed to update your data! Please contact an administrator.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
data_manager_title: '[Viewing user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\n&8When the data was saved)'
|
||||
data_manager_pinned: '[※ Snapshot pinned](#d8ff2b show_text=&7Pinned:\n&8This user data snapshot won''t be automatically rotated.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Health points) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hunger points) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)'
|
||||
data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements you have progress in:\n&8%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\n&8⚠ Based on in-game statistics)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventory…]](color=#a17b5f-#f5b98c show_text=&7Click to view run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Click to view run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ Delete…]](#ff3300 show_text=&7Click to delete this snapshot of user data.\n&8This will not affect the user''s current data.\n&#ff3300&⚠ This cannot be undone! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Restore…]](#00fb9a show_text=&7Click to restore this user data.\n&8This will set the user''s data to this snapshot.\n&#ff3300&⚠ %1%''s current data will be overwritten! suggest_command=/husksync:userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click to pin or unpin this user data snapshot\n&8Pinned snapshots won''t be automatically rotated run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'and %1% more…'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ Successfully unpinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| You are running the latest version of HuskSync (v%1%).](#00fb9a)'
|
||||
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)'
|
||||
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_no_permission: '[Error:](#ff3300) [You do not have permission to execute this command](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [That command can only be run through console](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: That command can only be used in-game.'
|
||||
error_no_data_to_display: '[Error:](#ff3300) [Could not find any user data to display.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Error:](#ff3300) [Could not find any user data for that version UUID.](#ff7e5e)'
|
||||
husksync_command_description: 'Manage the HuskSync plugin'
|
||||
userdata_command_description: 'View, manage & restore player userdata'
|
||||
inventory_command_description: 'View & edit a player''s inventory'
|
||||
enderchest_command_description: 'View & edit a player''s Ender Chest'
|
||||
locales:
|
||||
synchronization_complete: '[⏵ Data synchronized!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Failed to synchronize your data! Please contact an administrator.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1%''s Inventory'
|
||||
ender_chest_viewer_menu_title: '&0%1%''s Ender Chest'
|
||||
inventory_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold)[''s inventory as of ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold)[''s Ender Chest as of ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Your data has been updated!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Failed to update your data! Please contact an administrator.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
data_manager_title: '[Viewing user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\n&8When the data was saved)'
|
||||
data_manager_pinned: '[※ Snapshot pinned](#d8ff2b show_text=&7Pinned:\n&8This user data snapshot won''t be automatically rotated.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Health points) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hunger points) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)'
|
||||
data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements you have progress in:\n&8%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\n&8⚠ Based on in-game statistics)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventory…]](color=#a17b5f-#f5b98c show_text=&7Click to view run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Click to view run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ Delete…]](#ff3300 show_text=&7Click to delete this snapshot of user data.\n&8This will not affect the user''s current data.\n&#ff3300&⚠ This cannot be undone! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Restore…]](#00fb9a show_text=&7Click to restore this user data.\n&8This will set the user''s data to this snapshot.\n&#ff3300&⚠ %1%''s current data will be overwritten! suggest_command=/husksync:userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click to pin or unpin this user data snapshot\n&8Pinned snapshots won''t be automatically rotated run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'and %1% more…'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
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%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: 'disconnect'
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB migration'
|
||||
save_cause_legacy_migration: 'legacy migration'
|
||||
save_cause_converted_from_v2: 'converted from v2'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| You are running the latest version of HuskSync (v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Reloaded config and message files.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
error_invalid_syntax: '[Error:](#ff3300) [Incorrect syntax. Usage:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Error:](#ff3300) [Could not find a player by that name.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[Error:](#ff3300) [You do not have permission to execute this command](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [That command can only be run through console](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: That command can only be used in-game.'
|
||||
error_no_data_to_display: '[Error:](#ff3300) [Could not find any user data to display.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Error:](#ff3300) [Could not find any user data for that version UUID.](#ff7e5e)'
|
||||
husksync_command_description: 'Manage the HuskSync plugin'
|
||||
userdata_command_description: 'View, manage & restore player userdata'
|
||||
inventory_command_description: 'View & edit a player''s inventory'
|
||||
enderchest_command_description: 'View & edit a player''s Ender Chest'
|
||||
@@ -1,49 +1,65 @@
|
||||
synchronization_complete: '[⏵ ¡Datos sincronizados!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Fallo al sincronizar los datos, por favor, contacte con un administrador.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1% Inventario de:'
|
||||
ender_chest_viewer_menu_title: '&0%1% Enderchest de:'
|
||||
inventory_viewer_opened: '[Viendo una snapshot de](#00fb9a) [%1%](#00fb9a bold) [Inventario a partir de ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Viendo una snapshot de](#00fb9a) [%1%](#00fb9a bold) [Enderchest a partir de ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 ¡Tus datos han sido actualizados!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Error al actualizar tus datos, por favor, contacte con un administrador.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
data_manager_title: '[Viendo una snapshot sobre la informacion del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version del registro:\n&8Cuando los datos se han guardado)'
|
||||
data_manager_pinned: '[※ Snapshot anclada](#d8ff2b show_text=&Anclado:\n&8La informacion de este jugador no se rotará automaticamente.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Motivo del guardado:\n&8Lo que ha causado que se guarde)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Puntos de vida) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Puntos de hambre) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Nivel de exp) [🏹 %5%](dark_aqua show_text=&7Gamemode)'
|
||||
data_manager_advancements_statistics: '[⭐ Logros: %1%](color=#ffc43b-#f5c962 show_text=&7Logros que has conseguido:\n&8%2%) [⌛ Tiempo de juego: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\n&8⚠ Based on in-game statistics)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventario…]](color=#a17b5f-#f5b98c show_text=&7Click para ver run_command=/inventory %1% %2%) [[⌀ Enderchest…]](#b649c4-#d254ff show_text=&7Click para ver run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ Borrar…]](#ff3300 show_text=&7Click para borrar la snapshot del usuario.\n&8Esto no afectará a la informacion actual del jugador.\n&#ff3300&⚠ ¡Esto no se puede deshacer! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Restaurar…]](#00fb9a show_text=&7Click para restaurar la informacion de este usuario.\n&8Esto hará que la informacion actual cambie por esta snapshot.\n&#ff3300&⚠ %1% la informacion actual será sustituida! suggest_command=/husksync:userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click para anclar/desanclar esta snapshot\n&8Las snapshot ancladas no seran rotadas automaticamente run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'y %1% más…'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_deleted: '[❌ Se ha eliminado correctamente la snapshot del usuario](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Restaurado correctamente](#00fb9a) [%1%](#00fb9a show_text=&7UUID del jugador:\n&8%2%)[Informacion actual de la snapshot del jugador](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Se ha anclado perfectamente la snapshot del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7UUID del usuario:\n&8%4%)'
|
||||
data_unpinned: '[※ Se ha desanclado perfectamente la snapshot del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7UUID del usuario:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| You are running the latest version of HuskSync (v%1%).](#00fb9a)'
|
||||
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)'
|
||||
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_no_permission: '[Error:](#ff3300) [No tienes permisos para ejecutar este comando.](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [Este comando solo se puede ejecutar desde la consola.](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: Ese comando solo se puede utilizar desde el juego.'
|
||||
error_no_data_to_display: '[Error:](#ff3300) [No se ha podido encontrar informacion sobre el jugador.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Error:](#ff3300) [No se ha podido encontrar informacion sobre la UUID de ese jugador.](#ff7e5e)'
|
||||
husksync_command_description: 'Manage the HuskSync plugin'
|
||||
userdata_command_description: 'View, manage & restore player userdata'
|
||||
inventory_command_description: 'View & edit a player''s inventory'
|
||||
enderchest_command_description: 'View & edit a player''s Ender Chest'
|
||||
locales:
|
||||
synchronization_complete: '[⏵ ¡Datos sincronizados!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Fallo al sincronizar los datos, por favor, contacte con un administrador.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1% Inventario de:'
|
||||
ender_chest_viewer_menu_title: '&0%1% Enderchest de:'
|
||||
inventory_viewer_opened: '[Viendo una snapshot de](#00fb9a) [%1%](#00fb9a bold) [Inventario a partir de ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Viendo una snapshot de](#00fb9a) [%1%](#00fb9a bold) [Enderchest a partir de ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 ¡Tus datos han sido actualizados!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Error al actualizar tus datos, por favor, contacte con un administrador.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
data_manager_title: '[Viendo una snapshot sobre la informacion del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version del registro:\n&8Cuando los datos se han guardado)'
|
||||
data_manager_pinned: '[※ Snapshot anclada](#d8ff2b show_text=&Anclado:\n&8La informacion de este jugador no se rotará automaticamente.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Motivo del guardado:\n&8Lo que ha causado que se guarde)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Puntos de vida) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Puntos de hambre) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Nivel de exp) [🏹 %5%](dark_aqua show_text=&7Gamemode)'
|
||||
data_manager_advancements_statistics: '[⭐ Logros: %1%](color=#ffc43b-#f5c962 show_text=&7Logros que has conseguido:\n&8%2%) [⌛ Tiempo de juego: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\n&8⚠ Based on in-game statistics)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventario…]](color=#a17b5f-#f5b98c show_text=&7Click para ver run_command=/inventory %1% %2%) [[⌀ Enderchest…]](#b649c4-#d254ff show_text=&7Click para ver run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ Borrar…]](#ff3300 show_text=&7Click para borrar la snapshot del usuario.\n&8Esto no afectará a la informacion actual del jugador.\n&#ff3300&⚠ ¡Esto no se puede deshacer! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Restaurar…]](#00fb9a show_text=&7Click para restaurar la informacion de este usuario.\n&8Esto hará que la informacion actual cambie por esta snapshot.\n&#ff3300&⚠ %1% la informacion actual será sustituida! suggest_command=/husksync:userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click para anclar/desanclar esta snapshot\n&8Las snapshot ancladas no seran rotadas automaticamente run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'y %1% más…'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_deleted: '[❌ Se ha eliminado correctamente la snapshot del usuario](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Restaurado correctamente](#00fb9a) [%1%](#00fb9a show_text=&7UUID del jugador:\n&8%2%)[Informacion actual de la snapshot del jugador](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Se ha anclado perfectamente la snapshot del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7UUID del usuario:\n&8%4%)'
|
||||
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%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: 'disconnect'
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB migration'
|
||||
save_cause_legacy_migration: 'legacy migration'
|
||||
save_cause_converted_from_v2: 'converted from v2'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| You are running the latest version of HuskSync (v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Recargada la configuración y los archivos de lenguaje.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
error_invalid_syntax: '[Error:](#ff3300) [Sintanxis incorrecta. Usa:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Error:](#ff3300) [No se ha podido encontrar un jugador con ese nombre.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[Error:](#ff3300) [No tienes permisos para ejecutar este comando.](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [Este comando solo se puede ejecutar desde la consola.](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: Ese comando solo se puede utilizar desde el juego.'
|
||||
error_no_data_to_display: '[Error:](#ff3300) [No se ha podido encontrar informacion sobre el jugador.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Error:](#ff3300) [No se ha podido encontrar informacion sobre la UUID de ese jugador.](#ff7e5e)'
|
||||
husksync_command_description: 'Manage the HuskSync plugin'
|
||||
userdata_command_description: 'View, manage & restore player userdata'
|
||||
inventory_command_description: 'View & edit a player''s inventory'
|
||||
enderchest_command_description: 'View & edit a player''s Ender Chest'
|
||||
65
common/src/main/resources/locales/id-id.yml
Normal file
65
common/src/main/resources/locales/id-id.yml
Normal file
@@ -0,0 +1,65 @@
|
||||
locales:
|
||||
synchronization_complete: '[⏵ Data disinkronkan!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Gagal menyinkronkan datamu! Mohon hubungi administrator.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0Inventaris milik %1%'
|
||||
ender_chest_viewer_menu_title: '&0Peti Ender milik %1%'
|
||||
inventory_viewer_opened: '[Melihat cuplikan inventaris milik](#00fb9a) [%1%](#00fb9a bold)[pada ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Melihat cuplikan Peti Ender milik](#00fb9a) [%1%](#00fb9a bold)[pada ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Datamu telah diperbarui!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Gagal memperbarui datamu! Mohon hubungi administrator.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ Pendaftaran pengguna selesai!](#00fb9a)'
|
||||
data_manager_title: '[Melihat cuplikan data pengguna](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%](#00fb9a bold show_text=&7UUID pemain:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Versi stempel waktu:\n&8Ketika data disimpan)'
|
||||
data_manager_pinned: '[※ Cuplikan disematkan](#d8ff2b show_text=&7Disematkan:\n&8Cuplikan data pengguna ini tidak akan diputar secara otomatis.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Penyebab penyimpanan:\n&8Apa yang menyebabkan data disimpan)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Nama server tempat data disimpan)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Ukuran cuplikan:\n&8Perkiraan ukuran file cuplikan (dalam KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Poin kesehatan) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Poin kelaparan) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Level XP) [🏹 %5%](dark_aqua show_text=&7Mode game)'
|
||||
data_manager_advancements_statistics: '[⭐ Kemajuan: %1%](color=#ffc43b-#f5c962 show_text=&7Kemajuan yang telah kamu capai:\n&8%2%) [⌛ Waktu Bermain: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Waktu bermain dalam game\n&8⚠ Statistik berdasarkan dalam game)\n'
|
||||
data_manager_item_buttons: '[Lihat:](gray) [[🪣 Inventaris…]](color=#a17b5f-#f5b98c show_text=&7Klik untuk lihat run_command=/inventory %1% %2%) [[⌀ Peti Ender…]](#b649c4-#d254ff show_text=&7Klik untuk lihat run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Kelola:](gray) [[❌ Hapus…]](#ff3300 show_text=&7Klik untuk menghapus cuplikan data pengguna ini.\n&8Ini tidak akan berdampak pada data pengguna saat ini.\n&#ff3300&⚠ Hal ini tidak dapat dibatalkan! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Pulihkan…]](#00fb9a show_text=&7Klik untuk memulihkan data pengguna ini.\n&8Ini akan mengatur data pengguna ke cuplikan ini.\n&#ff3300&⚠ Data %1% saat ini akan ditimpa! suggest_command=/husksync:userdata restore %1% %2%) [[※ Sematkan/Tidak disematkan…]](#d8ff2b show_text=&7Klik untuk menyematkan atau tidak cuplikan data pengguna ini\n&8Cuplikan yang disematkan tidak akan diputar otomatis run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[Sistem:](gray) [[⏷ Pembuangan File…]](dark_gray show_text=&7Klik untuk membuang cuplikan data mentah pengguna ini ke sebuah file.\n&8Data yang dibuang dapat ditemukan di ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Pembuangan Web…]](dark_gray show_text=&7Klik untuk membuang cuplikan data mentah pengguna ini ke layanan mc-logs\n&8Kamu akan diberikan URL yang berisi data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'dan %1% lagi…'
|
||||
data_list_title: '[Cuplikan data %1%:](#00fb9a) [(%2%-%3% dari](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Cuplikan data pengguna untuk %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Disematkan:\n&8Cuplikan yang disematkan tidak akan dirotasi otomatis. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versi stampel waktu:&7\n&8Saat data disimpan\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Disimpan karena:\n&8Apa yang menyebabkan data disimpan run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Ukuran cuplikan:&7\n&8Perkiraan ukuran file cuplikan (dalam KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_deleted: '[❌ Berhasil menghapus cuplikan data pengguna](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
|
||||
data_restored: '[⏪ Berhasil dipulihkan](#00fb9a) [%1%](#00fb9a show_text=&7UUID Pemain:\n&8%2%)[data pengguna saat ini dari cuplikan](#00fb9a) [%3%.](#00fb9a show_text=&7Versi UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Berhasil menyematkan cuplikan data pengguna](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
|
||||
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%'
|
||||
list_footer: '\n%1%[Halaman](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7Lihat halaman sebelumnya run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7Lihat halaman selanjutnya run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Loncat ke halaman %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: 'memutuskan sambungan'
|
||||
save_cause_world_save: 'penyimpanan dunia'
|
||||
save_cause_death: 'kematian'
|
||||
save_cause_server_shutdown: 'pematian server'
|
||||
save_cause_inventory_command: 'perintah inventaris'
|
||||
save_cause_enderchest_command: 'perintah enderchest'
|
||||
save_cause_backup_restore: 'pemulihan cadangan'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'migrasi MPDB'
|
||||
save_cause_legacy_migration: 'migrasi peninggalan'
|
||||
save_cause_converted_from_v2: 'dikonversi dari v2'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| Kamu menjalankan versi terbaru dari HuskSync (v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| Versi baru HuskSync tersedia: v%1% (menjalankan: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Memuat ulang file konfigurasi dan pesan.](#00fb9a)\n[⚠ Pastikan file konfigurasi sudah diperbarui di semua server!](#00fb9a)\n[Diperlukan pengaktifan ulang agar perubahan konfigurasi dapat diterapkan.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| Laporan status sistem:](#00fb9a)'
|
||||
error_invalid_syntax: '[Kesalahan:](#ff3300) [Sintaks salah. Penggunaan:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Klik untuk menyarankan suggest_command=%1%)'
|
||||
error_invalid_player: '[Kesalahan:](#ff3300) [Tidak dapat menemukan pemain dengan nama tersebut.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[Kesalahan:](#ff3300) [Kamu tidak memiliki izin untuk menjalankan perintah ini](#ff7e5e)'
|
||||
error_console_command_only: '[Kesalahan:](#ff3300) [Perintah itu hanya dapat dijalankan melalui konsol](#ff7e5e)'
|
||||
error_in_game_command_only: 'Kesalahan: Perintah itu hanya dapat dijalankan dalam game.'
|
||||
error_no_data_to_display: '[Kesalahan:](#ff3300) [Tidak dapat menemukan data pengguna untuk ditampilkan.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Kesalahan:](#ff3300) [Tidak dapat menemukan data pengguna untuk versi UUID itu.](#ff7e5e)'
|
||||
husksync_command_description: 'Mengelola plugin HuskSync'
|
||||
userdata_command_description: 'Lihat, kelola & pulihkan data pengguna pemain'
|
||||
inventory_command_description: 'Lihat & edit inventaris pemain'
|
||||
enderchest_command_description: 'Lihat & edit Ender Chest pemain'
|
||||
@@ -1,49 +1,65 @@
|
||||
synchronization_complete: '[⏵ Dati sincronizzati!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Sincronizzazione fallita! Perfavore contatta un amministratore.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0Inventario di %1%'
|
||||
ender_chest_viewer_menu_title: '&0Enderchest di %1%'
|
||||
inventory_viewer_opened: '[Stai vedendo l''istantanea di](#00fb9a) [%1%](#00fb9a bold) [inventario del ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Stai vedendo l''istantanea di](#00fb9a) [%1%](#00fb9a bold) [Ender Chest del ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 I tuoi dati sono stati aggiornati!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Aggiornamento dei tuoi dati fallito! Perfavore contatta un amministratore.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
data_manager_title: '[Stai vedendo l''istantanea](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [di](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7:\n&8Quando i dati sono stati salvati)'
|
||||
data_manager_pinned: '[※ Istantanea fissata](#d8ff2b show_text=&7Pinned:\n&8Quest''istantanea non sarà cancellata automaticamente.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Causa di salvataggio:\n&8Cosa ha causato il salvataggio dei dati)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Peso dell''istantanea:\n&8Peso stimato del file (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Vita) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Fame) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Livello di XP) [🏹 %5%](dark_aqua show_text=&7Modalità di gioco)'
|
||||
data_manager_advancements_statistics: '[⭐ Progressi: %1%](color=#ffc43b-#f5c962 show_text=&7Progressi compiuti in:\n&8%2%) [⌛ Tempo di gioco: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Tempo di gioco\n&8⚠ Basato sulle statistiche di gioco)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventario…]](color=#a17b5f-#f5b98c show_text=&7Clicca per visualizzare run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Clicca per visualizzare run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Gestisci:](gray) [[❌ Cancella…]](#ff3300 show_text=&7Fare clic per eliminare questa istantanea.\n&8Questo non influisce sui dati attuali dell''utente.\n&#ff3300&⚠ Questo non può essere annullato! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Ripristina…]](#00fb9a show_text=&7Clicca per ripristinare i dati dell''utente.\n&8I dati dell''utente saranno ripristinati a quest''istantanea.\n&#ff3300&⚠ I dati di %1% saranno sovrascritti! suggest_command=/husksync:userdata restore %1% %2%) [[※ fissa/sblocca...]](#d8ff2b show_text=&7Clicca per fissare o sbloccare quest''istantanea\n&8Le istantanee fissate non saranno cancellate automaticamente run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[Sistema:](gray) [[⏷ Dump del File…]](dark_gray show_text=&7Clicca per ottenere il dump dei dati del giocatore.\n&8I dati salvati sono posizioanti nella cartella ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Dump su Web…]](dark_gray show_text=&7Clicca per ottenere il dump del file su mc-logs\n&8 Ti verrà consegnato l''url per visionare il dump. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'e %1% altro…'
|
||||
data_list_title: '[Lista delle istantanee di %1%:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Instantanea di %2%&8⚡ id: %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Fissata:\n&8Se fissata, l''istantanea non viene mai modificata. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Data di salvataggio:&7\n&8Momento preciso in cui è stato salvato il dato\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Causa di salvataggio:\n&8Che cosa ha causato il salvataggio run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Peso dell''istantanea:&7\n&8Peso stimato del file (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_deleted: '[❌ Istantanea eliminata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Ripristato con successo](#00fb9a) [Dati dall''istantanea di](#00fb9a)[%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versione di UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Instantanea fissata](#00fb9a) [%1%](#00fb9a show_text=&7UUID della versione:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ L''istantanea dei dati utente è stata sbloccata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Hai ottenuto il dump dell''istantanea %1% di %2% nel formato:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Pagina](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7Visualizza pagina precedente run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7Visualizza pagina successiva run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Vai alla pagina %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| Il plugin è all''ultima versione disponibile (v%1%).](#00fb9a)'
|
||||
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)'
|
||||
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_no_permission: '[Errore:](#ff3300) [Non hai il permesso di usare questo comando](#ff7e5e)'
|
||||
error_console_command_only: '[Errore:](#ff3300) [Questo comando può essere eseguito solo dalla](#ff7e5e)'
|
||||
error_in_game_command_only: 'Errore: Questo comando può essere utilizzato solo in gioco.'
|
||||
error_no_data_to_display: '[Errore:](#ff3300) [Impossibile trovare dati da visualizzare.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Errore:](#ff3300) [Impossibile trovare dati utente per questa versione di UUID.](#ff7e5e)'
|
||||
husksync_command_description: 'Gestisci il plugin HuskSync'
|
||||
userdata_command_description: 'Vedi, gestisci e recupera i dati del giocatore'
|
||||
inventory_command_description: 'Vedi e modifica l''Inventario di un giocatore'
|
||||
enderchest_command_description: 'Vedi e modifica l''Ender Chest di un giocatore'
|
||||
locales:
|
||||
synchronization_complete: '[⏵ Dati sincronizzati!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Sincronizzazione fallita! Perfavore contatta un amministratore.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0Inventario di %1%'
|
||||
ender_chest_viewer_menu_title: '&0Enderchest di %1%'
|
||||
inventory_viewer_opened: '[Stai vedendo l''istantanea di](#00fb9a) [%1%](#00fb9a bold) [inventario del ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Stai vedendo l''istantanea di](#00fb9a) [%1%](#00fb9a bold) [Ender Chest del ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 I tuoi dati sono stati aggiornati!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Aggiornamento dei tuoi dati fallito! Perfavore contatta un amministratore.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
data_manager_title: '[Stai vedendo l''istantanea](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [di](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7:\n&8Quando i dati sono stati salvati)'
|
||||
data_manager_pinned: '[※ Istantanea fissata](#d8ff2b show_text=&7Pinned:\n&8Quest''istantanea non sarà cancellata automaticamente.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Causa di salvataggio:\n&8Cosa ha causato il salvataggio dei dati)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Peso dell''istantanea:\n&8Peso stimato del file (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Vita) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Fame) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Livello di XP) [🏹 %5%](dark_aqua show_text=&7Modalità di gioco)'
|
||||
data_manager_advancements_statistics: '[⭐ Progressi: %1%](color=#ffc43b-#f5c962 show_text=&7Progressi compiuti in:\n&8%2%) [⌛ Tempo di gioco: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Tempo di gioco\n&8⚠ Basato sulle statistiche di gioco)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventario…]](color=#a17b5f-#f5b98c show_text=&7Clicca per visualizzare run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Clicca per visualizzare run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Gestisci:](gray) [[❌ Cancella…]](#ff3300 show_text=&7Fare clic per eliminare questa istantanea.\n&8Questo non influisce sui dati attuali dell''utente.\n&#ff3300&⚠ Questo non può essere annullato! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Ripristina…]](#00fb9a show_text=&7Clicca per ripristinare i dati dell''utente.\n&8I dati dell''utente saranno ripristinati a quest''istantanea.\n&#ff3300&⚠ I dati di %1% saranno sovrascritti! suggest_command=/husksync:userdata restore %1% %2%) [[※ fissa/sblocca...]](#d8ff2b show_text=&7Clicca per fissare o sbloccare quest''istantanea\n&8Le istantanee fissate non saranno cancellate automaticamente run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[Sistema:](gray) [[⏷ Dump del File…]](dark_gray show_text=&7Clicca per ottenere il dump dei dati del giocatore.\n&8I dati salvati sono posizioanti nella cartella ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Dump su Web…]](dark_gray show_text=&7Clicca per ottenere il dump del file su mc-logs\n&8 Ti verrà consegnato l''url per visionare il dump. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'e %1% altro…'
|
||||
data_list_title: '[Lista delle istantanee di %1%:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Instantanea di %2%&8⚡ id: %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Fissata:\n&8Se fissata, l''istantanea non viene mai modificata. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Data di salvataggio:&7\n&8Momento preciso in cui è stato salvato il dato\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Causa di salvataggio:\n&8Che cosa ha causato il salvataggio run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Peso dell''istantanea:&7\n&8Peso stimato del file (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_deleted: '[❌ Istantanea eliminata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Ripristato con successo](#00fb9a) [Dati dall''istantanea di](#00fb9a)[%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versione di UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Instantanea fissata](#00fb9a) [%1%](#00fb9a show_text=&7UUID della versione:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
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%'
|
||||
list_footer: '\n%1%[Pagina](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7Visualizza pagina precedente run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7Visualizza pagina successiva run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Vai alla pagina %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: 'disconnect'
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB migration'
|
||||
save_cause_legacy_migration: 'legacy migration'
|
||||
save_cause_converted_from_v2: 'converted from v2'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| Il plugin è all''ultima versione disponibile (v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| Disponibile una nuova versione: v%1% (running: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Configurazione e messaggi ricaricati.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
error_invalid_syntax: '[Errore:](#ff3300) [Sintassi errata. Usa:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Errore:](#ff3300) [Impossibile trovare un giocatore con questo nome.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[Errore:](#ff3300) [Non hai il permesso di usare questo comando](#ff7e5e)'
|
||||
error_console_command_only: '[Errore:](#ff3300) [Questo comando può essere eseguito solo dalla](#ff7e5e)'
|
||||
error_in_game_command_only: 'Errore: Questo comando può essere utilizzato solo in gioco.'
|
||||
error_no_data_to_display: '[Errore:](#ff3300) [Impossibile trovare dati da visualizzare.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Errore:](#ff3300) [Impossibile trovare dati utente per questa versione di UUID.](#ff7e5e)'
|
||||
husksync_command_description: 'Gestisci il plugin HuskSync'
|
||||
userdata_command_description: 'Vedi, gestisci e recupera i dati del giocatore'
|
||||
inventory_command_description: 'Vedi e modifica l''Inventario di un giocatore'
|
||||
enderchest_command_description: 'Vedi e modifica l''Ender Chest di un giocatore'
|
||||
|
||||
@@ -1,49 +1,65 @@
|
||||
synchronization_complete: '[⏵データが同期されました!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Failed to synchronize your data! Please contact an administrator.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1%''s Inventory'
|
||||
ender_chest_viewer_menu_title: '&0%1%''s Ender Chest'
|
||||
inventory_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold)[''s inventory as of ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold)[''s Ender Chest as of ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Your data has been updated!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Failed to update your data! Please contact an administrator.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
data_manager_title: '[Viewing user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\n&8When the data was saved)'
|
||||
data_manager_pinned: '[※ Snapshot pinned](#d8ff2b show_text=&7Pinned:\n&8This user data snapshot won''t be automatically rotated.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Health points) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hunger points) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)'
|
||||
data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements you have progress in:\n&8%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\n&8⚠ Based on in-game statistics)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventory…]](color=#a17b5f-#f5b98c show_text=&7Click to view run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Click to view run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ Delete…]](#ff3300 show_text=&7Click to delete this snapshot of user data.\n&8This will not affect the user''s current data.\n&#ff3300&⚠ This cannot be undone! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Restore…]](#00fb9a show_text=&7Click to restore this user data.\n&8This will set the user''s data to this snapshot.\n&#ff3300&⚠ %1%''s current data will be overwritten! suggest_command=/husksync:userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click to pin or unpin this user data snapshot\n&8Pinned snapshots won''t be automatically rotated run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'and %1% more…'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ Successfully unpinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| HuskSyncの最新バージョンを実行しています(v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| HuskSyncの最新バージョンが更新されています: 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)'
|
||||
error_invalid_syntax: '[Error:](#ff3300) [構文が正しくありません。使用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Error:](#ff3300) [そのプレイヤーは見つかりませんでした](#ff7e5e)'
|
||||
error_no_permission: '[Error:](#ff3300) [このコマンドを実行する権限がありません](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [そのコマンドは%1%コンソールからのみ実行できます](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: That command can only be used in-game.'
|
||||
error_no_data_to_display: '[Error:](#ff3300) [Could not find any user data to display.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Error:](#ff3300) [Could not find any user data for that version UUID.](#ff7e5e)'
|
||||
husksync_command_description: 'Manage the HuskSync plugin'
|
||||
userdata_command_description: 'View, manage & restore player userdata'
|
||||
inventory_command_description: 'View & edit a player''s inventory'
|
||||
enderchest_command_description: 'View & edit a player''s Ender Chest'
|
||||
locales:
|
||||
synchronization_complete: '[⏵データが同期されました!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ データの同期に失敗しました!管理者に連絡してください。](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1%のインベントリ'
|
||||
ender_chest_viewer_menu_title: '&0%1%のエンダーチェスト'
|
||||
inventory_viewer_opened: '[⌚ %2%](#00fb9a) [%1%](#00fb9a bold) [のインベントリのスナップショットを閲覧する](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[⌚ %2%](#00fb9a) [%1%](#00fb9a bold) [のエンダーチェストのスナップショットを閲覧する](#00fb9a)'
|
||||
data_update_complete: '[🔔 データが更新されました!](#00fb9a)'
|
||||
data_update_failed: '[🔔 データの更新に失敗しました!管理者に連絡してください。](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ ユーザー登録が完了しました!](#00fb9a)'
|
||||
data_manager_title: '[%3%](#00fb9a bold show_text=&7プレイヤーUUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a)[%1%](#00fb9a show_text=&7バージョンUUID:\n&8%2%)[を表示:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7バージョンタイムスタンプ:\n&8データの保存時期)'
|
||||
data_manager_pinned: '[※ ピン留めされたスナップショット](#d8ff2b show_text=&7ピン留め:\n&8このユーザーデータのスナップショットは自動的にローテーションされません。)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存理由:\n&8データが保存された理由)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7スナップショットサイズ:\n&8スナップショットの推定ファイルサイズ(単位:KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7体力) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7空腹度) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7経験値レベル) [🏹 %5%](dark_aqua show_text=&7ゲームモード)'
|
||||
data_manager_advancements_statistics: '[⭐ 進捗: %1%](color=#ffc43b-#f5c962 show_text=&7達成した進捗:\n&8%2%) [⌛ プレイ時間: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7ゲーム内のプレイ時間\n&8⚠ ゲーム内の統計に基づく)\n'
|
||||
data_manager_item_buttons: '[表示:](gray) [[🪣 インベントリ…]](color=#a17b5f-#f5b98c show_text=&7クリックで表示 run_command=/husksync:inventory %1% %2%) [[⌀ エンダーチェスト…]](#b649c4-#d254ff show_text=&7クリックで表示 run_command=/husksync:enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[管理:](gray) [[❌ 消去…]](#ff3300 show_text=&7クリックでこのユーザーデータのスナップショットを消去します。\n&8これはユーザーの現在のデータには影響しません。\n&#ff3300&⚠ この操作は元に戻せません! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ 復元…]](#00fb9a show_text=&7クリックでこのユーザーデータを復元します。\n&8これにより、ユーザーデータはこのスナップショットに設定されます。\n&#ff3300&⚠ %1% の現在のデータは上書きされます! suggest_command=/husksync:userdata restore %1% %2%) [[※ ピン留め/ピン外し…]](#d8ff2b show_text=&7クリックでこのユーザーデータのスナップショットをピン留め、若しくはピンを外します。\n&8ピン留めされたスナップショットは自動的にローテーションしません。 run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[システム:](gray) [[⏷ ファイルダンプ…]](dark_gray show_text=&7クリックで未加工のユーザーデータスナップショットをダンプファイルにします。\n&8データダンプの場所は ~/plugins/HuskSync/dumps/ です run_command=/husksync:userdata dump %1% %2% file) [[☂ Webダンプ…]](dark_gray show_text=&7クリックでユーザーデータスナップショットをmc-logsサービスにダンプします。\n&8データを含むURLが提供されます。 run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'さらに %1% 件…'
|
||||
data_list_title: '[%1% のユーザーデータスナップショット:](#00fb9a) [(%4%件中](#00fb9a bold) [%2%-%3%件](#00fb9a)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7%2% のユーザーデータスナップショット&8⚡ %4% run_command=/husksync:userdata view %2% %3%) [%5%](#d8ff2b show_text=&7ピン留め:\n&8ピン留めされたスナップショットは自動的にローテーションしません。 run_command=/husksync:userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7バージョンタイムスタンプ:&7\n&8データの保存時期\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7保存理由:\n&8データが保存された理由 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7スナップショットサイズ:&7\n&8スナップショットの推定ファイルサイズ (単位:KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_deleted: '[❌](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [の消去に成功しました。](#00fb9a)'
|
||||
data_restored: '[⏪](#00fb9a) [スナップショット](#00fb9a) [%3%](#00fb9a show_text=&7Version UUID:\n&8%4%) [から](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [の現在のユーザーデータの復元に成功しました。](#00fb9a)'
|
||||
data_pinned: '[※](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [のピン留めに成功しました。](#00fb9a)'
|
||||
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%'
|
||||
list_footer: '\n%1%[ページ](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7前のページへ run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7次のページへ run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7%1% ページ目へ run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: 'disconnect'
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB migration'
|
||||
save_cause_legacy_migration: 'legacy migration'
|
||||
save_cause_converted_from_v2: 'converted from v2'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| HuskSyncの最新バージョンを実行しています(v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| HuskSyncの最新バージョンが更新されています: v%1% (実行中: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 設定ファイルとメッセージファイルを再読み込みしました。](#00fb9a)\n[⚠ すべてのサーバーで設定ファイルが最新であることを確認してください!](#00fb9a)\n[設定の変更を有効にするには再起動が必要です。](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
error_invalid_syntax: '[Error:](#ff3300) [構文が正しくありません。使用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&クリックでサジェスト suggest_command=%1%)'
|
||||
error_invalid_player: '[Error:](#ff3300) [そのプレイヤーは見つかりませんでした](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[Error:](#ff3300) [このコマンドを実行する権限がありません](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [そのコマンドは%1%コンソールからのみ実行できます](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: そのコマンドはゲーム内でしか使えません。'
|
||||
error_no_data_to_display: '[Error:](#ff3300) [表示するユーザーデータが見つかりませんでした。](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Error:](#ff3300) [そのバージョンUUIDのユーザーデータが見つかりませんでした。](#ff7e5e)'
|
||||
husksync_command_description: 'HuskSyncプラグインを管理する'
|
||||
userdata_command_description: 'プレーヤーのユーザーデータを表示・管理・復元する'
|
||||
inventory_command_description: 'プレイヤーのインベントリを閲覧・編集する'
|
||||
enderchest_command_description: 'プレイヤーのエンダーチェストを閲覧・編集する'
|
||||
|
||||
65
common/src/main/resources/locales/ko-kr.yml
Normal file
65
common/src/main/resources/locales/ko-kr.yml
Normal file
@@ -0,0 +1,65 @@
|
||||
locales:
|
||||
synchronization_complete: '[⏵ 데이터 연동됨!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ 데이터 연동에 실패하였습니다! 관리자에게 문의해 주세요.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1%님의 인벤토리'
|
||||
ender_chest_viewer_menu_title: '&0%1%님의 엔더상자'
|
||||
inventory_viewer_opened: '[%1%](#00fb9a bold)[님의 ⌚ %2%의 인벤토리를 엽니다](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[%1%](#00fb9a bold)[님의 ⌚ %2%의 엔더상자를 엽니다](#00fb9a)'
|
||||
data_update_complete: '[🔔 당신의 데이터가 업데이트 되었습니다!](#00fb9a)'
|
||||
data_update_failed: '[🔔 데이터 업데이트에 실패하였습니다! 관리자에게 문의해 주세요.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ 유저 등록이 완료되었습니다!](#00fb9a)'
|
||||
data_manager_title: '[%3%](#00fb9a bold show_text=&7플레이어 UUID:\n&8%4%)[님의 ](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%) [데이터 스냅샷을 표시합니다](#00fb9a)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7저장 시각:\n&8데이터가 저장된 시각)'
|
||||
data_manager_pinned: '[※ 스냅샷 고정됨](#d8ff2b show_text=&7고정됨:\n&8이 유저의 데이터 스냅샷은 자동으로 갱신되지 않습니다.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7저장 사유:\n&8데이터가 저장된 사유입니다.)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7서버:\n&8데이터 저장이 이루어진 서버입니다.)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7스냅샷 크기:\n&8스냅샷 파일의 대략적인 크기입니다. (단위 KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7체력) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7허기) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7경험치 레벨) [🏹 %5%](dark_aqua show_text=&7게임 모드)'
|
||||
data_manager_advancements_statistics: '[⭐ 도전 과제: %1%](color=#ffc43b-#f5c962 show_text=&7진행한 도전 과제:\n&8%2%) [⌛ 플레이 타임: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7인게임 플레이 시간\n&8⚠ 인게임 통계에 기반합니다.)\n'
|
||||
data_manager_item_buttons: '[보기:](gray) [[🪣 인벤토리…]](color=#a17b5f-#f5b98c show_text=&7클릭하여 확인 run_command=/inventory %1% %2%) [[⌀ 엔더상자…]](#b649c4-#d254ff show_text=&7클릭하여 확인 run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[관리:](gray) [[❌ 삭제…]](#ff3300 show_text=&7클릭하여 이 유저 스냅샷 데이터를 삭제\n&8이 기능은 현재 유저의 인벤토리 데이터에는 영향을 미치지 않습니다.\n&#ff3300&⚠ 이 작업은 되돌릴 수 없습니다! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ 복구…]](#00fb9a show_text=&7클릭하여 유저 데이터 복구\n&8유저의 데이터가 이 스냅샷의 데이터로 변경됩니다.\n&#ff3300&⚠ %1%님의 현재 데이터에 덧씌워 집니다! suggest_command=/husksync:userdata restore %1% %2%) [[※ 고정/고정 해제…]](#d8ff2b show_text=&7클릭하여 유저 데이터를 고정 또는 고정 해제\n&8고정된 스냅샷은 자동적으로 갱신되지 않습니다. run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[시스템:](gray) [[⏷ 파일 덤프…]](dark_gray show_text=&7클릭하여 이 유저 데이터 스냅샷을 덤프하기\n&8데이터 덤프 파일은 ~/plugins/HuskSync/dumps/ 에서 찾을 수 있습니다. run_command=/husksync:userdata dump %1% %2% file) [[☂ 웹 덤프…]](dark_gray show_text=&7클릭하여 유저 데이터 스냅샷을 mc-log 서비스에 덤프하기\n&8데이터를 포함한 URL이 제공됩니다. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '외 %1%개...'
|
||||
data_list_title: '[%1%님의 유저 데이터 스냅샷 목록:](#00fb9a) [(%2%-%3% 중](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7%2%&7님의 유저 데이터 스냅샷&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7고정됨:\n&8고정된 스냅샷은 자동적으로 갱신되지 않습니다. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7저장 시각:&7\n&8데이터가 저장된 시각입니다.\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7저장 사유:\n&8데이터가 저장된 사유입니다. run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7스냅샷 크기:&7\n&8스냅샷 파일의 대략적인 크기입니다. (단위 KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_deleted: '[❌ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%) [님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 삭제하였습니다.](#00fb9a)'
|
||||
data_restored: '[⏪ 성공적으로 복구되었습니다.](#00fb9a) [%1%](#00fb9a show_text=&7플레이어 UUID:\n&8%2%)[님의 현재 유저 데이터 스냅샷이](#00fb9a) [%3%](#00fb9a show_text=&7버전 UUID:\n&8%4%)[으로 변경되었습니다.](#00fb9a)'
|
||||
data_pinned: '[※ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%)[님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 고정하였습니다.](#00fb9a)'
|
||||
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%'
|
||||
list_footer: '\n%1%[페이지](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7이전 페이지 보기 run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7다음 페이지 보기 run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7%1% 페이지 보기 run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: 'disconnect'
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB migration'
|
||||
save_cause_legacy_migration: 'legacy migration'
|
||||
save_cause_converted_from_v2: 'converted from v2'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| 가장 최신 버전의 HuskSync를 실행 중입니다 (v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| 새로운 버전의 HuskSync가 존재합니다: v%1% (현재 버전: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 콘피그와 메시지 파일을 다시 불러왔습니다.](#00fb9a)\n[⚠ 모든 서버의 컨피그 파일을 변경하였는지 확인하세요!](#00fb9a)\n[몇몇 설정은 재시작 후에 적용됩니다.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
error_invalid_syntax: '[오류:](#ff3300) [잘못된 사용법. 사용법:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&클릭하여 입력할 수 있습니다. suggest_command=%1%)'
|
||||
error_invalid_player: '[오류:](#ff3300) [해당 이름의 사용자를 찾을 수 없습니다.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[오류:](#ff3300) [해당 명령어를 사용할 권한이 없습니다.](#ff7e5e)'
|
||||
error_console_command_only: '[오류:](#ff3300) [해당 명령어는 콘솔을 통해서만 사용할 수 있습니다.](#ff7e5e)'
|
||||
error_in_game_command_only: '오류: 해당 명령어는 게임 내부에서만 사용할 수 있습니다.'
|
||||
error_no_data_to_display: '[오류:](#ff3300) [표시할 유저 데이터를 찾을 수 없습니다.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[오류:](#ff3300) [해당 버전 UUID의 유저 데이터 스냅샷을 찾을 수 없습니다.](#ff7e5e)'
|
||||
husksync_command_description: 'HuskSync 플러그인을 관리합니다.'
|
||||
userdata_command_description: '확인, 관리 또는 복구합니다.'
|
||||
inventory_command_description: '플레이어의 인벤토리를 열람 또는 편집합니다.'
|
||||
enderchest_command_description: '플레이어의 엔더상자를 열람 또는 편집합니다.'
|
||||
65
common/src/main/resources/locales/nl-nl.yml
Normal file
65
common/src/main/resources/locales/nl-nl.yml
Normal file
@@ -0,0 +1,65 @@
|
||||
locales:
|
||||
synchronization_complete: '[⏵ Data gesynchroniseerd!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Synchroniseren van jouw gegevens is niet gelukt! Neem contact op met een beheerder.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1%''s Inventaris'
|
||||
ender_chest_viewer_menu_title: '&0%1%''s Enderkist'
|
||||
inventory_viewer_opened: '[Momentopname bekijken van](#00fb9a) [%1%](#00fb9a bold)[''s inventaris per ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Momentopname bekijken van](#00fb9a) [%1%](#00fb9a bold)[''s Enderkist per ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Jouw gegevens zijn bijgewerkt!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Het is niet gelukt om jouw gegevens bij te werken! Neem contact op met een beheerder.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ Gebruikersregistratie voltooid!](#00fb9a)'
|
||||
data_manager_title: '[Momentopname van gebruikersgegevens bekijken](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%](#00fb9a bold show_text=&7Speler UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Versie tijdmarkering:\n&8Toen de gegevens werden opgeslagen)'
|
||||
data_manager_pinned: '[※ Momentopname vastgezet](#d8ff2b show_text=&7Vastgezet:\n&8Deze momentopname van gebruikersgegevens wordt niet automatisch gerouleerd.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Reden opslaan:\n&8Waarom de data is opgeslagen)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Grootte van momentopname:\n&8Geschatte bestandsgrootte van de momentopname (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Gezondheids punten) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Honger punten) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Speltype)'
|
||||
data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements waarin je voortgang hebt:\n&8%2%) [⌛ Speeltijd: %3%uren](color=#62a9f5-#7ab8fa show_text=&7In-game speeltijd\n&8⚠ Gebaseerd op in-game statistieken)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventaris…]](color=#a17b5f-#f5b98c show_text=&7Klikken om te bekijken run_command=/inventory %1% %2%) [[⌀ Enderkist…]](#b649c4-#d254ff show_text=&7Klikken om te bekijken run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Beheren:](gray) [[❌ Verwijderen…]](#ff3300 show_text=&7Klik om deze momentopname van gebruikersgegevens te verwijderen.\n&8Dit heeft geen invloed op de huidige gegevens van de gebruiker.\n&#ff3300&⚠ Dit kan niet ongedaan gemaakt worden! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Herstellen…]](#00fb9a show_text=&7Klik om deze gebruikersgegevens te herstellen.\n&8Hierdoor worden de gegevens van de gebruiker ingesteld op deze momentopname.\n&#ff3300&⚠ %1%''s huidige gegevens worden overschreven! suggest_command=/husksync:userdata restore %1% %2%) [[※ Vastzetten/losmaken…]](#d8ff2b show_text=&7Klik om deze momentopname van gebruikersgegevens vast te zetten of los te maken\n&8Vastgezette momentopnamen worden niet automatisch gerouleerd run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[Systeem:](gray) [[⏷ Bestandsdump…]](dark_gray show_text=&7Klik om deze ruwe gebruikersgegevenssnapshot naar een bestand te dumperen.\n&8Gegevensdumps zijn te vinden in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Webdump…]](dark_gray show_text=&7Klik om deze ruwe gebruikersgegevenssnapshot naar de mc-logs-service te dumpen\n&8Je ontvangt een URL met de gegevens. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'en %1% meer…'
|
||||
data_list_title: '[%1%''s momentopnamen van gebruikersgegevens:](#00fb9a) [(%2%-%3% van](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Gebruikersgegevens momentopname voor %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Vastgezet:\n&8Vastgezette momentopnamen worden niet automatisch gerouleerd. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versie tijdmarkering:&7\n&8Wanneer de data was opgeslagen\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Reden opslaan:\n&8Waarom de data is opgeslagen run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Grootte van momentopname:&7\n&8Geschatte bestandsgrootte van de momentopname (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_deleted: '[❌ Momentopname van gebruikersgegevens is verwijderd](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Succesvol hersteld](#00fb9a) [%1%](#00fb9a show_text=&7Speler UUID:\n&8%2%)[''s huidige gebruikersgegevens uit momentopname](#00fb9a) [%3%.](#00fb9a show_text=&7Versie UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Momentopname van gebruikersgegevens is vastgezet](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
|
||||
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%'
|
||||
list_footer: '\n%1%[Pagina](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7Bekijk vorige pagina run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7Bekijk volgende pagina run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Ga naar pagina %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: 'disconnect'
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB migration'
|
||||
save_cause_legacy_migration: 'legacy migration'
|
||||
save_cause_converted_from_v2: 'converted from v2'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| Je gebruikt de nieuwste versie van HuskSync (v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| Er is een nieuwe versie van HuskSync beschikbaar: v%1% (huidige versie: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Configuratie- en berichtbestanden opnieuw geladen.](#00fb9a)\n[⚠ Controleer of de configuratiebestanden up-to-date zijn op alle servers!](#00fb9a)\n[Een herstart is nodig voor de configuratiewijzigingen van kracht te laten worden.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
error_invalid_syntax: '[Error:](#ff3300) [Onjuiste syntaxis. Gebruik:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Error:](#ff3300) [Kan geen speler met die naam vinden.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[Error:](#ff3300) [Je hebt geen toestemming om deze opdracht uit te voeren](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [Dat command kan alleen via de console worden uitgevoerd](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: Dat command kan alleen in-game worden gebruikt.'
|
||||
error_no_data_to_display: '[Error:](#ff3300) [Kon geen gebruikersgegevens vinden om weer te geven.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Error:](#ff3300) [Kon geen gebruikersgegevens vinden voor dat versie-UUID.](#ff7e5e)'
|
||||
husksync_command_description: 'Beheer de HuskSync plugin'
|
||||
userdata_command_description: 'Bekijk, beheer en herstel de gebruikersgegevens van spelers'
|
||||
inventory_command_description: 'Bekijk en bewerk de inventaris van een speler'
|
||||
enderchest_command_description: 'Bekijk en bewerk de Enderkist van een speler'
|
||||
@@ -1,49 +1,65 @@
|
||||
synchronization_complete: '[⏵ Dados sincronizados!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Falha na sincronização de seus dados! Por favor entre em contato com um administrador.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1%''s Inventory'
|
||||
ender_chest_viewer_menu_title: '&0%1%''s Ender Chest'
|
||||
inventory_viewer_opened: '[Visualizando snapshot de](#00fb9a) [%1%](#00fb9a bold) [''s inventory a partir de ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Visualizando snapshot de](#00fb9a) [%1%](#00fb9a bold) [''s Ender Chest a partir de ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Seus dados foram atualizados!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Falha na atualização de seus dados! Por favor entre em contato com um administrador.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
data_manager_title: '[Visualizando snapshot dos dados do usuário](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\n&8Quando os dados foram salvos)'
|
||||
data_manager_pinned: '[※ Snapshot marcada](#d8ff2b show_text=&7Marcada:\n&8Essa snapshot de dados do usuário não será girada automaticamente.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Causa do salvamento:\n&8O motivo para que os dados fossem salvos)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Pontos de Vida) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Pontos de vida) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)'
|
||||
data_manager_advancements_statistics: '[⭐ Progressos: %1%](color=#ffc43b-#f5c962 show_text=&7Progressos que você tem realizado em:\n&8%2%) [⌛ Tempo de jogo: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Tempo de jogo dentro do jogo\n&8⚠ Com base em estatísticas dentro do jogo)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventory…]](color=#a17b5f-#f5b98c show_text=&7Clique para ver run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Clique para ver run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Gerenciar:](gray) [[❌ Deletar…]](#ff3300 show_text=&7Clique para deletar esta snapshot de dados do usuário\n&8Isto não afetará os dados atuais do usuário.\n&#ff3300&⚠ Isto não pode ser desfeito! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Restaurar…]](#00fb9a show_text=&7Clique para restaurar estes dados do usuário.\n&8Isto substituirá os dados atuais do usuário para os da snapshot.\n&#ff3300&⚠ %1%''s os dados atuais serão substituídos! suggest_command=/husksync:userdata restore %1% %2%) [[※ Marcar/Desmarcar…]](#d8ff2b show_text=&7Clique para marcar ou desmarcar este snapshot de dados do usuário\n&8Snapshots marcadas não serão giradas automaticamente run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'e %1% mais…'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_deleted: '[❌ Snapshot de dados do usuário deletada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Restaurada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Snapshot de dados do usuário marcada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ Snapshot de dados do usuário desmarcada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| You are running the latest version of HuskSync (v%1%).](#00fb9a)'
|
||||
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)'
|
||||
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_no_permission: '[Error:](#ff3300) [Você não tem permissão para executar este comando](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [Esse comando só pode ser executado através do console](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: Esse comando só pode ser usado dentro do jogo.'
|
||||
error_no_data_to_display: '[Error:](#ff3300) [Não encontramos nenhuma informação deste jogador para exibir.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Error:](#ff3300) [Não foi possível encontrar nenhuma informação deste jogador para essa versão UUID.](#ff7e5e)'
|
||||
husksync_command_description: 'Manage the HuskSync plugin'
|
||||
userdata_command_description: 'View, manage & restore player userdata'
|
||||
inventory_command_description: 'View & edit a player''s inventory'
|
||||
enderchest_command_description: 'View & edit a player''s Ender Chest'
|
||||
locales:
|
||||
synchronization_complete: '[⏵ Dados sincronizados!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Falha na sincronização de seus dados! Por favor entre em contato com um administrador.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1%''s Inventory'
|
||||
ender_chest_viewer_menu_title: '&0%1%''s Ender Chest'
|
||||
inventory_viewer_opened: '[Visualizando snapshot de](#00fb9a) [%1%](#00fb9a bold) [''s inventory a partir de ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Visualizando snapshot de](#00fb9a) [%1%](#00fb9a bold) [''s Ender Chest a partir de ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Seus dados foram atualizados!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Falha na atualização de seus dados! Por favor entre em contato com um administrador.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
data_manager_title: '[Visualizando snapshot dos dados do usuário](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\n&8Quando os dados foram salvos)'
|
||||
data_manager_pinned: '[※ Snapshot marcada](#d8ff2b show_text=&7Marcada:\n&8Essa snapshot de dados do usuário não será girada automaticamente.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Causa do salvamento:\n&8O motivo para que os dados fossem salvos)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Pontos de Vida) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Pontos de vida) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)'
|
||||
data_manager_advancements_statistics: '[⭐ Progressos: %1%](color=#ffc43b-#f5c962 show_text=&7Progressos que você tem realizado em:\n&8%2%) [⌛ Tempo de jogo: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Tempo de jogo dentro do jogo\n&8⚠ Com base em estatísticas dentro do jogo)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventory…]](color=#a17b5f-#f5b98c show_text=&7Clique para ver run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Clique para ver run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Gerenciar:](gray) [[❌ Deletar…]](#ff3300 show_text=&7Clique para deletar esta snapshot de dados do usuário\n&8Isto não afetará os dados atuais do usuário.\n&#ff3300&⚠ Isto não pode ser desfeito! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Restaurar…]](#00fb9a show_text=&7Clique para restaurar estes dados do usuário.\n&8Isto substituirá os dados atuais do usuário para os da snapshot.\n&#ff3300&⚠ %1%''s os dados atuais serão substituídos! suggest_command=/husksync:userdata restore %1% %2%) [[※ Marcar/Desmarcar…]](#d8ff2b show_text=&7Clique para marcar ou desmarcar este snapshot de dados do usuário\n&8Snapshots marcadas não serão giradas automaticamente run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'e %1% mais…'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_deleted: '[❌ Snapshot de dados do usuário deletada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Restaurada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Snapshot de dados do usuário marcada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
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%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: 'disconnect'
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB migration'
|
||||
save_cause_legacy_migration: 'legacy migration'
|
||||
save_cause_converted_from_v2: 'converted from v2'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| You are running the latest version of HuskSync (v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Arquivos de configuração e mensagens recarregados.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
error_invalid_syntax: '[Error:](#ff3300) [Sintaxe incorreta. Utilize:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Error:](#ff3300) [Não foi possível encontrar um jogador com esse nome.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[Error:](#ff3300) [Você não tem permissão para executar este comando](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [Esse comando só pode ser executado através do console](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: Esse comando só pode ser usado dentro do jogo.'
|
||||
error_no_data_to_display: '[Error:](#ff3300) [Não encontramos nenhuma informação deste jogador para exibir.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Error:](#ff3300) [Não foi possível encontrar nenhuma informação deste jogador para essa versão UUID.](#ff7e5e)'
|
||||
husksync_command_description: 'Manage the HuskSync plugin'
|
||||
userdata_command_description: 'View, manage & restore player userdata'
|
||||
inventory_command_description: 'View & edit a player''s inventory'
|
||||
enderchest_command_description: 'View & edit a player''s Ender Chest'
|
||||
65
common/src/main/resources/locales/ru-ru.yml
Normal file
65
common/src/main/resources/locales/ru-ru.yml
Normal file
@@ -0,0 +1,65 @@
|
||||
locales:
|
||||
synchronization_complete: '[⏵ Данные синхронизированы!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Не удалось синхронизировать данные! Пожалуйста, обратитесь к администратору.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0Инвентарь %1%'
|
||||
ender_chest_viewer_menu_title: '&0Эндер-сундук %1%'
|
||||
inventory_viewer_opened: '[Просмотр снимка инвентаря](#00fb9a) [%1%](#00fb9a bold) [во время ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Просмотр снимка эндер-сундука](#00fb9a) [%1%](#00fb9a bold) [во время ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Ваши данные обновлены!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Не удалось обновить ваши данные! Пожалуйста, обратитесь к администратору.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ Регистрация пользователя завершена!](#00fb9a)'
|
||||
data_manager_title: '[Просмотр снимка данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a bold show_text=&7UUID игрока:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Время:\n&8Когда данные были сохранены)'
|
||||
data_manager_pinned: '[※ Снимок закреплен](#d8ff2b show_text=&7Закреплен:\n&8Этот снимок данных пользователя не будет автоматически применено.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Причина сохранения:\n&8Что привело к сохранению данных)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Сервер:\n&8Название сервера где данные были сохранены)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Размер:\n&8Предполагаемый размер снимка (в килобайтах))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Здоровье) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Голод) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Уровень опыта) [🏹 %5%](dark_aqua show_text=&7Игровой режим)'
|
||||
data_manager_advancements_statistics: '[⭐ Достижения: %1%](color=#ffc43b-#f5c962 show_text=&7Полученные вами достижения:\n&8%2%) [⌛ Отыгранное время: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Сколько времени вы отыграли\n&8⚠ Строится по внутреигровой статистике)\n'
|
||||
data_manager_item_buttons: '[Просмотр:](gray) [[🪣 Инвентарь…]](color=#a17b5f-#f5b98c show_text=&7Нажмите для просмотра run_command=/inventory %1% %2%) [[⌀ Эндер-сундук…]](#b649c4-#d254ff show_text=&7Нажмите для просмотра run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Управление:](gray) [[❌ Удалить…]](#ff3300 show_text=&7Нажмите для удаления этого снимка данных пользователя\n&8Не влияет на текущие данные пользователя.\n&#ff3300&⚠ Необратимое действие! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Восстановить…]](#00fb9a show_text=&7Нажмите для восстановления данных пользователя\n&8Это восстановит данные пользователя до этого снимка.\n&#ff3300&⚠ Текущие данные %1% будут перезаписаны! suggest_command=/husksync:userdata restore %1% %2%) [[※ Закрепить/Открепить…]](#d8ff2b show_text=&7Нажмите для закрепления или открепления снимка данных пользователя\n&8Закрепленные снимки данных не удаляются автоматически. run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[Система:](gray) [[⏷ Дамп в файл…]](dark_gray show_text=&7Нажмите для дампа исходного снимка данных пользователя в файл\n&8Файлы дампов данных находятся в ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Выгрузить дамп…]](dark_gray show_text=&7Нажмите для дампа исходного снимка данных пользователя с помощью сервиса mc-logs\n&8Вы получите ссылку с дампом данных. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'и еще %1%…'
|
||||
data_list_title: '[Снимки данных %1%:](#00fb9a) [(%2%-%3% из](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Снимок данных %4% пользователя %2%&8⚡ run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Закреплен:\n&8Закрепленные снимки данных не удаляются автоматически run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Время:&7\n&8Когда данные были сохранены\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Причина сохранения:\n&8Что привело к сохранению данных run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Размер:&7\n&8Предполагаемый размер снимка (в килобайтах) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_deleted: '[❌ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID игрока:\n&8%4%) [удален.](#00fb9a)'
|
||||
data_restored: '[⏪ Данные пользователя](#00fb9a) [%1%](#00fb9a show_text=&7UUID игрока:\n&8%2%) [из снимка](#00fb9a) [%3%](#00fb9a show_text=&7UUID снимка:\n&8%4%) [успешно восстановлены.](#00fb9a)'
|
||||
data_pinned: '[※ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID игрока:\n&8%4%) [успешно закреплен.](#00fb9a)'
|
||||
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%'
|
||||
list_footer: '\n%1%[Страница](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%\n'
|
||||
list_previous_page_button: '[◀](white show_text=&7Просмотр предыдущей страницы run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7Просмотр следующей страницы run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Перейти на страницу %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: 'отключение с сервера'
|
||||
save_cause_world_save: 'сохранение мира'
|
||||
save_cause_death: 'смерть'
|
||||
save_cause_server_shutdown: 'отключение сервера'
|
||||
save_cause_inventory_command: 'команда inventory'
|
||||
save_cause_enderchest_command: 'команда enderchest'
|
||||
save_cause_backup_restore: 'восстановление из снимка'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'миграция из MPDB'
|
||||
save_cause_legacy_migration: 'миграция с legacy'
|
||||
save_cause_converted_from_v2: 'конвертация с v2'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| Вы используете последнюю версию HuskSync (v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| Доступна новая версия HuskSync: v%1% (текущая: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Конфигурация и файлы локализации перезагружены.](#00fb9a)\n[⚠ Убедитесь, что файлы конфигурации обновлены на всех серверах!](#00fb9a)\n[Необходима перезагрузка для вступления изменений конфигурации в силу.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
error_invalid_syntax: '[Ошибка:](#ff3300) [Неправильный синтаксис. Используйте:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Ошибка:](#ff3300) [Не удалось найти игрока с данным именем.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[Ошибка:](#ff3300) [У вас недостаточно прав для выполнения данной команды.](#ff7e5e)'
|
||||
error_console_command_only: '[Ошибка:](#ff3300) [Данная команда может быть выполнена только из консоли.](#ff7e5e)'
|
||||
error_in_game_command_only: 'Ошибка: Данная команда может быть выполнена только в игре.'
|
||||
error_no_data_to_display: '[Ошибка:](#ff3300) [Не удалось найти никаких пользовательских данных для отображения.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Ошибка:](#ff3300) [Не удалось найти никаких пользовательских данных с этим UUID снимка.](#ff7e5e)'
|
||||
husksync_command_description: 'Управление плагином HuskSync'
|
||||
userdata_command_description: 'Просмотр, редактирование и восстановление пользовательских данных игрока'
|
||||
inventory_command_description: 'Просмотр и редактирование инвентаря игрока'
|
||||
enderchest_command_description: 'Просмотр и редактирование эндер-сундука игрока'
|
||||
65
common/src/main/resources/locales/tr-tr.yml
Normal file
65
common/src/main/resources/locales/tr-tr.yml
Normal file
@@ -0,0 +1,65 @@
|
||||
locales:
|
||||
synchronization_complete: '[⏵ Veri senkronize edildi!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Veriler senkronize edilemedi! Lütfen bir yönetici ile iletişime geçin.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1%''ın Envanteri'
|
||||
ender_chest_viewer_menu_title: '&0%1%''ın Ender Sandığı'
|
||||
inventory_viewer_opened: '[Görüntülenen anlık](#00fb9a) [%1%](#00fb9a bold)[''ın envanteri ⌚ %2% tarihine kadar](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Görüntülenen anlık](#00fb9a) [%1%](#00fb9a bold)[''ın Ender Sandığı ⌚ %2% tarihine kadar](#00fb9a)'
|
||||
data_update_complete: '[🔔 Verileriniz güncellendi!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Verileriniz güncellenirken bir hata oluştu! Lütfen bir yönetici ile iletişime geçin.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ Kullanıcı kaydı tamamlandı!](#00fb9a)'
|
||||
data_manager_title: '[Kullanıcı veri anlık görünümü](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%](#00fb9a bold show_text=&7Oyuncu UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Versiyon zaman damgası:\n&8Verinin ne zaman kaydedildiği)'
|
||||
data_manager_pinned: '[※ Anlık sabitlendi](#d8ff2b show_text=&7Sabitlendi:\n&8Bu kullanıcı veri anlığı otomatik olarak döndürülmeyecek.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Kaydetme sebebi:\n&8Verinin kaydedilme nedeni)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Sunucu:\n&8Verinin kaydedildiği sunucu adı)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Anlık boyutu:\n&8Anlının tahmini dosya boyutu (KiB cinsinden))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Can puanı) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Açlık puanı) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP seviyesi) [🏹 %5%](dark_aqua show_text=&7Oyun modu)'
|
||||
data_manager_advancements_statistics: '[⭐ Gelişmeler: %1%](color=#ffc43b-#f5c962 show_text=&7Gelişmelerdeki ilerlemeniz:\n&8%2%) [⌛ Oynama Süresi: %3%s](color=#62a9f5-#7ab8fa show_text=&7Oyunda geçen süre\n&8⚠ Oyun içi istatistiklere dayanır)\n'
|
||||
data_manager_item_buttons: '[Görünüm:](gray) [[🪣 Envanter…]](color=#a17b5f-#f5b98c show_text=&7Görüntülemek için tıklayın run_command=/inventory %1% %2%) [[⌀ Ender Sandığı…]](#b649c4-#d254ff show_text=&7Görüntülemek için tıklayın run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Yönet:](gray) [[❌ Sil…]](#ff3300 show_text=&7Bu kullanıcı veri anlığını silmek için tıklayın.\n&8Bu, kullanıcının mevcut verilerini etkilemez.\n&#ff3300&⚠ Geri alınamaz! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Geri Yükle…]](#00fb9a show_text=&7Bu kullanıcı verisini geri yüklemek için tıklayın.\n&8Bu, kullanıcının verisini bu anlığa ayarlar.\n&#ff3300&⚠ %1%''nın mevcut verisi üzerine yazılacak! suggest_command=/husksync:userdata restore %1% %2%) [[※ Sabitle/Çöz…]](#d8ff2b show_text=&7Bu kullanıcı veri anlığını sabitlemek veya çözmek için tıklayın\n&8Sabitlenmiş anlıklar otomatik olarak döndürülmez run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[Sistem:](gray) [[⏷ Dosya Dök…]](dark_gray show_text=&7Bu ham kullanıcı veri anlığını bir dosyaya dökmek için tıklayın.\n&8Veri dökümleri ~/plugins/HuskSync/dumps/ klasöründe bulunabilir. run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dök…]](dark_gray show_text=&7Bu ham kullanıcı veri anlığını mc-logs servisine dökme için tıklayın\n&8Verileri içeren bir URL ile sağlanacaksınız. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 've %1% daha fazla…'
|
||||
data_list_title: '[%1%''ın kullanıcı veri anlıkları:](#00fb9a) [(%2%-%3% /](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Oyuncu Veri Anlığı %2% için %3%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Sabitlendi:\n&8Sabitlenmiş anlıklar otomatik olarak döndürülmez. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versiyon zaman damgası:&7\n&8Verinin ne zaman kaydedildiği\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Kaydetme sebebi:\n&8Verinin kaydedilme nedeni run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Anlık boyutu:&7\n&8Anlının tahmini dosya boyutu (KiB cinsinden) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_deleted: '[❌ Kullanıcı veri anlığı başarıyla silindi](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Başarıyla geri yüklendi](#00fb9a) [%1%](#00fb9a show_text=&7Oyuncu UUID:\n&8%2%)[''ın mevcut kullanıcı verisi anlığından](#00fb9a) [%3%.](#00fb9a show_text=&7Versiyon UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Kullanıcı veri anlığı başarıyla sabitlendi](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
|
||||
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%'
|
||||
list_footer: '\n%1%[Sayfa](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7Önceki sayfayı görüntüle run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7Sonraki sayfayı görüntüle run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Sayfaya git %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: 'bağlantı kesilmesi'
|
||||
save_cause_world_save: 'dünya kaydı'
|
||||
save_cause_death: 'ölüm'
|
||||
save_cause_server_shutdown: 'sunucu kapatma'
|
||||
save_cause_inventory_command: 'envanter komutu'
|
||||
save_cause_enderchest_command: 'ender sandığı komutu'
|
||||
save_cause_backup_restore: 'yedek geri yükleme'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB migration'
|
||||
save_cause_legacy_migration: 'legacy migration'
|
||||
save_cause_converted_from_v2: 'v2 den dönüştürüldü'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| HuskSync\''in en son sürümünü kullanıyorsunuz (v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| HuskSync\''in yeni bir sürümü mevcut: v%1% (kullanılan sürüm: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Yapılandırma ve mesaj dosyaları yeniden yüklendi.](#00fb9a)\n[⚠ Lütfen yapılandırma dosyalarının tüm sunucularda güncel olduğundan emin olun!](#00fb9a)\n[Yapılandırma değişikliklerinin etkili olabilmesi için bir yeniden başlatma gereklidir.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| Sistem durumu raporu:](#00fb9a)'
|
||||
error_invalid_syntax: '[Hata:](#ff3300) [Yanlış sözdizimi. Kullanım:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Öneri için tıklayın Suggest_command=%1%)'
|
||||
error_invalid_player: '[Hata:](#ff3300) [Bu isimde bir oyuncu bulunamadı.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[Hata:](#ff3300) [Bu komutu gerçekleştirmek için izniniz yok](#ff7e5e)'
|
||||
error_console_command_only: '[Hata:](#ff3300) [Bu komut yalnızca konsoldan çalıştırılabilir](#ff7e5e)'
|
||||
error_in_game_command_only: 'Hata: Bu komut yalnızca oyun içinde kullanılabilir.'
|
||||
error_no_data_to_display: '[Hata:](#ff3300) [Görüntülenecek kullanıcı verisi bulunamadı.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Hata:](#ff3300) [Bu sürüm UUID''si için kullanıcı verisi bulunamadı.](#ff7e5e)'
|
||||
husksync_command_description: 'HuskSync eklentisini yönet'
|
||||
userdata_command_description: 'Oyuncu verilerini görüntüle, yönet ve geri yükle'
|
||||
inventory_command_description: 'Oyuncunun envanterini görüntüle ve düzenle'
|
||||
enderchest_command_description: 'Oyuncunun Ender Chest''ini görüntüle ve düzenle'
|
||||
@@ -1,49 +1,65 @@
|
||||
synchronization_complete: '[⏵ Дані синхронізовано!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Failed to synchronize your data! Please contact an administrator.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1%''s Inventory'
|
||||
ender_chest_viewer_menu_title: '&0%1%''s Ender Chest'
|
||||
inventory_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold)[''s inventory as of ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold)[''s Ender Chest as of ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Your data has been updated!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Failed to update your data! Please contact an administrator.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
data_manager_title: '[Viewing user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\n&8When the data was saved)'
|
||||
data_manager_pinned: '[※ Snapshot pinned](#d8ff2b show_text=&7Pinned:\n&8This user data snapshot won''t be automatically rotated.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Health points) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hunger points) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)'
|
||||
data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements you have progress in:\n&8%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\n&8⚠ Based on in-game statistics)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventory…]](color=#a17b5f-#f5b98c show_text=&7Click to view run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Click to view run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ Delete…]](#ff3300 show_text=&7Click to delete this snapshot of user data.\n&8This will not affect the user''s current data.\n&#ff3300&⚠ This cannot be undone! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Restore…]](#00fb9a show_text=&7Click to restore this user data.\n&8This will set the user''s data to this snapshot.\n&#ff3300&⚠ %1%''s current data will be overwritten! suggest_command=/husksync:userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click to pin or unpin this user data snapshot\n&8Pinned snapshots won''t be automatically rotated run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'and %1% more…'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ Successfully unpinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| You are running the latest version of HuskSync (v%1%).](#00fb9a)'
|
||||
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)'
|
||||
error_invalid_syntax: '[Помилка:](#ff3300) [Неправильний синтакс. Використання:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Помилка:](#ff3300) [Гравця не знайдено](#ff7e5e)'
|
||||
error_no_permission: '[Помилка:](#ff3300) [Ввас немає дозволу на використання цієї команди](#ff7e5e)'
|
||||
error_console_command_only: '[Помилка:](#ff3300) [Ця команда може бути використана лише з допомогою %1% консолі](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: That command can only be used in-game.'
|
||||
error_no_data_to_display: '[Error:](#ff3300) [Could not find any user data to display.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Error:](#ff3300) [Could not find any user data for that version UUID.](#ff7e5e)'
|
||||
husksync_command_description: 'Manage the HuskSync plugin'
|
||||
userdata_command_description: 'View, manage & restore player userdata'
|
||||
inventory_command_description: 'View & edit a player''s inventory'
|
||||
enderchest_command_description: 'View & edit a player''s Ender Chest'
|
||||
locales:
|
||||
synchronization_complete: '[⏵ Дані синхронізовано!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ Failed to synchronize your data! Please contact an administrator.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1%''s Inventory'
|
||||
ender_chest_viewer_menu_title: '&0%1%''s Ender Chest'
|
||||
inventory_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold)[''s inventory as of ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold)[''s Ender Chest as of ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Your data has been updated!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Failed to update your data! Please contact an administrator.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
data_manager_title: '[Viewing user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\n&8When the data was saved)'
|
||||
data_manager_pinned: '[※ Snapshot pinned](#d8ff2b show_text=&7Pinned:\n&8This user data snapshot won''t be automatically rotated.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Health points) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hunger points) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)'
|
||||
data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements you have progress in:\n&8%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\n&8⚠ Based on in-game statistics)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventory…]](color=#a17b5f-#f5b98c show_text=&7Click to view run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Click to view run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ Delete…]](#ff3300 show_text=&7Click to delete this snapshot of user data.\n&8This will not affect the user''s current data.\n&#ff3300&⚠ This cannot be undone! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Restore…]](#00fb9a show_text=&7Click to restore this user data.\n&8This will set the user''s data to this snapshot.\n&#ff3300&⚠ %1%''s current data will be overwritten! suggest_command=/husksync:userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click to pin or unpin this user data snapshot\n&8Pinned snapshots won''t be automatically rotated run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: 'and %1% more…'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
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%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: 'disconnect'
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB migration'
|
||||
save_cause_legacy_migration: 'legacy migration'
|
||||
save_cause_converted_from_v2: 'converted from v2'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| You are running the latest version of HuskSync (v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Перезавантажено конфіґ та файли повідомлень.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
error_invalid_syntax: '[Помилка:](#ff3300) [Неправильний синтакс. Використання:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[Помилка:](#ff3300) [Гравця не знайдено](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[Помилка:](#ff3300) [Ввас немає дозволу на використання цієї команди](#ff7e5e)'
|
||||
error_console_command_only: '[Помилка:](#ff3300) [Ця команда може бути використана лише з допомогою %1% консолі](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: That command can only be used in-game.'
|
||||
error_no_data_to_display: '[Error:](#ff3300) [Could not find any user data to display.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Error:](#ff3300) [Could not find any user data for that version UUID.](#ff7e5e)'
|
||||
husksync_command_description: 'Manage the HuskSync plugin'
|
||||
userdata_command_description: 'View, manage & restore player userdata'
|
||||
inventory_command_description: 'View & edit a player''s inventory'
|
||||
enderchest_command_description: 'View & edit a player''s Ender Chest'
|
||||
@@ -1,49 +1,65 @@
|
||||
synchronization_complete: '[⏵ 数据同步完成!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ 无法同步数据! 请联系管理员.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1% 的背包'
|
||||
ender_chest_viewer_menu_title: '&0%1% 的末影箱'
|
||||
inventory_viewer_opened: '[正在查看玩家](#00fb9a) [%1%](#00fb9a bold) [于 ⌚ %2% 的背包备份](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[正在查看玩家](#00fb9a) [%1%](#00fb9a bold) [于 ⌚ %2% 的末影箱备份](#00fb9a)'
|
||||
data_update_complete: '[🔔 你的用户数据已更新!](#00fb9a)'
|
||||
data_update_failed: '[🔔 无法更新你的用户数据! 请联系管理员.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
data_manager_title: '[正在查看玩家](#00fb9a) [%3%](#00fb9a bold show_text=&7玩家 UUID:\n&8%4%) [的数据备份](#00fb9a) [%1%](#00fb9a show_text=&7备份版本 UUID:\n&8%2%) [:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7备份时间:\n&7何时保存了此数据)'
|
||||
data_manager_pinned: '[※ 置顶备份](#d8ff2b show_text=&7置顶:\n&8此数据备份不会按照备份时间自动排序.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7备份原因:\n&7为何保存了此数据)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7血量) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7饱食度) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7经验等级) [🏹 %5%](dark_aqua show_text=&7游戏模式)'
|
||||
data_manager_advancements_statistics: '[⭐ 成就: %1%](color=#ffc43b-#f5c962 show_text=&7%2%) [⌛ 游玩时间: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7⚠ 基于游戏内的统计)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 背包…]](color=#a17b5f-#f5b98c show_text=&7点击查看 run_command=/inventory %1% %2%) [[⌀ 末影箱…]](#b649c4-#d254ff show_text=&7点击查看 run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ 删除…]](#ff3300 show_text=&7点击删除此数据备份.\n这不会影响玩家当前的数据.\n&#ff3300&⚠ 此操作不可撤销! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ 恢复…]](#00fb9a show_text=&7点击让玩家恢复到此数据备份.\n这将会使玩家的数据恢复到这个备份.\n&#ff3300&⚠ %1% 当前的用户数据会被备份数据所覆盖! suggest_command=/husksync:userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click to pin or unpin this user data snapshot\n&8Pinned snapshots won''t be automatically rotated run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '还有 %1% …'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_deleted: '[❌ 成功删除玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&7%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本 UUID:\n&7%2%)'
|
||||
data_restored: '[⏪ 成功恢复玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&7%2%)[的数据备份](#00fb9a) [%3%.](#00fb9a show_text=&7备份版本 UUID:\n&7%4%)'
|
||||
data_pinned: '[※ 成功置顶玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本 UUID:\n&8%2%)'
|
||||
data_unpinned: '[※ 成功取消置顶玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本 UUID:\n&8%2%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| 你正在使用最新版本的HuskSync (v%1%)](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| 一个新版本的HuskSync已经可以更新: v%1% (当前: 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)'
|
||||
error_invalid_syntax: ':](#ff3300) [格式错误, 使用方法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[错误:](#ff3300) [无法找到目标玩家.](#ff7e5e)'
|
||||
error_no_permission: '[错误:](#ff3300) [你没有执行此指令的权限](#ff7e5e)'
|
||||
error_console_command_only: '[错误:](#ff3300) [该指令只能在控制台运行](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: 该指令只能在游戏内运行.'
|
||||
error_no_data_to_display: '[错误:](#ff3300) [无法找到用户数据显示.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[错误:](#ff3300) [无法找到该备份的 UUID .](#ff7e5e)'
|
||||
husksync_command_description: 'Manage the HuskSync plugin'
|
||||
userdata_command_description: 'View, manage & restore player userdata'
|
||||
inventory_command_description: 'View & edit a player''s inventory'
|
||||
enderchest_command_description: 'View & edit a player''s Ender Chest'
|
||||
locales:
|
||||
synchronization_complete: '[⏵ 数据同步完成!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ 无法同步你的数据! 请联系管理员.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1% 的背包'
|
||||
ender_chest_viewer_menu_title: '&0%1% 的末影箱'
|
||||
inventory_viewer_opened: '[查看备份](#00fb9a) [%1%](#00fb9a bold) [于 ⌚ %2% 的背包备份](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[查看备份](#00fb9a) [%1%](#00fb9a bold) [于 ⌚ %2% 的末影箱备份](#00fb9a)'
|
||||
data_update_complete: '[🔔 你的数据已更新!](#00fb9a)'
|
||||
data_update_failed: '[🔔 无法更新你的数据! 请联系管理员.](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ 用户注册完成!](#00fb9a)'
|
||||
data_manager_title: '[正在查看玩家](#00fb9a) [%3%](#00fb9a bold show_text=&7玩家UUID:\n&8%4%) [的数据备份](#00fb9a) [%1%](#00fb9a show_text=&7备份版本UUID:\n&8%2%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7备份时间:\n&8数据保存时间)'
|
||||
data_manager_pinned: '[※ 置顶备份](#d8ff2b show_text=&7已置顶:\n&8此玩家数据备份不会按照备份时间自动排序.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存原因:\n&8导致数据保存的原因)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7服务器:\n&8数据保存所在服务器的名称)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7备份大小:\n&8预计备份的文件大小(以KiB为单位))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7生命值) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7饥饿值) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7经验等级) [🏹 %5%](dark_aqua show_text=&7游戏模式)'
|
||||
data_manager_advancements_statistics: '[⭐ 进度: %1%](color=#ffc43b-#f5c962 show_text=&7你已经取得的进度:\n&8%2%) [⌛ 游玩时间: %3%小时](color=#62a9f5-#7ab8fa show_text=&7在游戏内游玩的时间\n&8⚠ 基于游戏内的统计信息)\n'
|
||||
data_manager_item_buttons: '[查看:](gray) [[🪣 背包…]](color=#a17b5f-#f5b98c show_text=&7点击查看 run_command=/inventory %1% %2%) [[⌀ 末影箱…]](#b649c4-#d254ff show_text=&7点击查看 run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[管理:](gray) [[❌ 删除…]](#ff3300 show_text=&7点击删除此玩家数据备份.\n&8这不会影响用户的当前数据.\n&#ff3300&⚠ 此操作无法撤消! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ 恢复…]](#00fb9a show_text=&7点击还原此玩家数据.\n&8这将使用户的数据恢复到此备份.\n&#ff3300&⚠ %1%当前数据将被覆盖! suggest_command=/husksync:userdata restore %1% %2%) [[※ 置顶/取消置顶…]](#d8ff2b show_text=&7点击置顶或取消置顶此玩家数据备份\n&8已置顶的备份不会按照备份时间自动排序 run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[系统:](gray) [[⏷ 文件转储…]](dark_gray show_text=&7点击将此原始玩家数据备份转储到文件中.\n&8数据转储可以在~/plugins/HuskSync/dumps/中找到 run_command=/husksync:userdata dump %1% %2% file) [[☂ 网络转储…]](dark_gray show_text=&7点击将此原始玩家数据备份转储到mc-logs服务中\n&8你将获得包含数据的网址. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '以及其他 %1%…'
|
||||
data_list_title: '[%1%的玩家数据备份:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7玩家数据备份 %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7已置顶:\n&8已置顶的备份不会按照备份时间自动排序 run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7备份时间:&7\n&8数据保存时间\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7保存原因:\n&8导致数据保存的原因 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7备份大小:&7\n&8预计备份文件大小(以KiB为单位) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7%2%的用户数据快照\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7置顶:\n&8已置顶的快照不会自动排序. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&无效的快照数据\n&#ff7e5e&点击删除\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_deleted: '[❌ 成功删除玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&7%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本UUID:\n&7%2%)'
|
||||
data_restored: '[⏪ 成功恢复玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&7%2%)[的数据备份](#00fb9a) [%3%.](#00fb9a show_text=&7备份版本UUID:\n&7%4%)'
|
||||
data_pinned: '[※ 成功置顶玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本UUID:\n&8%2%)'
|
||||
data_unpinned: '[※ 成功取消置顶玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本UUID:\n&8%2%)'
|
||||
data_dumped: '[☂ 已成功将 %1% 的玩家数据快照 %2% 转储到:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[页数](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7查看上一页 run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7查看下一页 run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7跳转至第 %1% 页 run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: '断开连接'
|
||||
save_cause_world_save: '保存世界'
|
||||
save_cause_death: '死亡'
|
||||
save_cause_server_shutdown: '服务器关闭'
|
||||
save_cause_inventory_command: '背包命令'
|
||||
save_cause_enderchest_command: '末影箱命令'
|
||||
save_cause_backup_restore: '备份还原'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB迁移'
|
||||
save_cause_legacy_migration: '旧版迁移'
|
||||
save_cause_converted_from_v2: '从v2转换'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| 你正在运行最新版本的HuskSync(v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| 检测到HuskSync有新版本可以更新了:v%1%(当前版本:v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 重新加载配置和消息文件完成.](#00fb9a)\n[⚠ 确保在所有服务器上更新配置文件!](#00fb9a)\n[需要重新启动才能使配置更改生效.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| 系统状态报告:](#00fb9a)'
|
||||
error_invalid_syntax: '[错误:](#ff3300) [语法错误.用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&点击建议 suggest_command=%1%)'
|
||||
error_invalid_player: '[错误:](#ff3300) [找不到这个名称的玩家.](#ff7e5e)'
|
||||
error_invalid_data: '[错误:](#ff3300) [无法解压缩快照数据, 因为它无效或已损坏.](#ff7e5e) [(详情…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[错误:](#ff3300) [你没有执行此命令的权限](#ff7e5e)'
|
||||
error_console_command_only: '[错误:](#ff3300) [该命令只能在控制台中运行](#ff7e5e)'
|
||||
error_in_game_command_only: '错误: 该命令只能在游戏中使用.'
|
||||
error_no_data_to_display: '[错误:](#ff3300) [找不到要显示的任何玩家数据.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[错误:](#ff3300) [找不到该版本UUID的任何玩家数据.](#ff7e5e)'
|
||||
husksync_command_description: '管理HuskSync插件'
|
||||
userdata_command_description: '查看、管理和还原玩家玩家数据'
|
||||
inventory_command_description: '查看和编辑玩家的背包'
|
||||
enderchest_command_description: '查看和编辑玩家的末影箱'
|
||||
|
||||
@@ -1,49 +1,65 @@
|
||||
synchronization_complete: '[⏵資料已同步!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ 無法同步您的資料! 請聯繫管理員](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1% 的背包'
|
||||
ender_chest_viewer_menu_title: '&0%1% 的終界箱'
|
||||
inventory_viewer_opened: '[查看](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的背包快照資料](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[查看](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的終界箱快照資料](#00fb9a)'
|
||||
data_update_complete: '[🔔 你的資料已更新!](#00fb9a)'
|
||||
data_update_failed: '[🔔 無法更新您的資料! 請聯繫管理員](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
data_manager_title: '[查看](#00fb9a) [%3%](#00fb9a bold show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7快照時間:\n&8何時保存的資料)'
|
||||
data_manager_pinned: '[※ 被標記的快照](#d8ff2b show_text=&7標記:\n&8此快照資料不會自動輪換更新)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存原因:\n&8保存此快照的原因)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7血量) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7飽食度) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7經驗等級) [🏹 %5%](dark_aqua show_text=&7遊戲模式)'
|
||||
data_manager_advancements_statistics: '[⭐ 成就: %1%](color=#ffc43b-#f5c962 show_text=&7已獲得的成就:\n&8%2%) [⌛ 遊戲時間: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7遊戲內的遊玩時間\n&8⚠ 根據遊戲內統計)\n'
|
||||
data_manager_item_buttons: '[查看:](gray) [[🪣 背包…]](color=#a17b5f-#f5b98c show_text=&7點擊查看 run_command=/inventory %1% %2%) [[⌀ 終界箱…]](#b649c4-#d254ff show_text=&7點擊查看 run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[管理:](gray) [[❌ 刪除…]](#ff3300 show_text=&7點擊刪除這個快照\n&8這不會影像目前玩家的資料\n&#ff3300&⚠ 此操作不能取消! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ 恢復…]](#00fb9a show_text=&7點擊將玩家資料覆蓋為此快照\n&8這將導致玩家的資料會被此快照覆蓋\n&#ff3300&⚠ %1% 當前的資料將被覆蓋! suggest_command=/husksync:userdata restore %1% %2%) [[※ 標記…]](#d8ff2b show_text=&7點擊切換標記狀態\n&8被標記的快照將不會自動輪換更新 run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[系統:](gray) [[⏷ 本地轉存…]](dark_gray show_text=&7點擊將此玩家資料快照轉存到本地文件中\n&8轉存的資料可以在以下路徑找到 ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ 雲端轉存…]](dark_gray show_text=&7點擊將此玩家資料快照轉存到 mc-logs 服務\n&8您將獲得一個包含資料的 URL. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '還有 %1% …'
|
||||
data_list_title: '[%1% 的玩家資料快照:](#00fb9a) [(%2%-%3% 共](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_deleted: '[❌ 成功刪除:](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%)'
|
||||
data_restored: '[⏪ 成功將玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&8%2%)[的資料恢復為 快照:](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ 成功標記](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%)'
|
||||
data_unpinned: '[※ 成功解除](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [的標記](#00fb9a)'
|
||||
data_dumped: '[☂ 成功將 %2% 資料快照 %1% 儲存至:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[頁面](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7查看上一頁 run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7查看下一頁 run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7跳至第 %1% 頁 run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| 您運行的是最新版本的 HuskSync (v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| 發現可用的新版本: v%1% (running: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 已重新載入配置和訊息文件](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||
error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[錯誤:](#ff3300) [找不到這位玩家](#ff7e5e)'
|
||||
error_no_permission: '[錯誤:](#ff3300) [您沒有權限執行這個指令](#ff7e5e)'
|
||||
error_console_command_only: '[錯誤:](#ff3300) [該指令只能透過 控制台 執行](#ff7e5e)'
|
||||
error_in_game_command_only: '[錯誤:](#ff3300) [該指令只能在遊戲內執行](#ff7e5e)'
|
||||
error_no_data_to_display: '[錯誤:](#ff3300) [找不到任何可顯示的用戶資訊.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[錯誤:](#ff3300) [找不到正確的 Version UUID.](#ff7e5e)'
|
||||
husksync_command_description: 'Manage the HuskSync plugin'
|
||||
userdata_command_description: 'View, manage & restore player userdata'
|
||||
inventory_command_description: 'View & edit a player''s inventory'
|
||||
enderchest_command_description: 'View & edit a player''s Ender Chest'
|
||||
locales:
|
||||
synchronization_complete: '[⏵ 資料已同步!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ 無法同步您的資料! 請聯繫管理員](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1% 的背包'
|
||||
ender_chest_viewer_menu_title: '&0%1% 的終界箱'
|
||||
inventory_viewer_opened: '[查看備份](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的背包備份](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[查看備份](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的終界箱備份](#00fb9a)'
|
||||
data_update_complete: '[🔔 你的數據資料已更新!](#00fb9a)'
|
||||
data_update_failed: '[🔔 無法更新你的數據資料! 請聯繫管理員](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ 用戶註冊完成!](#00fb9a)'
|
||||
data_manager_title: '[正在查看玩家](#00fb9a) [%3%](#00fb9a bold show_text=&7玩家UUID:\n&8%4%) [的數據備份](#00fb9a) [%1%](#00fb9a show_text=&7備份版本UUID:\n&8%2%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7備份時間:\n&8數據保存時間)'
|
||||
data_manager_pinned: '[※ 被標記的備份](#d8ff2b show_text=&7已標記:\n&8此玩家數據備份不會按照備份時間自動排序)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存原因:\n&8導致數據保存的原因)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7伺服器:\n&8數據保存所在伺服器的名稱)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7備份大小:\n&8備份的估計文件大小(以KiB為單位))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7血量) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7飽食度) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7經驗等級) [🏹 %5%](dark_aqua show_text=&7遊戲模式)'
|
||||
data_manager_advancements_statistics: '[⭐ 成就: %1%](color=#ffc43b-#f5c962 show_text=&7已獲得的成就:\n&8%2%) [⌛ 遊玩時間: %3%小時](color=#62a9f5-#7ab8fa show_text=&7在遊戲内遊玩的時間\n&8⚠ 基於遊戲内的統計訊息)\n'
|
||||
data_manager_item_buttons: '[查看:](gray) [[🪣 背包…]](color=#a17b5f-#f5b98c show_text=&7點擊查看 run_command=/inventory %1% %2%) [[⌀ 終界箱…]](#b649c4-#d254ff show_text=&7點擊查看 run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[管理:](gray) [[❌ 刪除…]](#ff3300 show_text=&7點擊刪除此玩家數據的備份.\n&8這不會影響玩家的當前數據.\n&#ff3300&⚠ 此操作無法撤銷! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ 恢復…]](#00fb9a show_text=&7點擊還原此玩家的數據.\n&8這將會讓用户的數據恢復到此備份.\n&#ff3300&⚠ %1%當前的數據將被覆蓋! suggest_command=/husksync:userdata restore %1% %2%) [[※ 標記/取消標記…]](#d8ff2b show_text=&7點擊標記或取消標記此玩家數據備份\n&8已標記的備份不會按照備份時間自動排序 run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[系統:](gray) [[⏷ 本地轉存…]](dark_gray show_text=&7點擊將此玩家數據資料轉存到本地文件中.\n&8轉存的資料可以在以下路徑找到~/plugins/HuskSync/dumps/中找到 run_command=/husksync:userdata dump %1% %2% file) [[☂ 雲端轉存…]](dark_gray show_text=&7點擊將此玩家數據資料轉存到 mc-logs 服務中\n&8您將獲得一個包含資料的URL. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '以及其他 %1%…'
|
||||
data_list_title: '[%1%的玩家數據備份:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7玩家數據備份 %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7已標記:\n&8標記的備份不會自動加載 run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7備份時間:&7\n&8數據保存時間\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7保存原因:\n&8導致數據保存的原因 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7備份大小:&7\n&8備份的估計文件大小(以KiB為單位) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_deleted: '[❌ 成功刪除玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&7%4%) [的數據備份](#00fb9a) [%1%.](#00fb9a show_text=&7備份版本UUID:\n&7%2%)'
|
||||
data_restored: '[⏪ 成功恢復玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&7%2%)[的數據備份](#00fb9a) [%3%.](#00fb9a show_text=&7備份版本UUID:\n&7%4%)'
|
||||
data_pinned: '[※ 成功標記玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的數據備份](#00fb9a) [%1%.](#00fb9a show_text=&7備份版本UUID:\n&8%2%)'
|
||||
data_unpinned: '[※ 成功取消標記玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的數據備份](#00fb9a) [%1%.](#00fb9a show_text=&7備份版本UUID:\n&8%2%)'
|
||||
data_dumped: '[☂ 已成功將 %1% 的玩家數據快照 %2% 儲存至:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[頁數](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7查看上一頁 run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7查看下一頁 run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7跳至第 %1% 頁 run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: '斷開連接'
|
||||
save_cause_world_save: '保存世界'
|
||||
save_cause_death: '死亡'
|
||||
save_cause_server_shutdown: '伺服器關閉'
|
||||
save_cause_inventory_command: '背包指令'
|
||||
save_cause_enderchest_command: '終界箱指令'
|
||||
save_cause_backup_restore: '備份還原'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB遷移'
|
||||
save_cause_legacy_migration: '舊版遷移'
|
||||
save_cause_converted_from_v2: '從v2轉換'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| 您運行的是最新版本的HuskSync(v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| 發現可用的新版本:v%1%(當前版本:v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 已重新載入配置和訊息文件.](#00fb9a)\n[⚠ 確保所有伺服器上的配置文件都是最新的!](#00fb9a)\n[需要重新啟動配置更改才能生效.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| 系統狀態報告:](#00fb9a)'
|
||||
error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&點擊建議 suggest_command=%1%)'
|
||||
error_invalid_player: '[錯誤:](#ff3300) [找不到這位玩家.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[錯誤:](#ff3300) [您沒有權限執行這個指令](#ff7e5e)'
|
||||
error_console_command_only: '[錯誤:](#ff3300) [該指令只能透過 控制台 執行](#ff7e5e)'
|
||||
error_in_game_command_only: '錯誤: 該指令只能在遊戲內執行.'
|
||||
error_no_data_to_display: '[錯誤:](#ff3300) [找不到任何可顯示的玩家數據.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[錯誤:](#ff3300) [找不到該版本UUID的任何玩家數據.](#ff7e5e)'
|
||||
husksync_command_description: '管理HuskSync插件'
|
||||
userdata_command_description: '查看、管理和恢復玩家用户數據'
|
||||
inventory_command_description: '查看和編輯玩家的背包'
|
||||
enderchest_command_description: '查看和編輯玩家的終界箱'
|
||||
|
||||
@@ -19,9 +19,8 @@
|
||||
|
||||
package net.william278.husksync.config;
|
||||
|
||||
import net.william278.annotaml.Annotaml;
|
||||
import de.exlll.configlib.YamlConfigurations;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -30,14 +29,14 @@ import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.net.URL;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("Locales Tests")
|
||||
public class LocalesTests {
|
||||
|
||||
@@ -48,10 +47,10 @@ public class LocalesTests {
|
||||
@Test
|
||||
public void testLoadEnglishLocales() {
|
||||
try (InputStream locales = LocalesTests.class.getClassLoader().getResourceAsStream("locales/en-gb.yml")) {
|
||||
Assertions.assertNotNull(locales, "en-gb.yml is missing from the locales folder");
|
||||
englishLocales = Annotaml.create(Locales.class, locales).get();
|
||||
} catch (IOException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
assertNotNull(locales, "en-gb.yml is missing from the locales folder");
|
||||
englishLocales = YamlConfigurations.read(locales, Locales.class);
|
||||
} catch (Throwable e) {
|
||||
fail("Failed to load en-gb.yml", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,23 +58,20 @@ public class LocalesTests {
|
||||
@DisplayName("Test All Locale Keys Present")
|
||||
@MethodSource("provideLocaleFiles")
|
||||
public void testAllLocaleKeysPresent(@NotNull File file, @SuppressWarnings("unused") @NotNull String keyName) {
|
||||
try {
|
||||
final Set<String> fileKeys = Annotaml.create(file, Locales.class).get().rawLocales.keySet();
|
||||
englishLocales.rawLocales.keySet()
|
||||
.forEach(key -> Assertions.assertTrue(fileKeys.contains(key),
|
||||
"Locale key " + key + " is missing from " + file.getName()));
|
||||
} catch (IOException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
final Set<String> fileKeys = YamlConfigurations.load(file.toPath(), Locales.class).locales.keySet();
|
||||
englishLocales.locales.keySet().forEach(key -> assertTrue(
|
||||
fileKeys.contains(key), "Locale key " + key + " is missing from " + file.getName()
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Stream<Arguments> provideLocaleFiles() {
|
||||
URL url = LocalesTests.class.getClassLoader().getResource("locales");
|
||||
Assertions.assertNotNull(url, "locales folder is missing");
|
||||
return Stream.of(Objects.requireNonNull(new File(url.getPath())
|
||||
.listFiles(file -> file.getName().endsWith("yml") && !file.getName().equals("en-gb.yml"))))
|
||||
.map(file -> Arguments.of(file, file.getName().replace(".yml", "")));
|
||||
final URL url = LocalesTests.class.getClassLoader().getResource("locales");
|
||||
assertNotNull(url, "locales folder is missing");
|
||||
|
||||
return Stream.of(Objects.requireNonNull(new File(url.getPath()).listFiles(
|
||||
file -> file.getName().endsWith("yml") && !file.getName().equals("en-gb.yml")
|
||||
))).map(file -> Arguments.of(file, file.getName().replace(".yml", "")));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,11 +24,11 @@ import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@DisplayName("Plan Hook Tests")
|
||||
public class PlanDataExtensionTests {
|
||||
public class PlanHookTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Test Plan Hook Implementation")
|
||||
public void testPlanHookImplementation() {
|
||||
@DisplayName("Test Plan Data Extension")
|
||||
public void testPlanDataExtension() {
|
||||
new ExtensionExtractor(new PlanHook.PlanDataExtension()).validateAnnotations();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
HuskSync provides three API events your plugin can listen to when certain parts of the data synchronization process are performed. These events deal with HuskSync class types, so you may want to familiarize yourself with the [API basics](API) first. Two of the events can be cancelled (thus aborting the synchronization process at certain stages), and some events expose methods letting you affect their outcome (such as modifying the data that is saved during the process).
|
||||
|
||||
Consult the Javadocs for more information—and don't forget to register your listener when listening for these event calls. Please note that carrying out expensive blocking operations during these events is strongly discouraged as this may affect plugin performance.
|
||||
Consult the Javadocs for more information. Please note that carrying out expensive blocking operations during these events is strongly discouraged as this may affect plugin performance.
|
||||
|
||||
## List of API Events
|
||||
## Bukkit Platform Events
|
||||
> **Tip:** Don't forget to register your listener when listening for these event calls.
|
||||
>
|
||||
| Bukkit Event class | Cancellable | Description |
|
||||
|---------------------------|:-----------:|---------------------------------------------------------------------------------------------|
|
||||
| `BukkitDataSaveEvent` | ✅ | Called when player data snapshot is created, saved and cached due to a DataSaveCause |
|
||||
|
||||
40
docs/API.md
40
docs/API.md
@@ -1,7 +1,7 @@
|
||||
The HuskSync API (v3) provides methods for retrieving and updating [data snapshots](Data-Snapshot-API), a number of [[API Events]] for tracking when user data is synced and saved, and infrastructure for registering serializers to [synchronise custom data types](Custom-Data-API).
|
||||
|
||||
## Compatibility
|
||||
[](https://repo.william278.net/#/releases/net/william278/husksync/)
|
||||
[](https://repo.william278.net/#/releases/net/william278/husksync/)
|
||||
|
||||
The HuskSync API shares version numbering with the plugin itself for consistency and convenience. Please note minor and patch plugin releases may make API additions and deprecations, but will not introduce breaking changes without notice.
|
||||
|
||||
@@ -11,21 +11,32 @@ The HuskSync API shares version numbering with the plugin itself for consistency
|
||||
| v2.x | _v2.0—v2.2.8_ | ❌ |
|
||||
| v1.x | _v1.0—v1.4.1_ | ❌️ |
|
||||
|
||||
### Platforms
|
||||
> **Note:** For versions older than `v3.3`, the HuskSync API was only distributed for the Bukkit platform (as `net.william278:husksync`)
|
||||
|
||||
The HuskSync API is available for the following platforms:
|
||||
|
||||
* `bukkit` - Bukkit, Spigot, Paper, etc. Provides Bukkit API event listeners and adapters to `org.bukkit` objects.
|
||||
* `common` - Common API for all platforms.
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Targeting older versions</summary>
|
||||
|
||||
HuskSync versions prior to `v2.2.5` are distributed on [JitPack](https://jitpack.io/#/net/william278/HuskSync), and you will need to use the `https://jitpack.io` repository instead.
|
||||
* The HuskSync API was only distributed for the Bukkit module prior to `v3.3`; the artifact ID was `net.william278:husksync` instead of `net.william278.husksync:husksync-PLATFORM`.
|
||||
* HuskSync versions prior to `v2.2.5` are distributed on [JitPack](https://jitpack.io/#/net/william278/HuskSync), and you will need to use the `https://jitpack.io` repository instead.
|
||||
</details>
|
||||
|
||||
## Table of Contents
|
||||
1. [API Introduction](#api-introduction)
|
||||
1. [Setup with Maven](#11-setup-with-maven)
|
||||
2. [Setup with Gradle](#12-setup-with-gradle)
|
||||
2. [Creating a class to interface with the API](#3-creating-a-class-to-interface-with-the-api)
|
||||
3. [Checking if HuskSync is present and creating the hook](#4-checking-if-husksync-is-present-and-creating-the-hook)
|
||||
4. [Getting an instance of the API](#5-getting-an-instance-of-the-api)
|
||||
5. [CompletableFuture and Optional basics](#6-completablefuture-and-optional-basics)
|
||||
6. [Next steps](#7-next-steps)
|
||||
2. [Adding HuskSync as a dependency](#2-adding-husksync-as-a-dependency)
|
||||
3. [Creating a class to interface with the API](#3-creating-a-class-to-interface-with-the-api)
|
||||
4. [Checking if HuskSync is present and creating the hook](#4-checking-if-husksync-is-present-and-creating-the-hook)
|
||||
5. [Getting an instance of the API](#5-getting-an-instance-of-the-api)
|
||||
6. [CompletableFuture and Optional basics](#6-completablefuture-and-optional-basics)
|
||||
7. [Next steps](#7-next-steps)
|
||||
|
||||
## API Introduction
|
||||
### 1.1 Setup with Maven
|
||||
@@ -44,8 +55,8 @@ Add the repository to your `pom.xml` as per below. You can alternatively specify
|
||||
Add the dependency to your `pom.xml` as per below. Replace `VERSION` with the latest version of HuskSync (without the v): 
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>net.william278</groupId>
|
||||
<artifactId>husksync</artifactId>
|
||||
<groupId>net.william278.husksync</groupId>
|
||||
<artifactId>husksync-PLATFORM</artifactId>
|
||||
<version>VERSION</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
@@ -68,12 +79,12 @@ Add the dependency as per below. Replace `VERSION` with the latest version of Hu
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
compileOnly 'net.william278:husksync:VERSION'
|
||||
compileOnly 'net.william278.husksync:husksync-PLATFORM:VERSION'
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
### 2. Adding HuskSync as a dependency
|
||||
## 2. Adding HuskSync as a dependency
|
||||
- Add HuskSync to your `softdepend` (if you want to optionally use HuskSync) or `depend` (if your plugin relies on HuskSync) section in `plugin.yml` of your project.
|
||||
|
||||
```yaml
|
||||
@@ -117,9 +128,10 @@ public class MyPlugin extends JavaPlugin {
|
||||
|
||||
## 5. Getting an instance of the API
|
||||
- You can now get the API instance by calling `HuskSyncAPI#getInstance()`
|
||||
- If targeting the Bukkit platform, you can also use `BukkitHuskSyncAPI#getBukkitInstance()` to get the Bukkit-extended API instance (recommended)
|
||||
|
||||
```java
|
||||
import net.william278.husksync.api.BukkitHuskSyncAPI;
|
||||
import net.william278.husksync.api.HuskSyncAPI;
|
||||
|
||||
public class HuskSyncAPIHook {
|
||||
|
||||
@@ -135,7 +147,7 @@ public class HuskSyncAPIHook {
|
||||
## 6. CompletableFuture and Optional basics
|
||||
- HuskSync's API methods often deal with `CompletableFuture`s and `Optional`s.
|
||||
- A `CompletableFuture` is an asynchronous callback mechanism. The method will be processed asynchronously and the data returned when it has been retrieved. Then, use `CompletableFuture#thenAccept(data -> {})` to do what you want to do with the `data` you requested after it has asynchronously been retrieved, to prevent lag.
|
||||
- An `Optional` is a null-safe representation of data, or no data. You can check if the Optional is empty via `Optional#isEmpty()` (which will be returned by the API if no data could be found for the call you made). If the optional does contain data, you can get it via `Optional#get().
|
||||
- An `Optional` is a null-safe representation of data, or no data. You can check if the Optional is empty via `Optional#isEmpty()` (which will be returned by the API if no data could be found for the call you made). If the optional does contain data, you can get it via `Optional#get()`.
|
||||
|
||||
> **Warning:** You should never call `#join()` on futures returned from the HuskSyncAPI as futures are processed on server asynchronous tasks, which could lead to thread deadlock and crash your server if you attempt to lock the main thread to process them.
|
||||
|
||||
@@ -143,4 +155,4 @@ public class HuskSyncAPIHook {
|
||||
Now that you've got everything ready, you can start doing stuff with the HuskSync API!
|
||||
- [[Data Snapshot API]] — Get, edit, create & delete data snapshots and update players
|
||||
- [[Custom Data API]] — Register custom data types to sync your plugin's data with HuskSync
|
||||
- [[API Events]] — Listen to, cancel and modify the result of data synchronization events
|
||||
- [[API Events]] — Listen to, cancel and modify the result of data synchronization events
|
||||
|
||||
@@ -11,7 +11,7 @@ This page contains a table of HuskSync commands and their required permission no
|
||||
<tbody>
|
||||
<!-- /husksync command -->
|
||||
<tr>
|
||||
<td rowspan="5"><code>/husksync</code></td>
|
||||
<td rowspan="6"><code>/husksync</code></td>
|
||||
<td><code>/husksync</code></td>
|
||||
<td>View & manage plugin system information</td>
|
||||
<td><code>husksync.command.husksync</code></td>
|
||||
@@ -21,6 +21,11 @@ This page contains a table of HuskSync commands and their required permission no
|
||||
<td>View information about the plugin</td>
|
||||
<td><code>husksync.command.husksync.about</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/husksync status</code></td>
|
||||
<td>View plugin system status information</td>
|
||||
<td><code>husksync.command.husksync.status</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/husksync reload</code></td>
|
||||
<td>Reload the plugin configuration</td>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
This page contains the configuration file reference for HuskSync. The config file is located in `/plugins/HuskSync/config.yml`
|
||||
This page contains the configuration structure for HuskSync.
|
||||
|
||||
## Example config
|
||||
## Configuration structure
|
||||
📁 `plugins/HuskSync/`
|
||||
- 📄 `config.yml`: General plugin configuration
|
||||
- 📄 `server.yml`: Server ID configuration
|
||||
- 📄 `messages-xx-xx.yml`: Plugin locales, formatted in MineDown (see [[Translations]])
|
||||
|
||||
## Example files
|
||||
<details>
|
||||
<summary>config.yml</summary>
|
||||
|
||||
@@ -12,94 +18,125 @@ This page contains the configuration file reference for HuskSync. The config fil
|
||||
# ┣╸ Information: https://william278.net/project/husksync
|
||||
# ┣╸ Config Help: https://william278.net/docs/husksync/config-file/
|
||||
# ┗╸ Documentation: https://william278.net/docs/husksync
|
||||
# Locale of the default language file to use. Docs: https://william278.net/docs/huskhomes/translations
|
||||
|
||||
# Locale of the default language file to use.
|
||||
# Docs: https://william278.net/docs/husksync/translations
|
||||
language: en-gb
|
||||
# Whether to automatically check for plugin updates on startup
|
||||
check_for_updates: true
|
||||
# Specify a common ID for grouping servers running HuskSync. Don't modify this unless you know what you're doing!
|
||||
cluster_id: ''
|
||||
# Enable development debug logging
|
||||
debug_logging: false
|
||||
debug_logging: true
|
||||
# Whether to provide modern, rich TAB suggestions for commands (if available)
|
||||
brigadier_tab_completion: false
|
||||
# Whether to enable the Player Analytics hook. Docs: https://william278.net/docs/husksync/plan-hook
|
||||
# Whether to enable the Player Analytics hook.
|
||||
# Docs: https://william278.net/docs/husksync/plan-hook
|
||||
enable_plan_hook: true
|
||||
# Whether to cancel game event packets directly when handling locked players if ProtocolLib is installed
|
||||
cancel_packets: true
|
||||
# Database settings
|
||||
database:
|
||||
# Type of database to use (MYSQL, MARIADB)
|
||||
# Type of database to use (MYSQL, MARIADB, POSTGRES, MONGO)
|
||||
type: MYSQL
|
||||
# Specify credentials here for your MYSQL, MARIADB, POSTGRES OR MONGO database
|
||||
credentials:
|
||||
# Specify credentials here for your MYSQL or MARIADB database
|
||||
host: localhost
|
||||
port: 3306
|
||||
database: HuskSync
|
||||
database: minecraft
|
||||
username: root
|
||||
password: pa55w0rd
|
||||
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:
|
||||
# MYSQL / MARIADB database Hikari connection pool properties. Don't modify this unless you know what you're doing!
|
||||
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
|
||||
# Redis settings
|
||||
redis:
|
||||
# Specify the credentials of your Redis database here. Set "password" to '' if you don't have one
|
||||
credentials:
|
||||
# Specify the credentials of your Redis database here. Set "password" to '' if you don't have one
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: ''
|
||||
use_ssl: false
|
||||
use_ssl: false
|
||||
# Options for if you're using Redis sentinel. Don't modify this unless you know what you're doing!
|
||||
sentinel:
|
||||
# The master set name for the Redis sentinel.
|
||||
master: ''
|
||||
# List of host:port pairs
|
||||
nodes: []
|
||||
password: ''
|
||||
# Redis settings
|
||||
synchronization:
|
||||
# The data synchronization mode to use (LOCKSTEP or DELAY). LOCKSTEP is recommended for most networks.
|
||||
# Docs: https://william278.net/docs/husksync/sync-modes
|
||||
mode: LOCKSTEP
|
||||
# The number of data snapshot backups that should be kept at once per user
|
||||
max_user_data_snapshots: 16
|
||||
# Number of hours between new snapshots being saved as backups (Use "0" to backup all snapshots)
|
||||
snapshot_backup_frequency: 4
|
||||
# List of save cause IDs for which a snapshot will be automatically pinned (so it won't be rotated). Docs: https://william278.net/docs/husksync/data-rotation#save-causes
|
||||
# List of save cause IDs for which a snapshot will be automatically pinned (so it won't be rotated).
|
||||
# Docs: https://william278.net/docs/husksync/data-rotation#save-causes
|
||||
auto_pinned_save_causes:
|
||||
- INVENTORY_COMMAND
|
||||
- ENDERCHEST_COMMAND
|
||||
- BACKUP_RESTORE
|
||||
- CONVERTED_FROM_V2
|
||||
- LEGACY_MIGRATION
|
||||
- MPDB_MIGRATION
|
||||
# Whether to create a snapshot for users on a world when the server saves that world
|
||||
save_on_world_save: true
|
||||
# Whether to create a snapshot for users when they die (containing their death drops)
|
||||
save_on_death: false
|
||||
# Whether to save empty death drops for users when they die
|
||||
save_empty_drops_on_death: true
|
||||
# Configuration for how and when to sync player data when they die
|
||||
save_on_death:
|
||||
# Whether to create a snapshot for users when they die (containing their death drops)
|
||||
enabled: true
|
||||
# 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.
|
||||
items_to_save: ITEMS_TO_KEEP
|
||||
# Should a death snapshot still be created even if the items to save on the player's death are empty?
|
||||
save_empty_items: true
|
||||
# Whether dead players who log out and log in to a different server should have their items saved.
|
||||
sync_dead_players_changing_server: true
|
||||
# Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing
|
||||
compress_data: true
|
||||
# Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)
|
||||
notification_display_slot: ACTION_BAR
|
||||
# (Experimental) Persist Cartography Table locked maps 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
|
||||
# Whether to synchronize player max health (requires health syncing to be enabled)
|
||||
synchronize_max_health: true
|
||||
# Whether dead players who log out and log in to a different server should have their items saved. You may need to modify this if you're using the keepInventory gamerule.
|
||||
synchronize_dead_players_changing_server: true
|
||||
# How long, in milliseconds, this server should wait for a response from the redis server before pulling data from the database instead (i.e., if the user did not change servers).
|
||||
# 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).
|
||||
network_latency_milliseconds: 500
|
||||
# Which data types to synchronize (Docs: https://william278.net/docs/husksync/sync-features)
|
||||
# Which data types to synchronize.
|
||||
# Docs: https://william278.net/docs/husksync/sync-features
|
||||
features:
|
||||
hunger: true
|
||||
persistent_data: false
|
||||
inventory: true
|
||||
game_mode: true
|
||||
advancements: true
|
||||
experience: true
|
||||
ender_chest: true
|
||||
experience: true
|
||||
advancements: true
|
||||
game_mode: true
|
||||
flight_status: true
|
||||
potion_effects: true
|
||||
location: false
|
||||
statistics: true
|
||||
health: true
|
||||
hunger: true
|
||||
attributes: true
|
||||
persistent_data: true
|
||||
location: false
|
||||
# Commands which should be blocked before a player has finished syncing (Use * to block all commands)
|
||||
blacklisted_commands_while_locked:
|
||||
- '*'
|
||||
# For attribute syncing, which attributes should be ignored/skipped when syncing
|
||||
# (e.g. ['minecraft:generic.max_health', 'minecraft:generic.attack_damage'])
|
||||
ignored_attributes: []
|
||||
# Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts
|
||||
event_priorities:
|
||||
quit_listener: LOWEST
|
||||
@@ -109,5 +146,18 @@ synchronization:
|
||||
|
||||
</details>
|
||||
|
||||
## Messages files
|
||||
You can customize the plugin locales, too, by editing your `messages-xx-xx.yml` file. This file is formatted using [MineDown syntax](https://github.com/Phoenix616/MineDown). For more information, see [[Translations]].
|
||||
<details>
|
||||
<summary>server.yml</summary>
|
||||
|
||||
```yaml
|
||||
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
# ┃ HuskSync - Server ID ┃
|
||||
# ┃ Developed by William278 ┃
|
||||
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
# ┣╸ This file should contain the ID of this server as defined in your proxy config.
|
||||
# ┗╸ If you join it using /server alpha, then set it to 'alpha' (case-sensitive)
|
||||
|
||||
name: beta
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -1,15 +1,18 @@
|
||||
HuskSync allows you to save and synchronize custom data through the existing versatile DataSnapshot format. This page assumes you've read the [[API]] introduction and are familiar with the aforementioned [[Data Snapshot API]].
|
||||
HuskSync allows you to save and synchronize custom data through the existing versatile DataSnapshot format. This page assumes you've read the [[API]] introduction and are familiar with the aforementioned [[Data Snapshot API]]. This page discusses API implementations that target the Bukkit platform.
|
||||
|
||||
To do this, you create and register an implementation of a platform `Data` class (e.g., `BukkitData`) and a corresponding `Serializer` class (e.g., `BukkitSerializer`). You can then apply your custom data type to a user using the `OnlineUser#setData(Identifier, Data)` method.
|
||||
|
||||
> **Note:** Before you begin, consider if this is what you'd like to do. For simpler/smaller data sync tasks you may wish to consider using the PersistentDataContainer API format instead, which is a bit more portable if you decide to exit the HuskSync ecosystem.
|
||||
|
||||
If you'd like to have a look at an example of a data extension for HuskSync that provides serializers for Pixelmon data when running the plugin on Arclight, check out [PokeSync by GsTio86](https://github.com/GsTio86/PokeSync)!
|
||||
|
||||
## Table of Contents
|
||||
1. [Extending the BukkitData Class](#1-extending-the-bukkitdata-class)
|
||||
1. [Implementing Adaptable](#11-implementing-adaptable)
|
||||
2. [Extending the BukkitSerializer Class](#2-extending-the-bukkitserializer-class)
|
||||
3. [Identifiers and registering our Serializer](#3-identifiers--registering-our-serializer)
|
||||
4. [Setting and Getting our Data to/from a User](#4-setting-and-getting-our-data-tofrom-a-user)
|
||||
1. [Persisting custom data on the DataSaveEvent](#41-persisting-custom-data-on-the-datasaveevent)
|
||||
|
||||
## 1. Extending the BukkitData Class
|
||||
* HuskSync provides a `Data` interface that you must implement that will represent your custom data.
|
||||
@@ -128,4 +131,14 @@ huskSyncAPI.getUser(player).setData(LOGIN_PARTICLES_ID, loginParticleData);
|
||||
|
||||
// Get our data from a player
|
||||
LoginParticleData loginParticleData = (LoginParticleData) huskSyncAPI.getUser(player).getData(LOGIN_PARTICLES_ID);
|
||||
```
|
||||
```
|
||||
|
||||
### 4.1 Persisting custom data on the DataSaveEvent
|
||||
Add an EventListener to the `DataSaveEvent` and use the `#editData` consumer method to apply custom data during standard DataSaves. This will persist data to users any time the data save routine executes (on user logout, server shutdownm, world save, etc).
|
||||
|
||||
```java
|
||||
@EventHandler
|
||||
public void onDataSave(BukkitDataSaveEvent event) {
|
||||
event.editData((unpacked) -> unpacked.setData(LOGIN_PARTICLES_ID, new LoginParticleData("FIREWORKS_SPARK", 10)));
|
||||
}
|
||||
```
|
||||
|
||||
@@ -164,8 +164,10 @@ huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> {
|
||||
| `husksync:statistics` | User statistics | `#getStatistics` | `#setStatistics` |
|
||||
| `husksync:health` | User health | `#getHealth` | `#setHealth` |
|
||||
| `husksync:hunger` | User hunger, saturation & exhaustion | `#getHunger` | `#setHunger` |
|
||||
| `husksync:attributes` | User attributes | `#getAttributes` | `#setAttributes` |
|
||||
| `husksync:experience` | User level, experience, and score | `#getExperience` | `#setExperience` |
|
||||
| `husksync:game_mode` | User game mode and flight status | `#getGameMode` | `#setGameMode` |
|
||||
| `husksync:game_mode` | User game mode | `#getGameMode` | `#setGameMode` |
|
||||
| `husksync:flight_status` | User ability to fly/if flying now | `#getFlightStatus` | `#setFlightStatus` |
|
||||
| `husksync:persistent_data` | User persistent data container | `#getPersistentData` | `#setPersistentData` |
|
||||
| Custom types; `plugin:foo` | Any custom data | `#getData(Identifer)` | `#setData(Identifier)` |
|
||||
|
||||
@@ -173,8 +175,8 @@ huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> {
|
||||
* You can only get data from snapshots where a serializer has been registered for it on this server and, in the case of the built-in data types, where the sync feature has been enabled in the [[Config File]]. If you try to get data from a snapshot where the data type is not supported, you will get an empty `Optional`.
|
||||
|
||||
### 4.2 Editing Health, Hunger, Experience, and GameMode data
|
||||
* `DataSnapshot.Unpacked#getHealth()` returns an `Optional<Data.Health>`, which you can then use to get the player's current and max health.
|
||||
* `DataSnapshot.Unpacked#setHealth(Data.Health)` sets the player's current and max health. You can create a `Health` instance to pass on the Bukkit platform through `BukkitData.Health.from(double, double, double)`.
|
||||
* `DataSnapshot.Unpacked#getHealth()` returns an `Optional<Data.Health>`, which you can then use to get the player's current health.
|
||||
* `DataSnapshot.Unpacked#setHealth(Data.Health)` sets the player's current health. You can create a `Health` instance to pass on the Bukkit platform through `BukkitData.Health.from(double, double)`.
|
||||
* Similar methods exist for Hunger, Experience, and GameMode data types
|
||||
* Once you've updated the data in the snapshot, you can save it to the database using `HuskSyncAPI#setCurrentData(user, userData)`.
|
||||
|
||||
@@ -201,21 +203,30 @@ huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> {
|
||||
System.out.println("User has no game mode data!");
|
||||
return;
|
||||
}
|
||||
Optional<Data.FlightStatus> flightStatusOptional = snapshot.getFlightStatus();
|
||||
if (flightStatusOptional.isEmpty()) {
|
||||
System.out.println("User has no flight status data!");
|
||||
return;
|
||||
}
|
||||
// getExperience() and getHunger() work similarly
|
||||
|
||||
// Get the health data
|
||||
Data.Health health = healthOptional.get();
|
||||
double currentHealth = health.getCurrentHealth(); // Current health
|
||||
double maxHealth = health.getMaxHealth(); // Max health
|
||||
double healthScale = health.getHealthScale(); // Health scale (e.g., 20 for 20 hearts)
|
||||
snapshot.setHealth(BukkitData.Health.from(20, 20, 20));
|
||||
snapshot.setHealth(BukkitData.Health.from(20, 20));
|
||||
// Need max health? Look at the Attributes data type.
|
||||
|
||||
// Get the game mode data
|
||||
Data.GameMode gameMode = gameModeOptional.get();
|
||||
String gameModeName = gameMode.getGameModeName(); // Game mode name (e.g., "SURVIVAL")
|
||||
boolean isFlying = gameMode.isFlying(); // Whether the player is *currently* flying
|
||||
boolean canFly = gameMode.canFly(); // Whether the player *can* fly
|
||||
snapshot.setGameMode(BukkitData.GameMode.from("SURVIVAL", false, false));
|
||||
snapshot.setGameMode(BukkitData.GameMode.from("SURVIVAL"));
|
||||
|
||||
// Get flight data
|
||||
Data.FlightStatus flightStatus = flightStatusOptional.get(); // Whether the player is flying
|
||||
boolean isFlying = flightStatus.isFlying(); // Whether the player is *currently* flying
|
||||
boolean canFly = flightStatus.isAllowFlight(); // Whether the player *can* fly
|
||||
snapshot.setFlightStatus(BukkitData.FlightStatus.from(false, false));
|
||||
|
||||
// Save the snapshot - This will update the player if online and save the snapshot to the database
|
||||
huskSyncAPI.setCurrentData(user, snapshot);
|
||||
@@ -245,11 +256,8 @@ huskSyncAPI.editCurrentData(user, snapshot -> {
|
||||
// Get the player's current health
|
||||
double currentHealth = health.getCurrentHealth();
|
||||
|
||||
// Get the player's max health
|
||||
double maxHealth = health.getMaxHealth();
|
||||
|
||||
// Set the player's health
|
||||
snapshot.setHealth(BukkitData.Health.from(20, 20, 20));
|
||||
// Set the player's health / health scale
|
||||
snapshot.setHealth(BukkitData.Health.from(20, 20));
|
||||
});
|
||||
```
|
||||
</details>
|
||||
@@ -307,7 +315,7 @@ huskSyncAPI.editCurrentInventory(user, inventory -> {
|
||||
// Get the player's inventory contents
|
||||
ItemStack[] inventoryContents = ((BukkitData.Items.Inventory) inventory).getContents();
|
||||
|
||||
// The array of ItemStacks is a copy of the player's inventory contents (in 1.20.1, this is an array of length 42)
|
||||
// The array of ItemStacks is a copy of the player's inventory contents (Typically an array of length 42)
|
||||
inventoryContents[0] = new ItemStack(Material.DIAMOND_SWORD);
|
||||
inventoryContents[1] = null; // null = an empty slot
|
||||
|
||||
|
||||
@@ -23,7 +23,8 @@ The name of the generated .json file will match the following format: `<username
|
||||
"pinned": false,
|
||||
"timestamp": "2023-09-15T17:27:08.6768038+01:00",
|
||||
"save_cause": "DISCONNECT",
|
||||
"minecraft_version": "1.20.1",
|
||||
"server": "alpha",
|
||||
"minecraft_version": "1.20.2",
|
||||
"platform_type": "bukkit",
|
||||
"format_version": 4,
|
||||
"data": {
|
||||
|
||||
33
docs/FAQs.md
33
docs/FAQs.md
@@ -12,14 +12,16 @@ HuskSync supports synchronising a wide range of different data elements, each of
|
||||
<details>
|
||||
<summary> <b>Are modded items supported?</b></summary>
|
||||
|
||||
Modded items are not supported.
|
||||
If you're running HuskSync on Arclight or similar, please note we will not be able to provide you with support, but have been reported to save & sync correctly with HuskSync v3.x+.
|
||||
|
||||
**TL;DR** — modded items may work, but since we can't guarantee compatibility, we do not officially mark them as supported. Be sure to test thoroughly before deploying on production!
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> <b>Are MMOItems / SlimeFun / ItemsAdder items supported?</b></summary>
|
||||
|
||||
These plugins, which provide custom items, should be supported as of HuskSync v3.x; but do note we cannot guarantee compatibility with all methods of injecting custom data to create custom items. Be sure to test thoroughly before deploying on production!
|
||||
These plugins, which provide custom items, should be supported as of HuskSync v3.x+; but do note we cannot guarantee compatibility with all methods of injecting custom data to create custom items. Be sure to test thoroughly before deploying on production!
|
||||
|
||||
</details>
|
||||
|
||||
@@ -33,9 +35,7 @@ HuskSync requires Redis to operate (for reasons demonstrated below). Redis is an
|
||||
<details>
|
||||
<summary> <b>How does the plugin synchronize data?</b></summary>
|
||||
|
||||

|
||||
|
||||
HuskSync makes use of both MySQL and Redis for optimal data synchronization.
|
||||
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`)
|
||||
|
||||
When a user changes servers, in addition to data being saved to MySQL, it is also cached via the Redis server with a temporary expiry key. When changing servers, the receiving server detects the key and sets the user data from Redis. When a player rejoins the network, the system fetches the last-saved data snapshot from the MySQL Database.
|
||||
|
||||
@@ -48,23 +48,26 @@ This approach is able to dramatically improve both synchronization performance a
|
||||
|
||||
This is a very common request, but there's a good reason why HuskSync does not support this.
|
||||
|
||||
The Vault API is designed to be a central "Vault" for storing user data. It's the role of economy plugins that *implement* vault to handle the data storage -- and, by extension, synchronization cross-server. Plugins that *hook into* Vault then expect to be able to use the Vault API to get the player's latest economy balance and data.
|
||||
Vault is a plugin that provides a common API for developers to do two things:
|
||||
|
||||
Plugins such as MySQLPlayerDataBridge that support synchronizing Vault *hook into* Vault and as a result can violate this expectation—plugins that expect Vault to return the latest user data no longer can. As a result, plugins like MySQLPlayerDataBridge have to provide lots of manual hooks and tweaks for individual plugins to ensure compatibility.
|
||||
1. Developers can _implement_ Vault to create economy plugins
|
||||
2. Developers can _target_ Vault to modify and check economy balances without having to write code to hook into individual economy plugins
|
||||
|
||||
This causes all sorts of compatibility issues with unsupported plugins and increases plugin size and update workload.
|
||||
In essence, Vault is beneficial as it allows developers to write less code. A developer only needs to write code that targets the Vault API when you need to do stuff with player economy balances.
|
||||
|
||||
As a result, I recommend using an economy plugin (that directly *implements* the Vault API), that works cross-server. XConomy is a popular choice for this, which I have personally had a good experience with in the past.
|
||||
_Vault itself, however, is not an Economy plugin_. The developers of Economy plugins that _implement_ are responsible for writing the implementation code and database systems for creating player economy accounts and updating balances. By extension, this also means it is the responsibility of Economy plugin developers to implement Vault's API in a way that allows that data to be synchronized cross-server; Vault itself does not contain API for doing so.
|
||||
|
||||
Most Economy plugins do not support doing this, however, as cross-server support isn't (and historically hasn't) been a priority. _MySQLPlayerDataBridge_ allows you to workaround this and synchronize Vault balances — but as detailed above, since Vault itself is not an economy plugin, the way this works is MySQLPlayerDataBridge has to provide and continually maintain a bespoke laundry list of manual, individual hooks and tweaks for both Economy plugins that _implement_ Vault and other plugins that _target_ Vault.
|
||||
|
||||
Implementing a similar system in HuskSync would considerably increase the size of the codebase, lengthen update times, and decrease overall system stability. The much better solution is to use an Economy plugin that _implements_ Vault in a way that works cross-server.
|
||||
|
||||
Indeed, there exist economy plugins — such as [XConomy](https://github.com/YiC200333/XConomy) and [RedisEconomy](https://github.com/Emibergo02/RedisEconomy) which do just this, and this is my recommended solution. Need to move from an incompatible Economy plugin? Vault provides methods for transferring balances between Economy plugins (`/vault-convert`).
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> <b>Is this better than MySQLPlayerDataBridge?</b></summary>
|
||||
|
||||
I can't provide a fair answer to this question! What I can say is that your mileage may 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.
|
||||
I can't provide a fair answer to this question! What I can say is that your mileage will of course vary.
|
||||
|
||||
With that said, servers running plugins or mods that make use of custom items (such as MMOItems, SlimeFun) are not supported by HuskSync and so MySQLPlayerDataBridge may be a better choice for you.
|
||||
|
||||
A migrator from MPDB is built-in to HuskSync.
|
||||
|
||||
</details>
|
||||
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>
|
||||
|
||||
@@ -14,6 +14,7 @@ Welcome! This is the plugin documentation for HuskSync v3.x+. Please click throu
|
||||
## Documentation
|
||||
* 🖥️ [[Commands]]
|
||||
* ✅ [[Sync Features]]
|
||||
* ⚙️ [[Sync Modes]]
|
||||
* 🟩 [[Plan Hook]]
|
||||
* ☂️ [[Dumping UserData]]
|
||||
* 📋 [[Event Priorities]]
|
||||
|
||||
@@ -8,22 +8,26 @@ HuskSync has some special handling when players die, to account for scenarios wh
|
||||
* **Snapshot creation on death**—HuskSync can create a special snapshot for backup purposes when a player dies, formed by taking their drops and setting this to their inventory. When `keepInventory` is enabled, the player drops are empty, so this creates an inaccurate snapshot. This option is disabled by default.
|
||||
|
||||
## How can this be fixed?
|
||||
You will need to set the `synchronization.save_on_death` (which controls making snapshots on death), `save_empty_drops_on_death` (which controls whether snapshots of players who have no items to drop should be created), and `synchronization.synchronize_dead_players_changing_server` (which controls whether to sync dead players when they change servers) options to `false` in `config.yml`.
|
||||
You should change the `items_to_save` mode to `ITEMS_TO_KEEP` instead of drops. Also, ensure `save_empty_items` and `sync_dead_players_changing_server` are enabled.
|
||||
|
||||
<details>
|
||||
<summary>Example in config.yml</summary>
|
||||
|
||||
```yml
|
||||
synchronization:
|
||||
# ...
|
||||
save_on_death: false # <-- Set this to false
|
||||
save_empty_drops_on_death: false # <-- Set this to false
|
||||
# ...
|
||||
synchronize_dead_players_changing_server: false # <-- Set this to false
|
||||
```
|
||||
|
||||
</details>
|
||||
<summary>Example in config.yml</summary>
|
||||
|
||||
```yml
|
||||
synchronization:
|
||||
#...
|
||||
save_on_death:
|
||||
# Whether to create a snapshot for users when they die (containing their death drops)
|
||||
enabled: true
|
||||
# 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
|
||||
items_to_save: ITEMS_TO_KEEP
|
||||
# Should a death snapshot still be created even if the items to save on the player's death are empty?
|
||||
save_empty_items: true
|
||||
# Whether dead players who log out and log in to a different server should have their items saved.
|
||||
sync_dead_players_changing_server: true
|
||||
#...
|
||||
```
|
||||
</details>
|
||||
|
||||
## Troubleshooting with custom keepInventory setups
|
||||
If the above doesn't work for you, you may need to do more things to get this to work properly.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
This guide will walk you through how to upgrade from HuskSync v1.4.x to HuskSync v2.x or v3.x. Data from HuskSync v2.x will automatically be imported into HuskSync v3.x.
|
||||
This guide will walk you through how to upgrade from HuskSync v1.4.x to HuskSync v3.x. Data from HuskSync v2.x will automatically be imported into HuskSync v3.x.
|
||||
|
||||
## Requirements
|
||||
- MySQL Database with HuskSync v1.4.x data
|
||||
@@ -9,15 +9,15 @@ This guide will walk you through how to upgrade from HuskSync v1.4.x to HuskSync
|
||||
### 1. Uninstall HuskSync v1.x from all servers
|
||||
- Switch off all servers and your proxy
|
||||
- Delete the .jar file from your `~/plugins/` folders on your Spigot servers
|
||||
- Also delete the .jar file from your `~/plugins/` folders on your Proxy. HuskSync v2.x no longer requires a proxy plugin.
|
||||
- Delete (or make a copy and delete) all HuskSync config data folders (`~/plugins/HuskSync/`). HuskSync 2.x has a new config and messages file.
|
||||
- Also delete the .jar file from your `~/plugins/` folders on your Proxy. HuskSync v3.x no longer requires a proxy plugin.
|
||||
- Delete (or make a copy and delete) all HuskSync config data folders (`~/plugins/HuskSync/`). HuskSync v3.x has new `config.yml`, `messages-xx-xx.yml` and `server.yml` files.
|
||||
|
||||
### 2. Install HuskSync v2.x on all Spigot servers
|
||||
- HuskSync v2.x must only be installed on your Spigot servers, not your proxy.
|
||||
### 2. Install HuskSync v3.x on all Spigot servers
|
||||
- HuskSync v3.x must only be installed on your Spigot servers, not your proxy.
|
||||
- Follow the setup instructions [here](Setup).
|
||||
|
||||
### 3. Configure the migrator
|
||||
- With your servers back on and correctly configured to run HuskSync v2.x, ensure nobody is online.
|
||||
- With your servers back on and correctly configured to run HuskSync v3.x, ensure nobody is online.
|
||||
- Use the console on one of your Spigot servers to enter: `husksync migrate legacy`
|
||||
- Carefully read the migration configuration instructions. In most cases, you won't have to change the settings, but if you do need to adjust them, use `husksync migrate legacy set <setting> <value>`.
|
||||
- Migration will be carried out *from* the database you specify with the settings in console *to* the database configured in `config.yml`. If you're migrating from multiple clusters, ensure you run the migrator on the correct servers corresponding to the migrator.
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
This guide will walk you through how to migrate from MySQLPlayerDataBridge (MPDB) to HuskSync v2.x.
|
||||
This guide will walk you through how to migrate from MySQLPlayerDataBridge (MPDB) to HuskSync v3.x.
|
||||
|
||||
> **Warning:** Please note that due to MPDB changes, HuskSync only supports migrating from MySQLPlayerDataBridge `<= v4.9.2`. Support for newer versions will be added in the future.
|
||||
|
||||
## Requirements
|
||||
- Spigot servers with MySQLPlayerDataBridge *still installed*
|
||||
|
||||
## Migration Instructions
|
||||
### 1. Install HuskSync v2.x on all Spigot servers
|
||||
### 1. Install HuskSync v3.x on all Spigot servers
|
||||
- Download, then install HuskSync on all your servers. Don't uninstall MySQLPlayerDataBridge yet.
|
||||
- Follow the setup instructions [here](Setup).
|
||||
- Follow the setup instructions [here](setup).
|
||||
- Start your servers again when done.
|
||||
|
||||
### 2. Configure the migrator
|
||||
- With your servers back on and correctly configured to run HuskSync v2.x, ensure nobody is online.
|
||||
- With your servers back on and correctly configured to run HuskSync v3.x, ensure nobody is online.
|
||||
- Use the console on one of your Spigot servers to enter: `husksync migrate mpdb`. If the MPDB migrator is not available, ensure MySQLPlayerDataBridge is still installed.
|
||||
- Adjust the migration setting as needed using the following command: `husksync migrate mpdb set <setting> <value>`.
|
||||
- Note that migration will be carried out *from* the database you specify with the settings in console *to* the database configured in `config.yml`.
|
||||
@@ -25,4 +27,4 @@ This guide will walk you through how to migrate from MySQLPlayerDataBridge (MPDB
|
||||
|
||||
### 5. Ensure the migration was successful
|
||||
- Verify that the migration was successful by logging in and using the `/userdata list <username>` command to see if the data was imported with the `mpdb_migration` saveCause.
|
||||
- You can delete the old tables in the database if you want. Be careful to make sure you delete the correct ones.
|
||||
- You can delete the old tables in the database if you want. Be careful to make sure you delete the correct ones.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user