mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-19 14:59:21 +00:00
Compare commits
131 Commits
refactor/l
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0772f09e98 | ||
|
|
e916673454 | ||
|
|
ac163d5130 | ||
|
|
d656b67570 | ||
|
|
3c66b65ac6 | ||
|
|
c227933b3b | ||
|
|
5cf9cb8e50 | ||
|
|
693cd6120f | ||
|
|
6efd800481 | ||
|
|
a723a7cba3 | ||
|
|
b1a5eb5f44 | ||
|
|
8232282d13 | ||
|
|
404d359f89 | ||
|
|
62e84d92fc | ||
|
|
9b2246eac2 | ||
|
|
e6d3935246 | ||
|
|
5c4111b6a7 | ||
|
|
51a700600a | ||
|
|
23c3ee08e9 | ||
|
|
c1d08f9c23 | ||
|
|
4cabdbe952 | ||
|
|
abdebd960b | ||
|
|
a7aea51a45 | ||
|
|
c8a4376208 | ||
|
|
315cd4ba6b | ||
|
|
6607ac5a6e | ||
|
|
562939498a | ||
|
|
e686d43ca8 | ||
|
|
e9ac400215 | ||
|
|
234870537a | ||
|
|
b5f392a20f | ||
|
|
9ea8eb4101 | ||
|
|
dc7cde1c33 | ||
|
|
4e75b5ca1d | ||
|
|
fe0bdccf40 | ||
|
|
c615ab592b | ||
|
|
16d4a8fd9b | ||
|
|
96f34092f6 | ||
|
|
a31c3c48f7 | ||
|
|
0e96374a03 | ||
|
|
24545563fa | ||
|
|
a9ea4d34e5 | ||
|
|
11453393d4 | ||
|
|
1d850a9ddb | ||
|
|
5b90b3d006 | ||
|
|
d85ec65384 | ||
|
|
9d681db030 | ||
|
|
c5c2dde0bf | ||
|
|
807bffe9aa | ||
|
|
a1956c6822 | ||
|
|
321dccb0b5 | ||
|
|
1015c50802 | ||
|
|
c5e759390b | ||
|
|
883695b0b0 | ||
|
|
879aef471a | ||
|
|
64c81a9a5a | ||
|
|
b61a9a7bc3 | ||
|
|
3f725eb40c | ||
|
|
8f1e4a5198 | ||
|
|
3875447430 | ||
|
|
dce84f285d | ||
|
|
1314683eea | ||
|
|
6b1f89aab0 | ||
|
|
27e958a474 | ||
|
|
39ebd0dc4f | ||
|
|
2ada0497ec | ||
|
|
e9f2856040 | ||
|
|
6050c584c0 | ||
|
|
7ebf91bfae | ||
|
|
2d7799628a | ||
|
|
1627de732b | ||
|
|
fea882c642 | ||
|
|
8b749357f7 | ||
|
|
e4ff7e6d6c | ||
|
|
396630821f | ||
|
|
70f65d126b | ||
|
|
e8925a0d79 | ||
|
|
2a3cf9be7d | ||
|
|
971d3f5167 | ||
|
|
99da65a4d8 | ||
|
|
25744b4ef7 | ||
|
|
8f2d1c7298 | ||
|
|
a9aa93a682 | ||
|
|
ef340840ab | ||
|
|
cf08015961 | ||
|
|
cb09e0cfb2 | ||
|
|
554fac89c0 | ||
|
|
215bed9908 | ||
|
|
935aafa74a | ||
|
|
c51ba85f38 | ||
|
|
6a67d1bbe0 | ||
|
|
20bc76a768 | ||
|
|
6928f97dff | ||
|
|
06742fb848 | ||
|
|
759983b000 | ||
|
|
5556e3b6ce | ||
|
|
bcffcb1f64 | ||
|
|
fa77e6e418 | ||
|
|
c8aa29c82f | ||
|
|
51cf982359 | ||
|
|
f6d860335f | ||
|
|
5cea4665a1 | ||
|
|
34b183a35e | ||
|
|
61298c24bb | ||
|
|
af9d32895e | ||
|
|
52fa67432c | ||
|
|
404f18d81f | ||
|
|
9ee8ea1c84 | ||
|
|
64f845e293 | ||
|
|
30d1acc67e | ||
|
|
8d047d8892 | ||
|
|
cb49ab8d73 | ||
|
|
436e85dada | ||
|
|
223333882d | ||
|
|
06d8dda7dd | ||
|
|
805ffb19c2 | ||
|
|
cd3e4ef063 | ||
|
|
557b738511 | ||
|
|
8ee6b7a199 | ||
|
|
dc880bc37f | ||
|
|
c419587933 | ||
|
|
afb4fdd5d5 | ||
|
|
bf8474e02d | ||
|
|
937ea9bc8e | ||
|
|
ef7b3c4f32 | ||
|
|
370712c5b2 | ||
|
|
ae657acee3 | ||
|
|
34dc6a537d | ||
|
|
e99ba66271 | ||
|
|
0111f25865 | ||
|
|
2a59a0b3f5 |
36
.github/workflows/ci.yml
vendored
36
.github/workflows/ci.yml
vendored
@@ -17,9 +17,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Set up JDK 21 📦'
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v5
|
||||
uses: mikepenz/action-junit-report@v6
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
@@ -51,30 +51,46 @@ jobs:
|
||||
version: ${{ env.version_name }}
|
||||
changelog: ${{ github.event.head_commit.message }}
|
||||
distro-names: |
|
||||
paper-1.20.1
|
||||
paper-1.21.1
|
||||
paper-1.21.4
|
||||
fabric-1.20.1
|
||||
paper-1.21.5
|
||||
paper-1.21.8
|
||||
paper-1.21.10
|
||||
paper-1.21.11
|
||||
fabric-1.21.1
|
||||
fabric-1.21.4
|
||||
fabric-1.21.5
|
||||
fabric-1.21.8
|
||||
distro-groups: |
|
||||
paper
|
||||
paper
|
||||
paper
|
||||
paper
|
||||
paper
|
||||
paper
|
||||
fabric
|
||||
fabric
|
||||
fabric
|
||||
fabric
|
||||
distro-descriptions: |
|
||||
Paper 1.20.1
|
||||
Paper 1.21.1
|
||||
Paper 1.21.4
|
||||
Fabric 1.20.1
|
||||
Paper 1.21.5
|
||||
Paper 1.21.8
|
||||
Paper 1.21.10
|
||||
Paper 1.21.11
|
||||
Fabric 1.21.1
|
||||
Fabric 1.21.4
|
||||
Fabric 1.21.5
|
||||
Fabric 1.21.8
|
||||
files: |
|
||||
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.20.1.jar
|
||||
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.1.jar
|
||||
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.4.jar
|
||||
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.20.1.jar
|
||||
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.5.jar
|
||||
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.8.jar
|
||||
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.10.jar
|
||||
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.11.jar
|
||||
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.1.jar
|
||||
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.4.jar
|
||||
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.4.jar
|
||||
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.5.jar
|
||||
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.8.jar
|
||||
6
.github/workflows/pr_tests.yml
vendored
6
.github/workflows/pr_tests.yml
vendored
@@ -13,9 +13,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout for CI 🛎'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Set up JDK 21 📦'
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
with:
|
||||
arguments: test
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v5
|
||||
uses: mikepenz/action-junit-report@v6
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
32
.github/workflows/release.yml
vendored
32
.github/workflows/release.yml
vendored
@@ -13,9 +13,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Set up JDK 21 📦'
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
RELEASES_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: 'Publish Test Report 📊'
|
||||
uses: mikepenz/action-junit-report@v5
|
||||
uses: mikepenz/action-junit-report@v6
|
||||
if: success() || failure() # Continue on failure
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
@@ -40,30 +40,42 @@ jobs:
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
changelog: ${{ github.event.release.body }}
|
||||
distro-names: |
|
||||
paper-1.20.1
|
||||
paper-1.21.1
|
||||
paper-1.21.4
|
||||
fabric-1.20.1
|
||||
paper-1.21.5
|
||||
paper-1.21.8
|
||||
paper-1.21.10
|
||||
fabric-1.21.1
|
||||
fabric-1.21.4
|
||||
fabric-1.21.5
|
||||
fabric-1.21.8
|
||||
distro-groups: |
|
||||
paper
|
||||
paper
|
||||
paper
|
||||
paper
|
||||
paper
|
||||
fabric
|
||||
fabric
|
||||
fabric
|
||||
fabric
|
||||
distro-descriptions: |
|
||||
Paper 1.20.1
|
||||
Paper 1.21.1
|
||||
Paper 1.21.4
|
||||
Fabric 1.20.1
|
||||
Paper 1.21.5
|
||||
Paper 1.21.8
|
||||
Paper 1.21.10
|
||||
Fabric 1.21.1
|
||||
Fabric 1.21.4
|
||||
Fabric 1.21.5
|
||||
Fabric 1.21.8
|
||||
files: |
|
||||
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.20.1.jar
|
||||
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.1.jar
|
||||
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.4.jar
|
||||
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.20.1.jar
|
||||
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.5.jar
|
||||
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.8.jar
|
||||
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.10.jar
|
||||
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.1.jar
|
||||
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.4.jar
|
||||
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.4.jar
|
||||
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.5.jar
|
||||
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.8.jar
|
||||
4
.github/workflows/update_docs.yml
vendored
4
.github/workflows/update_docs.yml
vendored
@@ -18,8 +18,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Push Docs to Github Wiki 📄️'
|
||||
uses: Andrew-Chen-Wang/github-wiki-action@v4
|
||||
uses: Andrew-Chen-Wang/github-wiki-action@v5
|
||||
with:
|
||||
path: 'docs'
|
||||
24
README.md
24
README.md
@@ -46,16 +46,20 @@
|
||||
## Compatibility
|
||||
HuskSync supports the following [compatible versions](https://william278.net/docs/husksync/compatibility) of Minecraft. Since v3.7, you must download the correct version of HuskSync for your server:
|
||||
|
||||
| Minecraft | Latest HuskSync | Java Version | Platforms | Support Status |
|
||||
|:---------------:|:---------------:|:------------:|:--------------|:-----------------------------|
|
||||
| 1.21.4 | _latest_ | 21 | Paper, Fabric | ✅ **Active Release** |
|
||||
| 1.21.3 | 3.7.1 | 21 | Paper, Fabric | 🗃️ Archived (December 2024) |
|
||||
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **November 2025** (LTS) |
|
||||
| 1.20.6 | 3.6.8 | 17 | Paper | 🗃️ Archived (October 2024) |
|
||||
| 1.20.4 | 3.6.8 | 17 | Paper | 🗃️ Archived (July 2024) |
|
||||
| 1.20.1 | _latest_ | 17 | Paper, Fabric | ✅ **November 2025** (LTS) |
|
||||
| 1.17.1 - 1.19.4 | 3.6.8 | 17 | Paper | 🗃️ Archived |
|
||||
| 1.16.5 | 3.2.1 | 16 | Paper | 🗃️ Archived |
|
||||
| Minecraft | Latest HuskSync | Java Version | Platforms | Support Status |
|
||||
|:---------------:|:---------------:|:------------:|:--------------|:------------------------------|
|
||||
| 1.21.10 | _latest_ | 21 | Paper | ✅ **Active Release** |
|
||||
| 1.21.7/8 | _latest_ | 21 | Paper, Fabric | ✅ **August 2026** |
|
||||
| 1.21.6 | 3.8.5 | 21 | Paper | 🗃️ Archived (July 2025) |
|
||||
| 1.21.5 | _latest_ | 21 | Paper | ✅ **February 2026** (Non-LTS) |
|
||||
| 1.21.4 | _latest_ | 21 | Paper, Fabric | ✅ **February 2026** (Non-LTS) |
|
||||
| 1.21.3 | 3.7.1 | 21 | Paper, Fabric | 🗃️ Archived (December 2024) |
|
||||
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **May 2026** (LTS) |
|
||||
| 1.20.6 | 3.6.8 | 17 | Paper | 🗃️ Archived (October 2024) |
|
||||
| 1.20.4 | 3.6.8 | 17 | Paper | 🗃️ Archived (July 2024) |
|
||||
| 1.20.1 | 3.8.7 | 17 | Paper, Fabric | 🗃️ Archived (November 2024) |
|
||||
| 1.17.1 - 1.19.4 | 3.6.8 | 17 | Paper | 🗃️ Archived |
|
||||
| 1.16.5 | 3.2.1 | 16 | Paper | 🗃️ Archived |
|
||||
|
||||
HuskSync is primarily developed against the latest release. Old Minecraft versions are allocated a support channel based on popularity, mod support, etc:
|
||||
|
||||
|
||||
22
build.gradle
22
build.gradle
@@ -1,11 +1,11 @@
|
||||
import org.apache.tools.ant.filters.ReplaceTokens
|
||||
|
||||
plugins {
|
||||
id 'com.gradleup.shadow' version '8.3.6'
|
||||
id 'com.gradleup.shadow' version '9.2.2'
|
||||
id 'org.cadixdev.licenser' version '0.6.1' apply false
|
||||
id 'fabric-loom' version "$fabric_loom_version" apply false
|
||||
id 'dev.architectury.loom' version '1.9-SNAPSHOT' apply false
|
||||
id 'gg.essential.multi-version.root' apply false
|
||||
id 'org.ajoberstar.grgit' version '5.3.0'
|
||||
id 'org.ajoberstar.grgit' version '5.3.2'
|
||||
id 'maven-publish'
|
||||
id 'java'
|
||||
}
|
||||
@@ -89,10 +89,10 @@ allprojects {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.4'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.4'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.4'
|
||||
testCompileOnly 'org.jetbrains:annotations:26.0.2'
|
||||
testImplementation(platform("org.junit:junit-bom:6.0.1"))
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
testCompileOnly 'org.jetbrains:annotations:26.0.2-1'
|
||||
}
|
||||
|
||||
license {
|
||||
@@ -100,14 +100,14 @@ allprojects {
|
||||
include '**/*.java'
|
||||
newLine = true
|
||||
}
|
||||
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
processResources {
|
||||
def tokenMap = rootProject.ext.properties
|
||||
tokenMap.merge("grgit",'',(s, s2) -> s)
|
||||
tokenMap.merge("grgit", '', (s, s2) -> s)
|
||||
filesMatching(['**/*.json', '**/*.yml']) {
|
||||
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
|
||||
tokens: tokenMap
|
||||
@@ -133,11 +133,11 @@ subprojects {
|
||||
|
||||
// Version-specific configuration
|
||||
if (['fabric', 'bukkit'].contains(project.parent?.name)) {
|
||||
compileJava.options.release.set (project.name == '1.20.1' ? 17 : 21) // 1.20.1 requires Java 17
|
||||
compileJava.options.release.set 21
|
||||
version += "+mc.${project.name}"
|
||||
|
||||
if (project.parent?.name?.equals('fabric')) {
|
||||
apply plugin: 'fabric-loom'
|
||||
apply plugin: 'dev.architectury.loom'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
minecraft_version_numeric=12001
|
||||
minecraft_api_version=1.20
|
||||
paper_api_version=1.20.1-R0.1-SNAPSHOT
|
||||
@@ -1,3 +1,4 @@
|
||||
minecraft_version_range=1.21.1
|
||||
minecraft_version_numeric=12101
|
||||
minecraft_api_version=1.21
|
||||
paper_api_version=1.21.1-R0.1-SNAPSHOT
|
||||
4
bukkit/1.21.10/gradle.properties
Normal file
4
bukkit/1.21.10/gradle.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
minecraft_version_range=>=1.21.9 <=1.21.10
|
||||
minecraft_version_numeric=12110
|
||||
minecraft_api_version=1.21
|
||||
paper_api_version=1.21.10-R0.1-SNAPSHOT
|
||||
4
bukkit/1.21.11/gradle.properties
Normal file
4
bukkit/1.21.11/gradle.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
minecraft_version_range=>=1.21.11 <=1.21.11
|
||||
minecraft_version_numeric=12111
|
||||
minecraft_api_version=1.21
|
||||
paper_api_version=1.21.11-R0.1-SNAPSHOT
|
||||
@@ -1,3 +1,4 @@
|
||||
minecraft_version_range=1.21.4
|
||||
minecraft_version_numeric=12104
|
||||
minecraft_api_version=1.21
|
||||
paper_api_version=1.21.4-R0.1-SNAPSHOT
|
||||
4
bukkit/1.21.5/gradle.properties
Normal file
4
bukkit/1.21.5/gradle.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
minecraft_version_range=1.21.5
|
||||
minecraft_version_numeric=12105
|
||||
minecraft_api_version=1.21
|
||||
paper_api_version=1.21.5-R0.1-SNAPSHOT
|
||||
4
bukkit/1.21.8/gradle.properties
Normal file
4
bukkit/1.21.8/gradle.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
minecraft_version_range=>=1.21.7 <=1.21.8
|
||||
minecraft_version_numeric=12108
|
||||
minecraft_api_version=1.21
|
||||
paper_api_version=1.21.8-R0.1-SNAPSHOT
|
||||
@@ -8,32 +8,32 @@ plugins {
|
||||
dependencies {
|
||||
implementation project(path: ':common')
|
||||
|
||||
implementation 'net.william278.uniform:uniform-bukkit:1.3.1'
|
||||
implementation 'net.william278.uniform:uniform-paper:1.3.1'
|
||||
implementation 'net.william278.toilet:toilet-bukkit:1.0.12'
|
||||
implementation 'net.william278.uniform:uniform-bukkit:1.3.9'
|
||||
implementation 'net.william278.uniform:uniform-paper:1.3.9'
|
||||
implementation 'net.william278.toilet:toilet-bukkit:1.0.16'
|
||||
implementation 'net.william278:mpdbdataconverter:1.0.1'
|
||||
implementation 'net.william278:hsldataconverter:1.0'
|
||||
implementation 'net.william278:mapdataapi:2.0'
|
||||
implementation 'org.bstats:bstats-bukkit:3.1.0'
|
||||
implementation 'net.kyori:adventure-platform-bukkit:4.3.4'
|
||||
implementation 'dev.triumphteam:triumph-gui:3.1.11'
|
||||
implementation 'net.kyori:adventure-platform-bukkit:4.4.1'
|
||||
implementation 'dev.triumphteam:triumph-gui:3.1.12'
|
||||
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
|
||||
implementation 'de.tr7zw:item-nbt-api:2.14.2-SNAPSHOT'
|
||||
implementation 'de.tr7zw:item-nbt-api:2.15.5'
|
||||
|
||||
compileOnly "io.papermc.paper:paper-api:${paper_api_version}"
|
||||
compileOnly 'com.github.retrooper:packetevents-spigot:2.7.0'
|
||||
compileOnly 'com.github.retrooper:packetevents-spigot:2.10.1'
|
||||
compileOnly 'com.github.dmulloy2:ProtocolLib:5.3.0'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.36'
|
||||
compileOnly 'commons-io:commons-io:2.18.0'
|
||||
compileOnly 'org.json:json:20250107'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.42'
|
||||
compileOnly 'commons-io:commons-io:2.21.0'
|
||||
compileOnly 'org.json:json:20250517'
|
||||
compileOnly 'net.william278:minedown:1.8.2'
|
||||
compileOnly 'de.exlll:configlib-yaml:4.5.0'
|
||||
compileOnly 'com.zaxxer:HikariCP:6.2.1'
|
||||
compileOnly 'de.exlll:configlib-yaml:4.6.4'
|
||||
compileOnly 'com.zaxxer:HikariCP:7.0.2'
|
||||
compileOnly 'net.william278:DesertWell:2.0.4'
|
||||
compileOnly 'net.william278:AdvancementAPI:97a9583413'
|
||||
compileOnly "redis.clients:jedis:$jedis_version"
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.36'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.42'
|
||||
}
|
||||
|
||||
processResources {
|
||||
@@ -42,6 +42,7 @@ processResources {
|
||||
version: version,
|
||||
paper_api_version: paper_api_version,
|
||||
minecraft_version: project.name,
|
||||
minecraft_version_range: minecraft_version_range,
|
||||
minecraft_api_version: minecraft_api_version
|
||||
])
|
||||
}
|
||||
@@ -93,5 +94,9 @@ shadowJar {
|
||||
tasks {
|
||||
runServer {
|
||||
minecraftVersion(project.name)
|
||||
|
||||
downloadPlugins {
|
||||
github("plan-player-analytics", "Plan", "5.6.2965", "Plan-5.6-build-2965.jar")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.gson.Gson;
|
||||
import de.tr7zw.changeme.nbtapi.utils.DataFixerUtil;
|
||||
import de.tr7zw.changeme.nbtapi.NBT;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -48,6 +48,7 @@ import net.william278.husksync.event.BukkitEventDispatcher;
|
||||
import net.william278.husksync.hook.PlanHook;
|
||||
import net.william278.husksync.listener.BukkitEventListener;
|
||||
import net.william278.husksync.listener.LockedHandler;
|
||||
import net.william278.husksync.maps.BukkitMapHandler;
|
||||
import net.william278.husksync.migrator.LegacyMigrator;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.migrator.MpdbMigrator;
|
||||
@@ -56,7 +57,6 @@ import net.william278.husksync.sync.DataSyncer;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.util.BukkitLegacyConverter;
|
||||
import net.william278.husksync.maps.BukkitMapHandler;
|
||||
import net.william278.husksync.util.BukkitTask;
|
||||
import net.william278.husksync.util.LegacyConverter;
|
||||
import net.william278.toilet.BukkitToilet;
|
||||
@@ -92,13 +92,12 @@ 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 TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap(
|
||||
SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR
|
||||
);
|
||||
private final HashMap<Identifier, Serializer<? extends Data>> serializers = Maps.newHashMap();
|
||||
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 final Set<UUID> disconnectingPlayers = Sets.newConcurrentHashSet();
|
||||
|
||||
private boolean disabling;
|
||||
private Gson gson;
|
||||
@@ -148,6 +147,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
// Check compatibility
|
||||
checkCompatibility();
|
||||
|
||||
// Preload NBT-API
|
||||
if (!NBT.preloadApi()) {
|
||||
log(Level.SEVERE, "Failed to load NBT API. HuskSync will not be initialized!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Register commands
|
||||
initialize("commands", (plugin) -> getUniform().register(PluginCommand.Type.create(this)));
|
||||
|
||||
@@ -338,22 +343,6 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
return Version.fromString(getServer().getBukkitVersion());
|
||||
}
|
||||
|
||||
public int getDataVersion(@NotNull Version mcVersion) {
|
||||
return switch (mcVersion.toStringWithoutMetadata()) {
|
||||
case "1.16", "1.16.1", "1.16.2", "1.16.3", "1.16.4", "1.16.5" -> DataFixerUtil.VERSION1_16_5;
|
||||
case "1.17", "1.17.1" -> DataFixerUtil.VERSION1_17_1;
|
||||
case "1.18", "1.18.1", "1.18.2" -> DataFixerUtil.VERSION1_18_2;
|
||||
case "1.19", "1.19.1", "1.19.2" -> DataFixerUtil.VERSION1_19_2;
|
||||
case "1.20", "1.20.1", "1.20.2" -> DataFixerUtil.VERSION1_20_2;
|
||||
case "1.20.3", "1.20.4" -> DataFixerUtil.VERSION1_20_4;
|
||||
case "1.20.5", "1.20.6" -> DataFixerUtil.VERSION1_20_5;
|
||||
case "1.21", "1.21.1" -> DataFixerUtil.VERSION1_21;
|
||||
case "1.21.2", "1.21.3" -> DataFixerUtil.VERSION1_21_2;
|
||||
case "1.21.4" -> 4189/*DataFixerUtil.VERSION1_21_4*/;
|
||||
default -> DataFixerUtil.getCurrentVersion();
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getPlatformType() {
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
@@ -46,13 +47,20 @@ public class PaperHuskSyncLoader implements PluginLoader {
|
||||
resolveLibraries(classpathBuilder).stream()
|
||||
.map(DefaultArtifact::new)
|
||||
.forEach(artifact -> resolver.addDependency(new Dependency(artifact, null)));
|
||||
resolver.addRepository(new RemoteRepository.Builder(
|
||||
"maven", "default", "https://repo.maven.apache.org/maven2/"
|
||||
).build());
|
||||
resolver.addRepository(new RemoteRepository.Builder("maven", "default", getMavenUrl()).build());
|
||||
|
||||
classpathBuilder.addLibrary(resolver);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String getMavenUrl() {
|
||||
return Stream.of(
|
||||
System.getenv("PAPER_DEFAULT_CENTRAL_REPOSITORY"),
|
||||
System.getProperty("org.bukkit.plugin.java.LibraryLoader.centralURL"),
|
||||
"https://maven-central.storage-download.googleapis.com/maven2"
|
||||
).filter(Objects::nonNull).findFirst().orElseThrow(IllegalStateException::new);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<String> resolveLibraries(@NotNull PluginClasspathBuilder classpathBuilder) {
|
||||
try (InputStream input = getLibraryListFile()) {
|
||||
|
||||
@@ -38,13 +38,10 @@ import org.bukkit.attribute.AttributeModifier;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
//#if MC==12001
|
||||
//$$ import org.bukkit.inventory.EquipmentSlot;
|
||||
//#else
|
||||
import org.bukkit.inventory.EquipmentSlotGroup;
|
||||
//#endif
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -198,7 +195,9 @@ public abstract class BukkitData implements Data {
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
user.getPlayer().getEnderChest().setContents(plugin.setMapViews(getContents()));
|
||||
ItemStack[] fullContents = plugin.setMapViews(getContents());
|
||||
ItemStack[] enderChestContents = Arrays.copyOf(fullContents, Math.min(fullContents.length, user.getPlayer().getEnderChest().getSize()));
|
||||
user.getPlayer().getEnderChest().setContents(enderChestContents);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -366,7 +365,7 @@ public abstract class BukkitData implements Data {
|
||||
|
||||
// Set player experience and level (prevent advancement awards applying twice), reset game rule
|
||||
if (!toAward.isEmpty()
|
||||
&& (player.getLevel() != expLevel || player.getExp() != expProgress)) {
|
||||
&& (player.getLevel() != expLevel || player.getExp() != expProgress)) {
|
||||
player.setLevel(expLevel);
|
||||
player.setExp(expProgress);
|
||||
}
|
||||
@@ -486,8 +485,13 @@ public abstract class BukkitData implements Data {
|
||||
@NotNull Map<String, Map<String, Integer>> map) {
|
||||
registry.forEach(i -> {
|
||||
try {
|
||||
final int stat = i instanceof Material m ? p.getStatistic(id, m) :
|
||||
(i instanceof EntityType e ? p.getStatistic(id, e) : -1);
|
||||
int stat = 0;
|
||||
if (i instanceof Material mat && ((id.getType() == Statistic.Type.BLOCK && mat.isBlock())
|
||||
|| (id.getType() == Statistic.Type.ITEM && mat.isItem()))) {
|
||||
stat = p.getStatistic(id, mat);
|
||||
} else if (i instanceof EntityType ent && id.getType() == Statistic.Type.ENTITY) {
|
||||
stat = p.getStatistic(id, ent);
|
||||
}
|
||||
if (stat != 0) {
|
||||
map.compute(id.getKey().getKey(), (k, v) -> v == null ? Maps.newHashMap() : v)
|
||||
.put(i.getKey().getKey(), stat);
|
||||
@@ -516,8 +520,18 @@ public abstract class BukkitData implements Data {
|
||||
try {
|
||||
switch (type) {
|
||||
case UNTYPED -> player.setStatistic(stat, value);
|
||||
case BLOCK, ITEM -> player.setStatistic(stat, Objects.requireNonNull(matchMaterial(key[0])), value);
|
||||
case ENTITY -> player.setStatistic(stat, Objects.requireNonNull(matchEntityType(key[0])), value);
|
||||
case BLOCK, ITEM -> {
|
||||
Material material = matchMaterial(key.length > 0 ? key[0] : null);
|
||||
if (material != null) {
|
||||
player.setStatistic(stat, material, value);
|
||||
}
|
||||
}
|
||||
case ENTITY -> {
|
||||
EntityType entity = matchEntityType(key.length > 0 ? key[0] : null);
|
||||
if (entity != null) {
|
||||
player.setStatistic(stat, entity, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable a) {
|
||||
plugin.log(Level.WARNING, "Failed to apply statistic " + id, a);
|
||||
@@ -562,6 +576,14 @@ public abstract class BukkitData implements Data {
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Attributes adapt(@NotNull Player player, @NotNull HuskSync plugin) {
|
||||
if (!Bukkit.isPrimaryThread()) {
|
||||
try {
|
||||
return Bukkit.getScheduler().callSyncMethod((Plugin) plugin, () -> adapt(player, plugin)).get();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to adapt attributes on main thread", e);
|
||||
}
|
||||
}
|
||||
|
||||
final List<Attribute> attributes = Lists.newArrayList();
|
||||
final AttributeSettings settings = plugin.getSettings().getSynchronization().getAttributes();
|
||||
Registry.ATTRIBUTE.forEach(id -> {
|
||||
@@ -594,33 +616,19 @@ public abstract class BukkitData implements Data {
|
||||
instance.getBaseValue(),
|
||||
instance.getModifiers().stream()
|
||||
.filter(modifier -> !settings.isIgnoredModifier(modifier.getName()))
|
||||
//#if MC==12001
|
||||
//$$ .filter(modifier -> modifier.getSlot() == null)
|
||||
//#else
|
||||
.filter(modifier -> modifier.getSlotGroup() != EquipmentSlotGroup.ANY)
|
||||
//#endif
|
||||
.map(BukkitData.Attributes::adapt).collect(Collectors.toSet())
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Modifier adapt(@NotNull AttributeModifier modifier) {
|
||||
//#if MC==12001
|
||||
//$$ return new Modifier(
|
||||
//$$ modifier.getUniqueId(),
|
||||
//$$ modifier.getName(),
|
||||
//$$ modifier.getAmount(),
|
||||
//$$ modifier.getOperation().ordinal(),
|
||||
//$$ modifier.getSlot() != null ? modifier.getSlot().ordinal() : -1
|
||||
//$$ );
|
||||
//#else
|
||||
return new Modifier(
|
||||
modifier.getKey().toString(),
|
||||
modifier.getAmount(),
|
||||
modifier.getOperation().ordinal(),
|
||||
modifier.getSlotGroup().toString()
|
||||
);
|
||||
//#endif
|
||||
}
|
||||
|
||||
private static void applyAttribute(@Nullable AttributeInstance instance, @Nullable Attribute attribute) {
|
||||
@@ -640,26 +648,25 @@ public abstract class BukkitData implements Data {
|
||||
|
||||
@NotNull
|
||||
private static AttributeModifier adapt(@NotNull Modifier modifier) {
|
||||
//#if MC==12001
|
||||
//$$ return new AttributeModifier(
|
||||
//$$ modifier.uuid(),
|
||||
//$$ modifier.name(),
|
||||
//$$ modifier.amount(),
|
||||
//$$ AttributeModifier.Operation.values()[modifier.operation()],
|
||||
//$$ modifier.equipmentSlot() != -1 ? EquipmentSlot.values()[modifier.equipmentSlot()] : null
|
||||
//$$ );
|
||||
//#else
|
||||
return new AttributeModifier(
|
||||
Objects.requireNonNull(NamespacedKey.fromString(modifier.name())),
|
||||
modifier.amount(),
|
||||
AttributeModifier.Operation.values()[modifier.operation()],
|
||||
Optional.ofNullable(EquipmentSlotGroup.getByName(modifier.slotGroup())).orElse(EquipmentSlotGroup.ANY)
|
||||
);
|
||||
//#endif
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
|
||||
if (!Bukkit.isPrimaryThread()) {
|
||||
try {
|
||||
Bukkit.getScheduler().callSyncMethod(plugin, () -> { this.apply(user, plugin); return null; }).get();
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to apply attributes on main thread", e);
|
||||
}
|
||||
}
|
||||
|
||||
final AttributeSettings settings = plugin.getSettings().getSynchronization().getAttributes();
|
||||
Registry.ATTRIBUTE.forEach(id -> {
|
||||
if (settings.isIgnoredAttribute(id.getKey().toString())) {
|
||||
|
||||
@@ -31,7 +31,11 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
|
||||
@Override
|
||||
default Optional<? extends Data> getData(@NotNull Identifier id) {
|
||||
if (!id.isCustom()) {
|
||||
if (id.isCustom()) {
|
||||
return Optional.ofNullable(getCustomDataStore().get(id));
|
||||
}
|
||||
|
||||
try {
|
||||
return switch (id.getKeyValue()) {
|
||||
case "inventory" -> getInventory();
|
||||
case "ender_chest" -> getEnderChest();
|
||||
@@ -48,8 +52,10 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
case "persistent_data" -> getPersistentData();
|
||||
default -> throw new IllegalStateException(String.format("Unexpected data type: %s", id));
|
||||
};
|
||||
} catch (Throwable e) {
|
||||
getPlugin().debug("Failed to get data for key: " + id.asMinimalString(), e);
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable(getCustomDataStore().get(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -135,7 +135,7 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onMapInitialize(@NotNull MapInitializeEvent event) {
|
||||
if (plugin.getSettings().getSynchronization().isPersistLockedMaps() && event.getMap().isLocked()) {
|
||||
getPlugin().runAsync(() -> ((BukkitHuskSync) plugin).renderPersistedMap(event.getMap()));
|
||||
getPlugin().runAsync(() -> ((BukkitHuskSync) plugin).renderInitializingLockedMap(event.getMap()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ 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
|
||||
@@ -124,7 +123,6 @@ public class BukkitLockedEventListener implements LockedHandler, Listener {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ public class PaperEventListener extends BukkitEventListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("RedundantMethodOverride")
|
||||
public void onEnable() {
|
||||
getPlugin().getServer().getPluginManager().registerEvents(this, getPlugin());
|
||||
lockedHandler.onEnable();
|
||||
|
||||
@@ -22,6 +22,7 @@ package net.william278.husksync.maps;
|
||||
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.ReadableItemNBT;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadableNBT;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
@@ -43,9 +44,11 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -53,6 +56,8 @@ public interface BukkitMapHandler {
|
||||
|
||||
// The map used to store HuskSync data in ItemStack NBT
|
||||
String MAP_DATA_KEY = "husksync:persisted_locked_map";
|
||||
// The legacy map key used to store pixel data (3.7.3 and below)
|
||||
String MAP_LEGACY_PIXEL_DATA_KEY = "husksync:canvas_data";
|
||||
// Name of server the map originates from
|
||||
String MAP_ORIGIN_KEY = "origin";
|
||||
// Original map id
|
||||
@@ -97,11 +102,12 @@ public interface BukkitMapHandler {
|
||||
}
|
||||
if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) {
|
||||
items[i] = function.apply(item);
|
||||
} else if (item.getItemMeta() instanceof BlockStateMeta b && b.getBlockState() instanceof Container box) {
|
||||
} else if (item.getItemMeta() instanceof BlockStateMeta b && b.getBlockState() instanceof Container box
|
||||
&& !box.getInventory().isEmpty()) {
|
||||
forEachMap(box.getInventory().getContents(), function);
|
||||
b.setBlockState(box);
|
||||
item.setItemMeta(b);
|
||||
} else if (item.getItemMeta() instanceof BundleMeta bundle) {
|
||||
} else if (item.getItemMeta() instanceof BundleMeta bundle && bundle.hasItems()) {
|
||||
bundle.setItems(List.of(forEachMap(bundle.getItems().toArray(ItemStack[]::new), function)));
|
||||
item.setItemMeta(bundle);
|
||||
}
|
||||
@@ -118,8 +124,8 @@ public interface BukkitMapHandler {
|
||||
|
||||
@Nullable
|
||||
@Blocking
|
||||
private Map.Entry<MapData, Boolean> readMapData(@NotNull String serverName, int mapId) {
|
||||
final Map.Entry<byte[], Boolean> readData = fetchMapData(serverName, mapId);
|
||||
private MapData readMapData(@NotNull String serverName, int mapId) {
|
||||
final byte[] readData = fetchMapData(serverName, mapId);
|
||||
if (readData == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -128,23 +134,23 @@ public interface BukkitMapHandler {
|
||||
|
||||
@Nullable
|
||||
@Blocking
|
||||
private Map.Entry<byte[], Boolean> fetchMapData(@NotNull String serverName, int mapId) {
|
||||
private byte[] fetchMapData(@NotNull String serverName, int mapId) {
|
||||
return fetchMapData(serverName, mapId, true);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Blocking
|
||||
private Map.Entry<byte[], Boolean> fetchMapData(@NotNull String serverName, int mapId, boolean doReverseLookup) {
|
||||
private byte[] fetchMapData(@NotNull String serverName, int mapId, boolean doReverseLookup) {
|
||||
// Read from Redis cache
|
||||
final byte[] redisData = getRedisManager().getMapData(serverName, mapId);
|
||||
if (redisData != null) {
|
||||
return new AbstractMap.SimpleImmutableEntry<>(redisData, true);
|
||||
return redisData;
|
||||
}
|
||||
|
||||
// Read from database and set to Redis
|
||||
@Nullable Map.Entry<byte[], Boolean> databaseData = getPlugin().getDatabase().getMapData(serverName, mapId);
|
||||
final byte[] databaseData = getPlugin().getDatabase().getMapData(serverName, mapId);
|
||||
if (databaseData != null) {
|
||||
getRedisManager().setMapData(serverName, mapId, databaseData.getKey());
|
||||
getRedisManager().setMapData(serverName, mapId, databaseData);
|
||||
return databaseData;
|
||||
}
|
||||
|
||||
@@ -156,7 +162,7 @@ public interface BukkitMapHandler {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Map.Entry<byte[], Boolean> fetchReversedMapData(@NotNull String serverName, int mapId) {
|
||||
private byte[] fetchReversedMapData(@NotNull String serverName, int mapId) {
|
||||
// Lookup binding from Redis cache, then fetch data if found
|
||||
Map.Entry<String, Integer> binding = getRedisManager().getReversedMapBound(serverName, mapId);
|
||||
if (binding != null) {
|
||||
@@ -173,13 +179,10 @@ public interface BukkitMapHandler {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Map.Entry<MapData, Boolean> deserializeMapData(@NotNull Map.Entry<byte[], Boolean> data) {
|
||||
private MapData deserializeMapData(byte @NotNull [] data) {
|
||||
try {
|
||||
return new AbstractMap.SimpleImmutableEntry<>(
|
||||
getPlugin().getDataAdapter().fromBytes(data.getKey(), AdaptableMapData.class)
|
||||
.getData(getPlugin().getDataVersion(getPlugin().getMinecraftVersion())),
|
||||
data.getValue()
|
||||
);
|
||||
return getPlugin().getDataAdapter().fromBytes(data, AdaptableMapData.class)
|
||||
.getData(getPlugin().getDataVersion(getPlugin().getMinecraftVersion()));
|
||||
} catch (IOException e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to deserialize map data", e);
|
||||
return null;
|
||||
@@ -248,92 +251,136 @@ public interface BukkitMapHandler {
|
||||
if (!nbt.hasTag(MAP_DATA_KEY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ReadableNBT mapData = nbt.getCompound(MAP_DATA_KEY);
|
||||
if (mapData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine map ID
|
||||
final String originServerName = mapData.getString(MAP_ORIGIN_KEY);
|
||||
final String currentServerName = getPlugin().getServerName();
|
||||
final int originalMapId = mapData.getInteger(MAP_ID_KEY);
|
||||
int newId = currentServerName.equals(originServerName)
|
||||
? originalMapId : getBoundMapId(originServerName, originalMapId, currentServerName);
|
||||
// Server the map was originally created on, and the current server. If they match, isOrigin is true.
|
||||
final String originServer = mapData.getString(MAP_ORIGIN_KEY);
|
||||
final String currentServer = getPlugin().getServerName();
|
||||
final boolean isOrigin = currentServer.equals(originServer);
|
||||
|
||||
// Determine the map's ID on its origin server, and the new ID it should be bound to here.
|
||||
// Then, update the map item / data accordingly (re-rendering and caching the map if needed)
|
||||
final int originalId = mapData.getInteger(MAP_ID_KEY);
|
||||
int newId = isOrigin ? originalId : getBoundMapId(originServer, originalId, currentServer);
|
||||
if (newId != -1) {
|
||||
meta.setMapId(newId);
|
||||
map.setItemMeta(meta);
|
||||
getPlugin().debug(String.format("Map ID set to %s", newId));
|
||||
return;
|
||||
handleBoundMap(meta, nbt, originServer, originalId, newId, isOrigin);
|
||||
} else {
|
||||
handleUnboundMap(meta, nbt, originServer, originalId, currentServer);
|
||||
}
|
||||
|
||||
// Read the pixel data and generate a map view otherwise
|
||||
getPlugin().debug("Deserializing map data from NBT and generating view...");
|
||||
final MapData canvasData = Objects.requireNonNull(readMapData(originServerName, originalMapId), "Pixel data null!").getKey();
|
||||
|
||||
// Add a renderer to the map with the data and save to file
|
||||
final MapView view = generateRenderedMap(canvasData);
|
||||
meta.setMapView(view);
|
||||
map.setItemMeta(meta);
|
||||
|
||||
// Bind in the database & Redis
|
||||
final int id = view.getId();
|
||||
getRedisManager().bindMapIds(originServerName, originalMapId, currentServerName, id);
|
||||
getPlugin().getDatabase().setMapBinding(originServerName, originalMapId, currentServerName, id);
|
||||
|
||||
getPlugin().debug(String.format("Bound map to view (#%s) on server %s", id, currentServerName));
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
default void renderPersistedMap(@NotNull MapView view) {
|
||||
if (getMapView(view.getId()).isPresent()) {
|
||||
private void handleBoundMap(@NotNull MapMeta meta, @NotNull ReadableItemNBT nbt, @NotNull String originServer,
|
||||
int originalId, int newId, boolean isOrigin) {
|
||||
MapView view = Bukkit.getMap(newId);
|
||||
if (isOrigin && view != null) {
|
||||
meta.setMapView(view);
|
||||
getPlugin().debug("Map ID set to original ID #%s".formatted(newId));
|
||||
return;
|
||||
}
|
||||
|
||||
@Nullable final Map.Entry<MapData, Boolean> data = readMapData(getPlugin().getServerName(), view.getId());
|
||||
Optional<MapView> optionalView = getMapView(newId);
|
||||
if (optionalView.isPresent()) {
|
||||
meta.setMapView(optionalView.get());
|
||||
getPlugin().debug("Map ID set to #%s".formatted(newId));
|
||||
return;
|
||||
}
|
||||
|
||||
getPlugin().debug("Deserializing map data from NBT and generating view...");
|
||||
MapData mapData = readMapData(originServer, originalId);
|
||||
if (mapData == null && nbt.hasTag(MAP_LEGACY_PIXEL_DATA_KEY)) {
|
||||
mapData = readLegacyMapItemData(nbt);
|
||||
}
|
||||
|
||||
if (mapData == null) {
|
||||
getPlugin().debug("Read pixel data was not found in database, skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
MapView newView = view != null ? view : Bukkit.createMap(getDefaultMapWorld());
|
||||
generateRenderedMap(mapData, newView);
|
||||
meta.setMapView(newView);
|
||||
}
|
||||
|
||||
private void handleUnboundMap(@NotNull MapMeta meta, @NotNull ReadableItemNBT nbt, @NotNull String originServer,
|
||||
int originalId, @NotNull String currentServer) {
|
||||
getPlugin().debug("Deserializing map data from NBT and generating view...");
|
||||
MapData mapData = readMapData(originServer, originalId);
|
||||
if (mapData == null && nbt.hasTag(MAP_LEGACY_PIXEL_DATA_KEY)) {
|
||||
mapData = readLegacyMapItemData(nbt);
|
||||
}
|
||||
|
||||
if (mapData == null) {
|
||||
getPlugin().debug("Read pixel data was not found in database, skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
final MapView view = generateRenderedMap(Objects.requireNonNull(mapData, "Pixel data null!"));
|
||||
meta.setMapView(view);
|
||||
|
||||
final int id = view.getId();
|
||||
getRedisManager().bindMapIds(originServer, originalId, currentServer, id);
|
||||
getPlugin().getDatabase().setMapBinding(originServer, originalId, currentServer, id);
|
||||
|
||||
getPlugin().debug("Bound map to view (#%s) on server %s".formatted(id, currentServer));
|
||||
}
|
||||
|
||||
// Render a persisted locked map that is initializing (i.e. in an item frame)
|
||||
default void renderInitializingLockedMap(@NotNull MapView view) {
|
||||
if (view.isVirtual()) {
|
||||
return;
|
||||
}
|
||||
final Optional<MapView> optionalView = getMapView(view.getId());
|
||||
if (optionalView.isPresent()) {
|
||||
view.getRenderers().clear();
|
||||
view.getRenderers().addAll(optionalView.get().getRenderers());
|
||||
view.setLocked(true);
|
||||
view.setScale(MapView.Scale.CLOSEST);
|
||||
view.setTrackingPosition(false);
|
||||
view.setUnlimitedTracking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
MapData data = readMapData(getPlugin().getServerName(), view.getId());
|
||||
if (data == null) {
|
||||
final World world = view.getWorld() == null ? getDefaultMapWorld() : view.getWorld();
|
||||
data = readLegacyMapFileData(view.getId());
|
||||
}
|
||||
|
||||
if (data == null) {
|
||||
World world = view.getWorld() == null ? getDefaultMapWorld() : view.getWorld();
|
||||
getPlugin().debug("Not rendering map: no data in DB for world %s, map #%s."
|
||||
.formatted(world.getName(), view.getId()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.getValue()) {
|
||||
// from this server, doesn't need tweaking
|
||||
return;
|
||||
}
|
||||
|
||||
final MapData canvasData = data.getKey();
|
||||
|
||||
// Create a new map view renderer with the map data color at each pixel
|
||||
// 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);
|
||||
renderMapView(view, data);
|
||||
}
|
||||
|
||||
// Sets the renderer of a map, and returns the generated MapView
|
||||
@NotNull
|
||||
private MapView generateRenderedMap(@NotNull MapData canvasData) {
|
||||
final MapView view = Bukkit.createMap(getDefaultMapWorld());
|
||||
view.getRenderers().clear();
|
||||
return generateRenderedMap(canvasData, Bukkit.createMap(getDefaultMapWorld()));
|
||||
}
|
||||
|
||||
// Create a new map view renderer with the map data color at each pixel
|
||||
@NotNull
|
||||
private MapView generateRenderedMap(@NotNull MapData canvasData, @NotNull MapView view) {
|
||||
renderMapView(view, canvasData);
|
||||
return view;
|
||||
}
|
||||
|
||||
private void renderMapView(@NotNull MapView view, @NotNull MapData canvasData) {
|
||||
view.getRenderers().clear();
|
||||
view.addRenderer(new PersistentMapRenderer(canvasData));
|
||||
view.setLocked(true);
|
||||
view.setScale(MapView.Scale.NORMAL);
|
||||
view.setScale(MapView.Scale.CLOSEST);
|
||||
view.setTrackingPosition(false);
|
||||
view.setUnlimitedTracking(false);
|
||||
|
||||
// Set the view to the map and return it
|
||||
setMapView(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -415,6 +462,36 @@ public interface BukkitMapHandler {
|
||||
);
|
||||
}
|
||||
|
||||
// Legacy - read maps from item stacks
|
||||
@Nullable
|
||||
@Blocking
|
||||
private MapData readLegacyMapItemData(@NotNull ReadableItemNBT nbt) {
|
||||
final int dataVer = getPlugin().getDataVersion(getPlugin().getMinecraftVersion());
|
||||
try {
|
||||
return MapData.fromByteArray(dataVer,
|
||||
Objects.requireNonNull(nbt.getByteArray(MAP_LEGACY_PIXEL_DATA_KEY)));
|
||||
} catch (IOException e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to read legacy map data", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy - read maps from files
|
||||
@Nullable
|
||||
private MapData readLegacyMapFileData(int mapId) {
|
||||
final Path path = getPlugin().getDataFolder().toPath().resolve("maps").resolve(mapId + ".dat");
|
||||
final File file = path.toFile();
|
||||
if (!file.exists()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return MapData.fromNbt(file);
|
||||
} catch (IOException e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to read legacy map file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link MapCanvas} implementation used for pre-rendering maps to be converted into {@link MapData}
|
||||
*/
|
||||
@@ -514,11 +591,7 @@ public interface BukkitMapHandler {
|
||||
final List<MapBanner> banners = Lists.newArrayList();
|
||||
for (int i = 0; i < getCursors().size(); i++) {
|
||||
final MapCursor cursor = getCursors().getCursor(i);
|
||||
//#if MC==12001
|
||||
//$$ final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH);
|
||||
//#else
|
||||
final String type = cursor.getType().getKey().getKey();
|
||||
//#endif
|
||||
if (type.startsWith(BANNER_PREFIX)) {
|
||||
banners.add(new MapBanner(
|
||||
type.replaceAll(BANNER_PREFIX, ""),
|
||||
@@ -530,7 +603,7 @@ public interface BukkitMapHandler {
|
||||
}
|
||||
|
||||
}
|
||||
return MapData.fromPixels(mapDataVersion, pixels, getDimension(), (byte) 2, banners, List.of());
|
||||
return MapData.fromPixels(mapDataVersion, pixels, getDimension(), (byte) 0, banners, List.of());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,8 +57,9 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOffline() {
|
||||
return player == null || !player.isOnline();
|
||||
public boolean hasDisconnected() {
|
||||
return getPlugin().getDisconnectingPlayers().contains(getUuid())
|
||||
|| player == null || !player.isOnline();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -51,11 +51,7 @@ public final class BukkitKeyedAdapter {
|
||||
|
||||
@Nullable
|
||||
public static PotionEffectType matchEffectType(@NotNull String key) {
|
||||
//#if MC==12001
|
||||
//$$ return PotionEffectType.getByName(key);
|
||||
//#else
|
||||
return getRegistryValue(Registry.EFFECT, key);
|
||||
//#endif
|
||||
}
|
||||
|
||||
private static <T extends Keyed> T getRegistryValue(@NotNull Registry<T> registry, @NotNull String keyString) {
|
||||
|
||||
0
bukkit/src/main/resources/META-INF/.mojang-mapped
Normal file
0
bukkit/src/main/resources/META-INF/.mojang-mapped
Normal file
@@ -1,2 +1,2 @@
|
||||
# File used for checking Minecraft server compatibility with this version of HuskSync
|
||||
minecraft_version: '${minecraft_version}'
|
||||
minecraft_version_range: '${minecraft_version_range}'
|
||||
@@ -3,31 +3,31 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'commons-io:commons-io:2.18.0'
|
||||
api 'org.apache.commons:commons-text:1.13.0'
|
||||
api 'commons-io:commons-io:2.21.0'
|
||||
api 'org.apache.commons:commons-text:1.14.0'
|
||||
api 'net.william278:minedown:1.8.2'
|
||||
api 'net.william278:mapdataapi:2.0'
|
||||
api 'org.json:json:20250107'
|
||||
api 'com.google.code.gson:gson:2.12.1'
|
||||
api 'org.json:json:20250517'
|
||||
api 'com.google.code.gson:gson:2.13.2'
|
||||
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
|
||||
api 'de.exlll:configlib-yaml:4.5.0'
|
||||
api 'de.exlll:configlib-yaml:4.6.4'
|
||||
api 'net.william278:paginedown:1.1.2'
|
||||
api 'net.william278:DesertWell:2.0.4'
|
||||
api('com.zaxxer:HikariCP:6.2.1') {
|
||||
api('com.zaxxer:HikariCP:7.0.2') {
|
||||
exclude module: 'slf4j-api'
|
||||
}
|
||||
|
||||
compileOnlyApi 'net.william278.toilet:toilet-common:1.0.12'
|
||||
compileOnlyApi 'net.william278.toilet:toilet-common:1.0.16'
|
||||
|
||||
compileOnly 'net.william278.uniform:uniform-common:1.3.1'
|
||||
compileOnly 'net.william278.uniform:uniform-common:1.3.9'
|
||||
compileOnly 'com.mojang:brigadier:1.1.8'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.36'
|
||||
compileOnly 'org.jetbrains:annotations:26.0.2'
|
||||
compileOnly 'net.kyori:adventure-api:4.19.0'
|
||||
compileOnly 'net.kyori:adventure-platform-api:4.3.4'
|
||||
compileOnly "net.kyori:adventure-text-serializer-plain:4.19.0"
|
||||
compileOnly 'com.google.guava:guava:33.4.0-jre'
|
||||
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.42'
|
||||
compileOnly 'org.jetbrains:annotations:26.0.2-1'
|
||||
compileOnly 'net.kyori:adventure-api:4.25.0'
|
||||
compileOnly 'net.kyori:adventure-platform-api:4.4.0'
|
||||
compileOnly "net.kyori:adventure-text-serializer-plain:4.25.0"
|
||||
compileOnly 'com.google.guava:guava:33.5.0-jre'
|
||||
compileOnly 'com.github.plan-player-analytics:Plan:5.6.2965'
|
||||
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"
|
||||
@@ -37,10 +37,10 @@ dependencies {
|
||||
|
||||
testImplementation "redis.clients:jedis:$jedis_version"
|
||||
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
|
||||
testImplementation 'com.google.guava:guava:33.4.0-jre'
|
||||
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
testCompileOnly 'de.exlll:configlib-yaml:4.5.0'
|
||||
testCompileOnly 'org.jetbrains:annotations:26.0.2'
|
||||
testImplementation 'com.google.guava:guava:33.5.0-jre'
|
||||
testImplementation 'com.github.plan-player-analytics:Plan:5.6.2965'
|
||||
testCompileOnly 'de.exlll:configlib-yaml:4.6.4'
|
||||
testCompileOnly 'org.jetbrains:annotations:26.0.2-1'
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.36'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.42'
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import com.fatboyindustrial.gsonjavatime.Converters;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
@@ -40,10 +41,7 @@ import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.sync.DataSyncer;
|
||||
import net.william278.husksync.user.ConsoleUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.util.CompatibilityChecker;
|
||||
import net.william278.husksync.util.DumpProvider;
|
||||
import net.william278.husksync.util.LegacyConverter;
|
||||
import net.william278.husksync.util.Task;
|
||||
import net.william278.husksync.util.*;
|
||||
import net.william278.uniform.Uniform;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -56,7 +54,7 @@ import java.util.logging.Level;
|
||||
* Abstract implementation of the HuskSync plugin.
|
||||
*/
|
||||
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider, SerializerRegistry,
|
||||
CompatibilityChecker, DumpProvider {
|
||||
CompatibilityChecker, DumpProvider, DataVersionSupplier {
|
||||
|
||||
int SPIGOT_RESOURCE_ID = 97144;
|
||||
|
||||
@@ -140,7 +138,7 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
if (getPlayerCustomDataStore().containsKey(user.getUuid())) {
|
||||
return getPlayerCustomDataStore().get(user.getUuid());
|
||||
}
|
||||
final Map<Identifier, Data> data = new HashMap<>();
|
||||
final Map<Identifier, Data> data = Maps.newHashMap();
|
||||
getPlayerCustomDataStore().put(user.getUuid(), data);
|
||||
return data;
|
||||
}
|
||||
@@ -251,14 +249,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
@NotNull
|
||||
Version getMinecraftVersion();
|
||||
|
||||
/**
|
||||
* Returns the data version for a Minecraft version
|
||||
*
|
||||
* @param minecraftVersion the Minecraft version
|
||||
* @return the data version int
|
||||
*/
|
||||
int getDataVersion(@NotNull Version minecraftVersion);
|
||||
|
||||
/**
|
||||
* Returns the platform type
|
||||
*
|
||||
@@ -315,6 +305,12 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
@NotNull
|
||||
Set<UUID> getLockedPlayers();
|
||||
|
||||
/**
|
||||
* Get the set of UUIDs of players who are currently marked as disconnecting or disconnected
|
||||
*/
|
||||
@NotNull
|
||||
Set<UUID> getDisconnectingPlayers();
|
||||
|
||||
default boolean isLocked(@NotNull UUID uuid) {
|
||||
return getLockedPlayers().contains(uuid);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ public class InventoryCommand extends ItemsCommand {
|
||||
@NotNull User user, boolean allowEdit) {
|
||||
final Optional<Data.Items.Inventory> optionalInventory = snapshot.getInventory();
|
||||
if (optionalInventory.isEmpty()) {
|
||||
viewer.sendMessage(new MineDown("what the FUCK is happening"));
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return;
|
||||
|
||||
@@ -215,11 +215,17 @@ public class UserDataCommand extends PluginCommand {
|
||||
|
||||
@NotNull
|
||||
private CommandProvider view() {
|
||||
return (sub) -> sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final UUID version = ctx.getArgument("version", UUID.class);
|
||||
viewSnapshot(user(sub, ctx), user, version);
|
||||
}, user("username"), versionUuid());
|
||||
return (sub) -> {
|
||||
sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
final UUID version = ctx.getArgument("version", UUID.class);
|
||||
viewSnapshot(user(sub, ctx), user, version);
|
||||
}, user("username"), versionUuid());
|
||||
sub.addSyntax((ctx) -> {
|
||||
final User user = ctx.getArgument("username", User.class);
|
||||
viewLatestSnapshot(user(sub, ctx), user);
|
||||
}, user("username"));
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -22,13 +22,11 @@ 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)
|
||||
@@ -66,5 +64,9 @@ public class Server {
|
||||
}
|
||||
return super.equals(other);
|
||||
}
|
||||
|
||||
|
||||
public String getName() {
|
||||
final String envServerName = System.getenv("HUSKSYNC_SERVER_NAME");
|
||||
return envServerName == null ? name : envServerName;
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,7 @@ public class Settings {
|
||||
}
|
||||
}
|
||||
|
||||
// 𝓡𝓮𝓭𝓲𝓼 settings
|
||||
// Redis settings
|
||||
@Comment("Redis settings")
|
||||
private RedisSettings redis = new RedisSettings();
|
||||
|
||||
@@ -159,7 +159,9 @@ public class Settings {
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class RedisSettings {
|
||||
|
||||
@Comment("Specify the credentials of your Redis server here. Set \"password\" to '' if you don't have one")
|
||||
@Comment({"Specify the credentials of your Redis server here.",
|
||||
"Set \"user\" to '' if you don't have one or would like to use the default user.",
|
||||
"Set \"password\" to '' if you don't have one."})
|
||||
private RedisCredentials credentials = new RedisCredentials();
|
||||
|
||||
@Getter
|
||||
@@ -168,8 +170,49 @@ public class Settings {
|
||||
public static class RedisCredentials {
|
||||
private String host = "localhost";
|
||||
private int port = 6379;
|
||||
@Comment("Only change the database if you know what you are doing. The default is 0.")
|
||||
private int database = 0;
|
||||
private String user = "";
|
||||
private String password = "";
|
||||
|
||||
@Comment("Use SSL/TLS for encrypted connections.")
|
||||
private boolean useSsl = false;
|
||||
|
||||
@Comment("Connection timeout in milliseconds.")
|
||||
private int connectionTimeout = 2000;
|
||||
|
||||
@Comment("Socket (read/write) timeout in milliseconds.")
|
||||
private int socketTimeout = 2000;
|
||||
|
||||
@Comment("Max number of connections in the pool.")
|
||||
private int maxTotalConnections = 50;
|
||||
|
||||
@Comment("Max number of idle connections in the pool.")
|
||||
private int maxIdleConnections = 8;
|
||||
|
||||
@Comment("Min number of idle connections in the pool.")
|
||||
private int minIdleConnections = 2;
|
||||
|
||||
@Comment("Enable health checks when borrowing connections from the pool.")
|
||||
private boolean testOnBorrow = true;
|
||||
|
||||
@Comment("Enable health checks when returning connections to the pool.")
|
||||
private boolean testOnReturn = true;
|
||||
|
||||
@Comment("Enable periodic idle connection health checks.")
|
||||
private boolean testWhileIdle = true;
|
||||
|
||||
@Comment("Min evictable idle time (ms) before a connection is eligible for eviction.")
|
||||
private long minEvictableIdleTimeMillis = 60000;
|
||||
|
||||
@Comment("Time (ms) between eviction runs.")
|
||||
private long timeBetweenEvictionRunsMillis = 30000;
|
||||
|
||||
@Comment("Number of retries for commands when connection fails.")
|
||||
private int maxRetries = 3;
|
||||
|
||||
@Comment("Base backoff time in ms for retries (exponential backoff multiplier).")
|
||||
private int retryBackoffMillis = 200;
|
||||
}
|
||||
|
||||
@Comment("Options for if you're using Redis sentinel. Don't modify this unless you know what you're doing!")
|
||||
@@ -321,6 +364,9 @@ public class Settings {
|
||||
@Getter(AccessLevel.NONE)
|
||||
private Map<String, String> eventPriorities = EventListener.ListenerType.getDefaults();
|
||||
|
||||
@Comment("Enable check-in petitions for data syncing (don't change this unless you know what you're doing)")
|
||||
private boolean checkinPetitions = false;
|
||||
|
||||
public boolean doAutoPin(@NotNull DataSnapshot.SaveCause cause) {
|
||||
return autoPinnedSaveCauses.contains(cause.name());
|
||||
}
|
||||
|
||||
@@ -31,7 +31,10 @@ public interface DataHolder {
|
||||
Map<Identifier, Data> getData();
|
||||
|
||||
default Optional<? extends Data> getData(@NotNull Identifier id) {
|
||||
return getData().entrySet().stream().filter(e -> e.getKey().equals(id)).map(Map.Entry::getValue).findFirst();
|
||||
if (getData().containsKey(id)) {
|
||||
return Optional.of(getData().get(id));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
default void setData(@NotNull Identifier identifier, @NotNull Data data) {
|
||||
|
||||
@@ -370,7 +370,7 @@ public class DataSnapshot {
|
||||
public static class Unpacked extends DataSnapshot implements DataHolder {
|
||||
|
||||
@Expose(serialize = false, deserialize = false)
|
||||
private final TreeMap<Identifier, Data> deserialized;
|
||||
private final Map<Identifier, Data> deserialized;
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@@ -381,7 +381,7 @@ public class DataSnapshot {
|
||||
}
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull TreeMap<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, serverName, Map.of(), minecraftVersion, platformType, formatVersion);
|
||||
this.deserialized = data;
|
||||
@@ -389,14 +389,15 @@ public class DataSnapshot {
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
private TreeMap<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
|
||||
private Map<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
|
||||
return data.entrySet().stream()
|
||||
.filter(e -> plugin.getIdentifier(e.getKey()).isPresent())
|
||||
.map(entry -> Map.entry(plugin.getIdentifier(entry.getKey()).orElseThrow(), entry.getValue()))
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> plugin.deserializeData(entry.getKey(), entry.getValue(), getMinecraftVersion()),
|
||||
(a, b) -> b, () -> Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR)
|
||||
(a, b) -> a,
|
||||
HashMap::new
|
||||
));
|
||||
}
|
||||
|
||||
@@ -406,7 +407,9 @@ public class DataSnapshot {
|
||||
return deserialized.entrySet().stream()
|
||||
.collect(Collectors.toMap(
|
||||
entry -> entry.getKey().toString(),
|
||||
entry -> plugin.serializeData(entry.getKey(), entry.getValue())
|
||||
entry -> plugin.serializeData(entry.getKey(), entry.getValue()),
|
||||
(a, b) -> a,
|
||||
HashMap::new
|
||||
));
|
||||
}
|
||||
|
||||
@@ -421,6 +424,20 @@ public class DataSnapshot {
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a sorted iterable of the snapshots the snapshot is holding
|
||||
*
|
||||
* @return The data map
|
||||
* @since 3.8.2
|
||||
*/
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public Iterable<Map.Entry<Identifier, Data>> getSortedIterable() {
|
||||
final TreeMap<Identifier, Data> tree = Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR);
|
||||
tree.putAll(deserialized);
|
||||
return tree.entrySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack the {@link DataSnapshot} into a {@link DataSnapshot.Packed packed} snapshot
|
||||
*
|
||||
@@ -453,12 +470,12 @@ public class DataSnapshot {
|
||||
private String serverName;
|
||||
private boolean pinned;
|
||||
private OffsetDateTime timestamp;
|
||||
private final TreeMap<Identifier, Data> data;
|
||||
private final Map<Identifier, Data> data;
|
||||
|
||||
private Builder(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
this.pinned = false;
|
||||
this.data = Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR);
|
||||
this.data = Maps.newHashMap();
|
||||
this.timestamp = OffsetDateTime.now();
|
||||
this.id = UUID.randomUUID();
|
||||
this.serverName = plugin.getServerName();
|
||||
|
||||
@@ -22,6 +22,7 @@ package net.william278.husksync.data;
|
||||
import lombok.*;
|
||||
import net.kyori.adventure.key.InvalidKeyException;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.kyori.adventure.key.KeyPattern;
|
||||
import org.intellij.lang.annotations.Subst;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -37,7 +38,10 @@ import java.util.stream.Stream;
|
||||
* Identifiers of different types of {@link Data}s
|
||||
*/
|
||||
@Getter
|
||||
public class Identifier {
|
||||
public class Identifier implements Comparable<Identifier> {
|
||||
|
||||
// Namespace for built-in identifiers
|
||||
private static final @KeyPattern String DEFAULT_NAMESPACE = "husksync";
|
||||
|
||||
// Built-in identifiers
|
||||
public static final Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
|
||||
@@ -93,8 +97,8 @@ public class Identifier {
|
||||
*/
|
||||
@NotNull
|
||||
public static Identifier from(@NotNull Key key, @NotNull Set<Dependency> dependencies) {
|
||||
if (key.namespace().equals("husksync")) {
|
||||
throw new IllegalArgumentException("You cannot register a key with \"husksync\" as the namespace!");
|
||||
if (key.namespace().equals(DEFAULT_NAMESPACE)) {
|
||||
throw new IllegalArgumentException("Cannot register with %s as key namespace!".formatted(key.namespace()));
|
||||
}
|
||||
return new Identifier(key, true, dependencies);
|
||||
}
|
||||
@@ -143,7 +147,7 @@ public class Identifier {
|
||||
@NotNull
|
||||
private static Identifier huskSync(@Subst("null") @NotNull String name,
|
||||
boolean configDefault) throws InvalidKeyException {
|
||||
return new Identifier(Key.key("husksync", name), configDefault, Collections.emptySet());
|
||||
return new Identifier(Key.key(DEFAULT_NAMESPACE, name), configDefault, Collections.emptySet());
|
||||
}
|
||||
|
||||
// Return an identifier with a HuskSync namespace
|
||||
@@ -151,7 +155,7 @@ public class Identifier {
|
||||
private static Identifier huskSync(@Subst("null") @NotNull String name,
|
||||
@SuppressWarnings("SameParameterValue") boolean configDefault,
|
||||
@NotNull Dependency... dependents) throws InvalidKeyException {
|
||||
return new Identifier(Key.key("husksync", name), configDefault, Set.of(dependents));
|
||||
return new Identifier(Key.key(DEFAULT_NAMESPACE, name), configDefault, Set.of(dependents));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,13 +213,30 @@ public class Identifier {
|
||||
* @return {@code false} if {@link #getKeyNamespace()} returns "husksync"; {@code true} otherwise
|
||||
*/
|
||||
public boolean isCustom() {
|
||||
return !getKeyNamespace().equals("husksync");
|
||||
return !getKeyNamespace().equals(DEFAULT_NAMESPACE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the minimal string representation of this key.
|
||||
* <p>
|
||||
* If the namespace of the key is {@link #DEFAULT_NAMESPACE}, only the key value will be returned.
|
||||
*
|
||||
* @return the minimal string key representation
|
||||
* @since 3.8
|
||||
*/
|
||||
@NotNull
|
||||
public String asMinimalString() {
|
||||
if (getKey().namespace().equals(DEFAULT_NAMESPACE)) {
|
||||
return getKey().value();
|
||||
}
|
||||
return getKey().asString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier as a string (the key)
|
||||
*
|
||||
* @return the identifier as a string
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
@Override
|
||||
@@ -224,14 +245,29 @@ public class Identifier {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the given object is an identifier with the same key as this identifier
|
||||
* Return whether this Identifier is equal to another Identifier
|
||||
*
|
||||
* @param obj the object to compare
|
||||
* @return {@code true} if the given object is an identifier with the same key as this identifier
|
||||
* @param obj another object
|
||||
* @return {@code true} if this identifier matches the identifier of {@code obj}
|
||||
* @since 3.8
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
return obj instanceof Identifier other ? toString().equals(other.toString()) : super.equals(obj);
|
||||
if (obj instanceof Identifier other) {
|
||||
return asMinimalString().equals(other.asMinimalString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hash code of the Identifier (equivalent to {@link #asMinimalString()}->{@code #hashCode()}
|
||||
*
|
||||
* @return the hash code
|
||||
* @since 3.8
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return asMinimalString().hashCode();
|
||||
}
|
||||
|
||||
// Get the config entry for the identifier
|
||||
@@ -240,6 +276,14 @@ public class Identifier {
|
||||
return Map.entry(getKeyValue(), enabledByDefault);
|
||||
}
|
||||
|
||||
// Comparable; always sort this Identifier after any dependencies
|
||||
@Override
|
||||
public int compareTo(@NotNull Identifier o) {
|
||||
if (this.dependsOn(o)) return 1;
|
||||
if (o.dependsOn(this)) return -1;
|
||||
return this.key.compareTo(o.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two identifiers based on their dependencies.
|
||||
* <p>
|
||||
@@ -313,6 +357,11 @@ public class Identifier {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return key.toString().hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public interface SerializerRegistry {
|
||||
|
||||
@@ -40,7 +41,7 @@ public interface SerializerRegistry {
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
<T extends Data> TreeMap<Identifier, Serializer<T>> getSerializers();
|
||||
<T extends Data> Map<Identifier, Serializer<T>> getSerializers();
|
||||
|
||||
/**
|
||||
* Register a data serializer for the given {@link Identifier}
|
||||
@@ -87,8 +88,7 @@ public interface SerializerRegistry {
|
||||
* @since 3.0
|
||||
*/
|
||||
default Optional<Identifier> getIdentifier(@NotNull String key) {
|
||||
return getSerializers().keySet().stream()
|
||||
.filter(id -> id.getKey().asString().equals(key)).findFirst();
|
||||
return getSerializers().keySet().stream().filter(e -> e.toString().equals(key)).findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,9 +99,7 @@ public interface SerializerRegistry {
|
||||
* @since 3.5.4
|
||||
*/
|
||||
default Optional<Serializer<Data>> getSerializer(@NotNull Identifier identifier) {
|
||||
return getSerializers().entrySet().stream()
|
||||
.filter(entry -> entry.getKey().getKey().equals(identifier.getKey()))
|
||||
.map(Map.Entry::getValue).findFirst();
|
||||
return Optional.ofNullable(getSerializers().get(identifier));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,14 +151,14 @@ public interface SerializerRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the set of registered data types
|
||||
* Get the list of registered data types, in dependency order
|
||||
*
|
||||
* @return the set of registered data types
|
||||
* @return the list of registered data types
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
default Set<Identifier> getRegisteredDataTypes() {
|
||||
return getSerializers().keySet();
|
||||
default List<Identifier> getRegisteredDataTypes() {
|
||||
return getSerializers().keySet().stream().sorted(DEPENDENCY_ORDER_COMPARATOR).toList();
|
||||
}
|
||||
|
||||
// Returns if a data type is available and enabled in the config
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A holder of data in the form of {@link Data}s, which can be synced
|
||||
@@ -46,7 +47,11 @@ public interface UserDataHolder extends DataHolder {
|
||||
.filter(Identifier::isEnabled)
|
||||
.map(id -> Map.entry(id, getData(id)))
|
||||
.filter(data -> data.getValue().isPresent())
|
||||
.collect(HashMap::new, (map, data) -> map.put(data.getKey(), data.getValue().get()), HashMap::putAll);
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> entry.getValue().get(),
|
||||
(a, b) -> a, HashMap::new
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,6 +80,15 @@ public interface UserDataHolder extends DataHolder {
|
||||
return DataSnapshot.builder(getPlugin()).data(this.getData()).saveCause(saveCause).buildAndPack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether data can be applied to the holder at this time
|
||||
*
|
||||
* @return {@code true} if data can be applied, otherwise false
|
||||
*/
|
||||
default boolean cannotApplySnapshot() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize and apply a data snapshot to this data owner
|
||||
* <p>
|
||||
@@ -90,9 +104,12 @@ public interface UserDataHolder extends DataHolder {
|
||||
* @since 3.0
|
||||
*/
|
||||
default void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull ThrowingConsumer<Boolean> runAfter) {
|
||||
final HuskSync plugin = getPlugin();
|
||||
if (cannotApplySnapshot()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unpack the snapshot
|
||||
final HuskSync plugin = getPlugin();
|
||||
final DataSnapshot.Unpacked unpacked;
|
||||
try {
|
||||
unpacked = snapshot.unpack(plugin);
|
||||
@@ -104,8 +121,12 @@ public interface UserDataHolder extends DataHolder {
|
||||
|
||||
// Synchronously attempt to apply the snapshot
|
||||
plugin.runSync(() -> {
|
||||
if (cannotApplySnapshot()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
for (Map.Entry<Identifier, Data> entry : unpacked.getData().entrySet()) {
|
||||
for (Map.Entry<Identifier, Data> entry : unpacked.getSortedIterable()) {
|
||||
final Identifier identifier = entry.getKey();
|
||||
if (!identifier.isEnabled()) {
|
||||
continue;
|
||||
|
||||
@@ -24,6 +24,7 @@ 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.intellij.lang.annotations.Language;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -68,7 +69,7 @@ public abstract class Database {
|
||||
* @return the formatted statement, with table placeholders replaced with the correct names
|
||||
*/
|
||||
@NotNull
|
||||
protected final String formatStatementTables(@NotNull String sql) {
|
||||
protected final String formatStatementTables(@NotNull @Language("SQL") String sql) {
|
||||
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))
|
||||
@@ -138,6 +139,15 @@ public abstract class Database {
|
||||
@NotNull
|
||||
public abstract List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Get the number of unpinned {@link DataSnapshot}s a user has
|
||||
*
|
||||
* @param user the user to count snapshots for
|
||||
* @return the number of snapshots this user has saved
|
||||
*/
|
||||
@Blocking
|
||||
public abstract int getUnpinnedSnapshotCount(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Gets a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
|
||||
*
|
||||
@@ -264,17 +274,17 @@ public abstract class Database {
|
||||
*
|
||||
* @param serverName Name of the server the map originates from
|
||||
* @param mapId Original map ID
|
||||
* @return Map.Entry (key: map data, value: is from current world)
|
||||
* @return the map data bytes, or null if not found
|
||||
*/
|
||||
@Blocking
|
||||
public abstract @Nullable Map.Entry<byte[], Boolean> getMapData(@NotNull String serverName, int mapId);
|
||||
public abstract byte @Nullable [] getMapData(@NotNull String serverName, int mapId);
|
||||
|
||||
/**
|
||||
* Get a map server -> ID binding in the database
|
||||
* Reverse lookup: given a local map binding, find the origin server and map ID.
|
||||
*
|
||||
* @param serverName Name of the server the map originates from
|
||||
* @param mapId Original map ID
|
||||
* @return Map.Entry (key: server name, value: map ID)
|
||||
* @param serverName Name of the local server (to_server_name in the binding)
|
||||
* @param mapId Local map ID on this server (to_id in the binding)
|
||||
* @return Map.Entry with origin server name (key) and origin map ID (value), or null if not found
|
||||
*/
|
||||
@Blocking
|
||||
public abstract @Nullable Map.Entry<String, Integer> getMapBinding(@NotNull String serverName, int mapId);
|
||||
@@ -296,7 +306,7 @@ public abstract class Database {
|
||||
* @param fromServerName Name of the server the map originates from
|
||||
* @param fromMapId Original map ID
|
||||
* @param toServerName Name of the new server
|
||||
* @return New map ID or -1 if not found
|
||||
* @return New map ID or -1 if not found
|
||||
*/
|
||||
@Blocking
|
||||
public abstract int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName);
|
||||
|
||||
@@ -234,6 +234,17 @@ public class MongoDbDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getUnpinnedSnapshotCount(@NotNull User user) {
|
||||
try {
|
||||
Document filter = new Document("player_uuid", user.getUuid()).append("pinned", false);
|
||||
return (int) mongoCollectionHelper.getCollection(userDataTable).countDocuments(filter);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current snapshot count", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
@@ -259,17 +270,14 @@ public class MongoDbDatabase extends Database {
|
||||
@Override
|
||||
protected void rotateSnapshots(@NotNull User user) {
|
||||
try {
|
||||
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
|
||||
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
|
||||
final int unpinnedSnapshots = getUnpinnedSnapshotCount(user);
|
||||
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
|
||||
if (unpinnedUserData.size() > maxSnapshots) {
|
||||
|
||||
if (unpinnedSnapshots > maxSnapshots) {
|
||||
Document filter = new Document("player_uuid", user.getUuid()).append("pinned", false);
|
||||
Document sort = new Document("timestamp", 1); // 1 = Ascending
|
||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable)
|
||||
.find(filter)
|
||||
.sort(sort)
|
||||
.limit(unpinnedUserData.size() - maxSnapshots);
|
||||
.find(filter).sort(sort)
|
||||
.limit(unpinnedSnapshots - maxSnapshots);
|
||||
|
||||
for (Document doc : iterable) {
|
||||
mongoCollectionHelper.deleteDocument(userDataTable, doc);
|
||||
@@ -368,14 +376,14 @@ public class MongoDbDatabase extends Database {
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public @Nullable Map.Entry<byte[], Boolean> getMapData(@NotNull String serverName, int mapId) {
|
||||
public byte @Nullable [] getMapData(@NotNull String serverName, int mapId) {
|
||||
try {
|
||||
Document filter = new Document("server_name", serverName).append("map_id", mapId);
|
||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(mapDataTable).find(filter);
|
||||
Document doc = iterable.first();
|
||||
if (doc != null) {
|
||||
final Binary bin = doc.get("data", Binary.class);
|
||||
return Map.entry(bin.getData(), true);
|
||||
return bin.getData();
|
||||
}
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
|
||||
@@ -391,8 +399,8 @@ public class MongoDbDatabase extends Database {
|
||||
final Document doc = iterable.first();
|
||||
if (doc != null) {
|
||||
return new AbstractMap.SimpleImmutableEntry<>(
|
||||
doc.getString("server_name"),
|
||||
doc.getInteger("to_id")
|
||||
doc.getString("from_server_name"),
|
||||
doc.getInteger("from_id")
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -299,11 +299,30 @@ public class MySqlDatabase extends Database {
|
||||
return retrievedData;
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's list of snapshots from the database", e);
|
||||
}
|
||||
return retrievedData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getUnpinnedSnapshotCount(@NotNull User user) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT COUNT(`version_uuid`)
|
||||
FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=? AND `pinned`=false;"""))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return resultSet.getInt(1);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current snapshot count", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
@@ -336,10 +355,9 @@ public class MySqlDatabase extends Database {
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateSnapshots(@NotNull User user) {
|
||||
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
|
||||
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
|
||||
final int unpinnedSnapshots = getUnpinnedSnapshotCount(user);
|
||||
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
|
||||
if (unpinnedUserData.size() > maxSnapshots) {
|
||||
if (unpinnedSnapshots > maxSnapshots) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM `%user_data_table%`
|
||||
@@ -347,7 +365,7 @@ public class MySqlDatabase extends Database {
|
||||
AND `pinned` IS FALSE
|
||||
ORDER BY `timestamp` ASC
|
||||
LIMIT %entry_count%;""".replace("%entry_count%",
|
||||
Integer.toString(unpinnedUserData.size() - maxSnapshots))))) {
|
||||
Integer.toString(unpinnedSnapshots - maxSnapshots))))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
@@ -455,7 +473,7 @@ public class MySqlDatabase extends Database {
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public @Nullable Map.Entry<byte[], Boolean> getMapData(@NotNull String serverName, int mapId) {
|
||||
public byte @Nullable [] getMapData(@NotNull String serverName, int mapId) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `data`
|
||||
@@ -470,7 +488,7 @@ public class MySqlDatabase extends Database {
|
||||
final Blob blob = resultSet.getBlob("data");
|
||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
return Map.entry(dataByteArray, true);
|
||||
return dataByteArray;
|
||||
}
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
|
||||
@@ -288,11 +288,30 @@ public class PostgresDatabase extends Database {
|
||||
return retrievedData;
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's list of snapshots from the database", e);
|
||||
}
|
||||
return retrievedData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getUnpinnedSnapshotCount(@NotNull User user) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT COUNT(version_uuid)
|
||||
FROM %user_data_table%
|
||||
WHERE player_uuid=? AND pinned=false;"""))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return resultSet.getInt(1);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current snapshot count", e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
@@ -323,10 +342,9 @@ public class PostgresDatabase extends Database {
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateSnapshots(@NotNull User user) {
|
||||
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
|
||||
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
|
||||
final int unpinnedSnapshots = getUnpinnedSnapshotCount(user);
|
||||
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
|
||||
if (unpinnedUserData.size() > maxSnapshots) {
|
||||
if (unpinnedSnapshots > maxSnapshots) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
WITH cte AS (
|
||||
@@ -339,7 +357,7 @@ public class PostgresDatabase extends Database {
|
||||
)
|
||||
DELETE FROM %user_data_table%
|
||||
WHERE version_uuid IN (SELECT version_uuid FROM cte);""".replace("%entry_count%",
|
||||
Integer.toString(unpinnedUserData.size() - maxSnapshots))))) {
|
||||
Integer.toString(unpinnedSnapshots - maxSnapshots))))) {
|
||||
statement.setObject(1, user.getUuid());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
@@ -451,7 +469,7 @@ public class PostgresDatabase extends Database {
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public @Nullable Map.Entry<byte[], Boolean> getMapData(@NotNull String serverName, int mapId) {
|
||||
public byte @Nullable [] getMapData(@NotNull String serverName, int mapId) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT data
|
||||
@@ -462,8 +480,7 @@ public class PostgresDatabase extends Database {
|
||||
statement.setInt(2, mapId);
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final byte[] data = resultSet.getBytes("data");
|
||||
return new AbstractMap.SimpleImmutableEntry<>(data, true);
|
||||
return resultSet.getBytes("data");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -25,9 +25,7 @@ import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import static net.william278.husksync.config.Settings.SynchronizationSettings.SaveOnDeathSettings;
|
||||
|
||||
@@ -49,6 +47,7 @@ public abstract class EventListener {
|
||||
* @param user The {@link OnlineUser} to handle
|
||||
*/
|
||||
protected final void handlePlayerJoin(@NotNull OnlineUser user) {
|
||||
plugin.getDisconnectingPlayers().remove(user.getUuid());
|
||||
if (user.isNpc()) {
|
||||
return;
|
||||
}
|
||||
@@ -62,11 +61,17 @@ public abstract class EventListener {
|
||||
* @param user The {@link OnlineUser} to handle
|
||||
*/
|
||||
protected final void handlePlayerQuit(@NotNull OnlineUser user) {
|
||||
if (user.isNpc() || plugin.isDisabling() || plugin.isLocked(user.getUuid())) {
|
||||
// Check the user is a user, the plugin isn't disabling, then mark as disconnecting
|
||||
if (user.isNpc() || plugin.isDisabling()) {
|
||||
return;
|
||||
}
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
plugin.getDataSyncer().syncSaveUserData(user);
|
||||
plugin.getDisconnectingPlayers().add(user.getUuid());
|
||||
|
||||
// Lock, then save their data if the user is unlocked
|
||||
if (!plugin.isLocked(user.getUuid())) {
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
plugin.getDataSyncer().syncSaveUserData(user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,7 +84,7 @@ public abstract class EventListener {
|
||||
return;
|
||||
}
|
||||
usersInWorld.stream()
|
||||
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
|
||||
.filter(user -> !user.isNpc() && !user.hasDisconnected() && !plugin.isLocked(user.getUuid()))
|
||||
.forEach(user -> plugin.getDataSyncer().saveCurrentUserData(
|
||||
user, DataSnapshot.SaveCause.WORLD_SAVE
|
||||
));
|
||||
|
||||
@@ -62,41 +62,62 @@ public class RedisManager extends JedisPubSub {
|
||||
/**
|
||||
* Initialize Redis connection pool
|
||||
*/
|
||||
|
||||
@Blocking
|
||||
public void initialize() throws IllegalStateException {
|
||||
final Settings.RedisSettings.RedisCredentials credentials = plugin.getSettings().getRedis().getCredentials();
|
||||
|
||||
final String user = credentials.getUser();
|
||||
final String password = credentials.getPassword();
|
||||
final String host = credentials.getHost();
|
||||
final int port = credentials.getPort();
|
||||
final int database = credentials.getDatabase();
|
||||
final boolean useSSL = credentials.isUseSsl();
|
||||
|
||||
// Create the jedis pool
|
||||
// Configure JedisPoolConfig
|
||||
final JedisPoolConfig config = new JedisPoolConfig();
|
||||
config.setMaxIdle(0);
|
||||
config.setTestOnBorrow(true);
|
||||
config.setTestOnReturn(true);
|
||||
config.setMaxTotal(credentials.getMaxTotalConnections());
|
||||
config.setMaxIdle(credentials.getMaxIdleConnections());
|
||||
config.setMinIdle(credentials.getMinIdleConnections());
|
||||
config.setTestOnBorrow(credentials.isTestOnBorrow());
|
||||
config.setTestOnReturn(credentials.isTestOnReturn());
|
||||
config.setTestWhileIdle(credentials.isTestWhileIdle());
|
||||
config.setMinEvictableIdleTimeMillis(credentials.getMinEvictableIdleTimeMillis());
|
||||
config.setTimeBetweenEvictionRunsMillis(credentials.getTimeBetweenEvictionRunsMillis());
|
||||
|
||||
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);
|
||||
// Standalone Redis setup
|
||||
DefaultJedisClientConfig.Builder clientConfigBuilder = DefaultJedisClientConfig.builder()
|
||||
.ssl(useSSL)
|
||||
.database(database)
|
||||
.timeoutMillis(credentials.getConnectionTimeout()) // connection and socket timeout combined
|
||||
.user(user.isEmpty() ? null : user)
|
||||
.password(password.isEmpty() ? null : password);
|
||||
|
||||
this.jedisPool = new JedisPool(config, new HostAndPort(host, port), clientConfigBuilder.build());
|
||||
} else {
|
||||
final String sentinelPassword = sentinel.getPassword();
|
||||
this.jedisPool = new JedisSentinelPool(sentinel.getMaster(), redisSentinelNodes, password.isEmpty()
|
||||
? null : password, sentinelPassword.isEmpty() ? null : sentinelPassword);
|
||||
this.jedisPool = new JedisSentinelPool(
|
||||
sentinel.getMaster(),
|
||||
redisSentinelNodes,
|
||||
config,
|
||||
credentials.getConnectionTimeout(),
|
||||
credentials.getSocketTimeout(),
|
||||
password.isEmpty() ? null : password,
|
||||
sentinelPassword.isEmpty() ? null : sentinelPassword,
|
||||
database);
|
||||
}
|
||||
|
||||
// Ping the server to check the connection
|
||||
try {
|
||||
jedisPool.getResource().ping();
|
||||
try (var jedis = jedisPool.getResource()) {
|
||||
jedis.ping();
|
||||
} catch (JedisException e) {
|
||||
throw new IllegalStateException("Failed to establish connection with Redis. "
|
||||
+ "Please check the supplied credentials in the config file", e);
|
||||
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();
|
||||
}
|
||||
@@ -113,8 +134,7 @@ public class RedisManager extends JedisPubSub {
|
||||
this,
|
||||
Arrays.stream(RedisMessage.Type.values())
|
||||
.map(type -> type.getMessageChannel(clusterId))
|
||||
.toArray(String[]::new)
|
||||
);
|
||||
.toArray(String[]::new));
|
||||
} catch (Throwable t) {
|
||||
// Thread was unlocked due error
|
||||
onThreadUnlock(t);
|
||||
@@ -158,28 +178,47 @@ public class RedisManager extends JedisPubSub {
|
||||
|
||||
final RedisMessage redisMessage = RedisMessage.fromJson(plugin, message);
|
||||
switch (messageType) {
|
||||
case UPDATE_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
|
||||
case UPDATE_USER_DATA -> redisMessage.getTargetUser(plugin).ifPresent(
|
||||
user -> {
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
try {
|
||||
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload());
|
||||
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin,
|
||||
redisMessage.getPayload());
|
||||
user.applySnapshot(data, DataSnapshot.UpdateCause.UPDATED);
|
||||
} 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(
|
||||
});
|
||||
case REQUEST_USER_DATA -> redisMessage.getTargetUser(plugin).ifPresent(
|
||||
user -> RedisMessage.create(
|
||||
UUID.fromString(new String(redisMessage.getPayload(), StandardCharsets.UTF_8)),
|
||||
user.createSnapshot(DataSnapshot.SaveCause.INVENTORY_COMMAND).asBytes(plugin)
|
||||
).dispatch(plugin, RedisMessage.Type.RETURN_USER_DATA)
|
||||
);
|
||||
user.createSnapshot(DataSnapshot.SaveCause.INVENTORY_COMMAND).asBytes(plugin))
|
||||
.dispatch(plugin, RedisMessage.Type.RETURN_USER_DATA));
|
||||
case CHECK_IN_PETITION -> {
|
||||
if (!redisMessage.isTargetServer(plugin)
|
||||
|| !plugin.getSettings().getSynchronization().isCheckinPetitions()) {
|
||||
return;
|
||||
}
|
||||
final String payload = new String(redisMessage.getPayload(), StandardCharsets.UTF_8);
|
||||
final User user = new User(UUID.fromString(payload.split("/")[0]), payload.split("/")[1]);
|
||||
|
||||
// Only release checkout if user is truly offline AND not being processed
|
||||
final boolean isOnline = plugin.getOnlineUser(user.getUuid()).isPresent();
|
||||
final boolean isLocked = plugin.isLocked(user.getUuid());
|
||||
|
||||
if (isOnline || isLocked) {
|
||||
plugin.debug("[%s] Petition ignored - user still being processed (online=%s, locked=%s)"
|
||||
.formatted(user.getName(), isOnline, isLocked));
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getRedisManager().setUserCheckedOut(user, false);
|
||||
plugin.debug("[%s] Petition accepted - user checked in".formatted(user.getName()));
|
||||
}
|
||||
case RETURN_USER_DATA -> {
|
||||
final CompletableFuture<Optional<DataSnapshot.Packed>> future = pendingRequests.get(
|
||||
redisMessage.getTargetUuid()
|
||||
);
|
||||
final UUID target = redisMessage.getTargetUuid().orElse(null);
|
||||
final CompletableFuture<Optional<DataSnapshot.Packed>> future = pendingRequests.get(target);
|
||||
if (future != null) {
|
||||
try {
|
||||
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload());
|
||||
@@ -188,7 +227,7 @@ public class RedisManager extends JedisPubSub {
|
||||
plugin.log(Level.SEVERE, "An exception occurred returning user data from Redis", e);
|
||||
future.complete(Optional.empty());
|
||||
}
|
||||
pendingRequests.remove(redisMessage.getTargetUuid());
|
||||
pendingRequests.remove(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,38 +250,43 @@ public class RedisManager extends JedisPubSub {
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
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, RedisMessage.Type.UPDATE_USER_DATA);
|
||||
});
|
||||
final RedisMessage redisMessage = RedisMessage.create(user.getUuid(), data.asBytes(plugin));
|
||||
redisMessage.dispatch(plugin, RedisMessage.Type.UPDATE_USER_DATA);
|
||||
}
|
||||
|
||||
public CompletableFuture<Optional<DataSnapshot.Packed>> getOnlineUserData(@NotNull UUID requestId, @NotNull User user,
|
||||
@NotNull DataSnapshot.SaveCause saveCause) {
|
||||
@Blocking
|
||||
public void petitionServerCheckin(@NotNull String server, @NotNull User user) {
|
||||
final RedisMessage redisMessage = RedisMessage.create(
|
||||
server, "%s/%s".formatted(user.getUuid(), user.getName()).getBytes(StandardCharsets.UTF_8));
|
||||
redisMessage.dispatch(plugin, RedisMessage.Type.CHECK_IN_PETITION);
|
||||
}
|
||||
|
||||
public CompletableFuture<Optional<DataSnapshot.Packed>> getOnlineUserData(@NotNull UUID requestId,
|
||||
@NotNull User user,
|
||||
@NotNull DataSnapshot.SaveCause saveCause) {
|
||||
return plugin.getOnlineUser(user.getUuid())
|
||||
.map(online -> CompletableFuture.completedFuture(
|
||||
Optional.of(online.createSnapshot(saveCause)))
|
||||
)
|
||||
Optional.of(online.createSnapshot(saveCause))))
|
||||
.orElse(this.getNetworkedUserData(requestId, user));
|
||||
}
|
||||
|
||||
// Request a user's dat x-server
|
||||
private CompletableFuture<Optional<DataSnapshot.Packed>> getNetworkedUserData(@NotNull UUID requestId, @NotNull User user) {
|
||||
private CompletableFuture<Optional<DataSnapshot.Packed>> getNetworkedUserData(@NotNull UUID requestId,
|
||||
@NotNull User user) {
|
||||
final CompletableFuture<Optional<DataSnapshot.Packed>> future = new CompletableFuture<>();
|
||||
pendingRequests.put(requestId, future);
|
||||
plugin.runAsync(() -> {
|
||||
final RedisMessage redisMessage = RedisMessage.create(
|
||||
user.getUuid(),
|
||||
requestId.toString().getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
requestId.toString().getBytes(StandardCharsets.UTF_8));
|
||||
redisMessage.dispatch(plugin, RedisMessage.Type.REQUEST_USER_DATA);
|
||||
});
|
||||
return future
|
||||
.orTimeout(
|
||||
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds(),
|
||||
TimeUnit.MILLISECONDS
|
||||
)
|
||||
TimeUnit.MILLISECONDS)
|
||||
.exceptionally(throwable -> {
|
||||
pendingRequests.remove(requestId);
|
||||
return Optional.empty();
|
||||
@@ -256,8 +300,7 @@ public class RedisManager extends JedisPubSub {
|
||||
jedis.setex(
|
||||
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId),
|
||||
RedisKeyType.TTL_1_YEAR,
|
||||
data.asBytes(plugin)
|
||||
);
|
||||
data.asBytes(plugin));
|
||||
plugin.debug(String.format("[%s] Set %s key on Redis", user.getName(), RedisKeyType.LATEST_SNAPSHOT));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred setting user data on Redis", e);
|
||||
@@ -268,8 +311,7 @@ public class RedisManager extends JedisPubSub {
|
||||
public void clearUserData(@NotNull User user) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.del(
|
||||
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId)
|
||||
);
|
||||
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId));
|
||||
plugin.debug(String.format("[%s] Cleared %s on Redis", user.getName(), RedisKeyType.LATEST_SNAPSHOT));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred clearing user data on Redis", e);
|
||||
@@ -283,8 +325,7 @@ public class RedisManager extends JedisPubSub {
|
||||
if (checkedOut) {
|
||||
jedis.set(
|
||||
key.getBytes(StandardCharsets.UTF_8),
|
||||
plugin.getServerName().getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
plugin.getServerName().getBytes(StandardCharsets.UTF_8));
|
||||
} else {
|
||||
if (jedis.del(key.getBytes(StandardCharsets.UTF_8)) == 0) {
|
||||
plugin.debug(String.format("[%s] %s key not set on Redis when attempting removal (%s)",
|
||||
@@ -348,8 +389,7 @@ public class RedisManager extends JedisPubSub {
|
||||
jedis.setex(
|
||||
getKey(RedisKeyType.SERVER_SWITCH, user.getUuid(), clusterId),
|
||||
RedisKeyType.TTL_10_SECONDS,
|
||||
new byte[0]
|
||||
);
|
||||
new byte[0]);
|
||||
plugin.debug(String.format("[%s] Set %s key to Redis",
|
||||
user.getName(), RedisKeyType.SERVER_SWITCH));
|
||||
} catch (Throwable e) {
|
||||
@@ -361,7 +401,8 @@ public class RedisManager extends JedisPubSub {
|
||||
* 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.
|
||||
* @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) {
|
||||
@@ -421,7 +462,7 @@ public class RedisManager extends JedisPubSub {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.ping();
|
||||
return startTime - System.currentTimeMillis();
|
||||
return System.currentTimeMillis() - startTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,13 +483,11 @@ public class RedisManager extends JedisPubSub {
|
||||
jedis.setex(
|
||||
getMapIdKey(fromServer, fromId, toServer, clusterId),
|
||||
RedisKeyType.TTL_1_YEAR,
|
||||
String.valueOf(toId).getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
String.valueOf(toId).getBytes(StandardCharsets.UTF_8));
|
||||
jedis.setex(
|
||||
getReversedMapIdKey(toServer, toId, clusterId),
|
||||
RedisKeyType.TTL_1_YEAR,
|
||||
String.format("%s:%s", fromServer, fromId).getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
String.format("%s:%s", fromServer, fromId).getBytes(StandardCharsets.UTF_8));
|
||||
plugin.debug(String.format("Bound map %s:%s -> %s:%s on Redis", fromServer, fromId, toServer, toId));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred binding map ids on Redis", e);
|
||||
@@ -500,8 +539,7 @@ public class RedisManager extends JedisPubSub {
|
||||
jedis.setex(
|
||||
getMapDataKey(serverName, mapId, clusterId),
|
||||
RedisKeyType.TTL_1_YEAR,
|
||||
data
|
||||
);
|
||||
data);
|
||||
plugin.debug(String.format("Set map data %s:%s on Redis", serverName, mapId));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred setting map data on Redis", e);
|
||||
@@ -547,16 +585,20 @@ public class RedisManager extends JedisPubSub {
|
||||
return String.format("%s:%s", keyType.getKeyPrefix(clusterId), uuid);
|
||||
}
|
||||
|
||||
private static byte[] getMapIdKey(@NotNull String fromServer, int fromId, @NotNull String toServer, @NotNull String clusterId) {
|
||||
return String.format("%s:%s:%s:%s", RedisKeyType.MAP_ID.getKeyPrefix(clusterId), fromServer, fromId, toServer).getBytes(StandardCharsets.UTF_8);
|
||||
private static byte[] getMapIdKey(@NotNull String fromServer, int fromId, @NotNull String toServer,
|
||||
@NotNull String clusterId) {
|
||||
return String.format("%s:%s:%s:%s", RedisKeyType.MAP_ID.getKeyPrefix(clusterId), fromServer, fromId, toServer)
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static byte[] getReversedMapIdKey(@NotNull String toServer, int toId, @NotNull String clusterId) {
|
||||
return String.format("%s:%s:%s", RedisKeyType.MAP_ID_REVERSED.getKeyPrefix(clusterId), toServer, toId).getBytes(StandardCharsets.UTF_8);
|
||||
return String.format("%s:%s:%s", RedisKeyType.MAP_ID_REVERSED.getKeyPrefix(clusterId), toServer, toId)
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static byte[] getMapDataKey(@NotNull String serverName, int mapId, @NotNull String clusterId) {
|
||||
return String.format("%s:%s:%s", RedisKeyType.MAP_DATA.getKeyPrefix(clusterId), serverName, mapId).getBytes(StandardCharsets.UTF_8);
|
||||
return String.format("%s:%s:%s", RedisKeyType.MAP_DATA.getKeyPrefix(clusterId), serverName, mapId)
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,25 +25,38 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Setter
|
||||
public class RedisMessage implements Adaptable {
|
||||
|
||||
private @Nullable String targetServer;
|
||||
@SerializedName("target_uuid")
|
||||
private UUID targetUuid;
|
||||
private @Nullable UUID targetUuid;
|
||||
@Getter
|
||||
@Setter
|
||||
@SerializedName("payload")
|
||||
private byte[] payload;
|
||||
|
||||
private RedisMessage(byte[] payload) {
|
||||
setPayload(payload);
|
||||
}
|
||||
|
||||
private RedisMessage(@NotNull UUID targetUuid, byte[] message) {
|
||||
this(message);
|
||||
this.setTargetUuid(targetUuid);
|
||||
this.setPayload(message);
|
||||
}
|
||||
|
||||
private RedisMessage(@NotNull String targetServer, byte[] message) {
|
||||
this(message);
|
||||
this.setTargetServer(targetServer);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@@ -55,6 +68,11 @@ public class RedisMessage implements Adaptable {
|
||||
return new RedisMessage(targetUuid, message);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static RedisMessage create(@NotNull String targetServer, byte[] message) {
|
||||
return new RedisMessage(targetServer, message);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static RedisMessage fromJson(@NotNull HuskSync plugin, @NotNull String json) throws JsonSyntaxException {
|
||||
return plugin.getGson().fromJson(json, RedisMessage.class);
|
||||
@@ -67,20 +85,23 @@ public class RedisMessage implements Adaptable {
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public UUID getTargetUuid() {
|
||||
return targetUuid;
|
||||
public Optional<UUID> getTargetUuid() {
|
||||
return Optional.ofNullable(targetUuid);
|
||||
}
|
||||
|
||||
public void setTargetUuid(@NotNull UUID targetUuid) {
|
||||
this.targetUuid = targetUuid;
|
||||
public Optional<OnlineUser> getTargetUser(@NotNull HuskSync plugin) {
|
||||
return getTargetUuid().flatMap(plugin::getOnlineUser);
|
||||
}
|
||||
|
||||
public boolean isTargetServer(@NotNull HuskSync plugin) {
|
||||
return targetServer != null && targetServer.equals(plugin.getServerName());
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
|
||||
UPDATE_USER_DATA,
|
||||
REQUEST_USER_DATA,
|
||||
RETURN_USER_DATA;
|
||||
RETURN_USER_DATA,
|
||||
CHECK_IN_PETITION;
|
||||
|
||||
@NotNull
|
||||
public String getMessageChannel(@NotNull String clusterId) {
|
||||
|
||||
@@ -185,7 +185,7 @@ public abstract class DataSyncer {
|
||||
final AtomicReference<Task.Repeating> task = new AtomicReference<>();
|
||||
final AtomicBoolean processing = new AtomicBoolean(false);
|
||||
final Runnable runnable = () -> {
|
||||
if (user.isOffline()) {
|
||||
if (user.cannotApplySnapshot()) {
|
||||
task.get().cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,10 @@ public class DelayDataSyncer extends DataSyncer {
|
||||
getRedis().setUserServerSwitch(onlineUser);
|
||||
saveData(
|
||||
onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
|
||||
(user, data) -> getRedis().setUserData(user, data)
|
||||
(user, data) -> {
|
||||
getRedis().setUserData(user, data);
|
||||
plugin.unlockPlayer(user.getUuid());
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class LockstepDataSyncer extends DataSyncer {
|
||||
|
||||
public LockstepDataSyncer(@NotNull HuskSync plugin) {
|
||||
@@ -44,9 +46,21 @@ public class LockstepDataSyncer extends DataSyncer {
|
||||
@Override
|
||||
public void syncApplyUserData(@NotNull OnlineUser user) {
|
||||
this.listenForRedisData(user, () -> {
|
||||
if (getRedis().getUserCheckedOut(user).isPresent()) {
|
||||
if (user.cannotApplySnapshot()) {
|
||||
plugin.debug("Not checking data state for user who has gone offline: %s".formatted(user.getName()));
|
||||
return false;
|
||||
}
|
||||
|
||||
// If they are checked out, ask the server to check them back in and return false
|
||||
final Optional<String> server = getRedis().getUserCheckedOut(user);
|
||||
if (server.isPresent() && !server.get().equals(plugin.getServerName())) {
|
||||
if (plugin.getSettings().getSynchronization().isCheckinPetitions()) {
|
||||
getRedis().petitionServerCheckin(server.get(), user);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// If they are checked in - or checked out on *this* server - we can apply their latest data
|
||||
getRedis().setUserCheckedOut(user, true);
|
||||
getRedis().getUserData(user).ifPresentOrElse(
|
||||
data -> user.applySnapshot(data, DataSnapshot.UpdateCause.SYNCHRONIZED),
|
||||
@@ -63,6 +77,7 @@ public class LockstepDataSyncer extends DataSyncer {
|
||||
(user, data) -> {
|
||||
getRedis().setUserData(user, data);
|
||||
getRedis().setUserCheckedOut(user, false);
|
||||
plugin.unlockPlayer(user.getUuid());
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@@ -43,11 +43,27 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the player has gone offline
|
||||
* Indicates if the player is offline
|
||||
*
|
||||
* @return {@code true} if the player has left the server; {@code false} otherwise
|
||||
* @deprecated use {@code hasDisconnected} instead
|
||||
*/
|
||||
public abstract boolean isOffline();
|
||||
@Deprecated(since = "3.8")
|
||||
public boolean isOffline() {
|
||||
return hasDisconnected();
|
||||
}
|
||||
|
||||
public abstract boolean hasDisconnected();
|
||||
|
||||
// Users cannot have snapshots applied if they have disconnected!
|
||||
@Override
|
||||
public boolean cannotApplySnapshot() {
|
||||
if (hasDisconnected()) {
|
||||
getPlugin().debug("[%s] Cannot apply snapshot as user is offline!".formatted(getName()));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
@@ -117,7 +133,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
||||
|
||||
|
||||
/**
|
||||
* Set a player's status from a {@link DataSnapshot}
|
||||
* Apply a {@link DataSnapshot} to a player, updating their data
|
||||
*
|
||||
* @param snapshot The {@link DataSnapshot} to set the player's status from
|
||||
* @param cause The {@link DataSnapshot.UpdateCause} of the snapshot
|
||||
@@ -125,14 +141,12 @@ 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(), getName(), cause.getDisplayName()
|
||||
));
|
||||
UserDataHolder.super.applySnapshot(
|
||||
event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin())
|
||||
);
|
||||
}
|
||||
getPlugin().debug(String.format("Attempting to apply snapshot (%s) to %s (cause: %s)",
|
||||
snapshot.getShortId(), getName(), cause.getDisplayName()
|
||||
));
|
||||
UserDataHolder.super.applySnapshot(
|
||||
event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -22,12 +22,13 @@ package net.william278.husksync.util;
|
||||
import de.exlll.configlib.Configuration;
|
||||
import de.exlll.configlib.YamlConfigurationProperties;
|
||||
import de.exlll.configlib.YamlConfigurationStore;
|
||||
import lombok.AllArgsConstructor;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static net.william278.husksync.config.ConfigProvider.YAML_CONFIGURATION_PROPERTIES;
|
||||
@@ -38,23 +39,22 @@ public interface CompatibilityChecker {
|
||||
|
||||
default void checkCompatibility() throws HuskSync.FailedToLoadException {
|
||||
final YamlConfigurationProperties p = YAML_CONFIGURATION_PROPERTIES.build();
|
||||
final Version compatible;
|
||||
final CompatibilityConfig compat;
|
||||
|
||||
// Load compatibility file
|
||||
try (InputStream input = getResource(COMPATIBILITY_FILE)) {
|
||||
final CompatibilityConfig compat = new YamlConfigurationStore<>(CompatibilityConfig.class, p).read(input);
|
||||
compatible = Objects.requireNonNull(compat.getCompatibleWith());
|
||||
compat = new YamlConfigurationStore<>(CompatibilityConfig.class, p).read(input);
|
||||
} catch (Throwable e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to load compatibility config, skipping check.", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check compatibility
|
||||
if (compatible.compareTo(getPlugin().getMinecraftVersion()) != 0) {
|
||||
if (!compat.isCompatibleWith(getPlugin().getMinecraftVersion())) {
|
||||
throw new HuskSync.FailedToLoadException("""
|
||||
Incompatible Minecraft version. This version of HuskSync is designed for Minecraft %s.
|
||||
Please download the correct version of HuskSync for your server's Minecraft version (%s)."""
|
||||
.formatted(compatible.toString(), getPlugin().getMinecraftVersion().toString()));
|
||||
.formatted(compat.minecraftVersionRange(), getPlugin().getMinecraftVersion().toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,11 +64,38 @@ public interface CompatibilityChecker {
|
||||
HuskSync getPlugin();
|
||||
|
||||
@Configuration
|
||||
record CompatibilityConfig(@NotNull String minecraftVersion) {
|
||||
record CompatibilityConfig(@NotNull String minecraftVersionRange) {
|
||||
|
||||
@NotNull
|
||||
public Version getCompatibleWith() {
|
||||
return Version.fromString(minecraftVersion);
|
||||
@AllArgsConstructor
|
||||
enum ExpressionType {
|
||||
GTE(">=", (v, s) -> v.compareTo(Version.fromString(s.substring(2))) >= 0),
|
||||
LTE("<=", (v, s) -> v.compareTo(Version.fromString(s.substring(2))) <= 0),
|
||||
GT(">", (v, s) -> v.compareTo(Version.fromString(s.substring(1))) > 0),
|
||||
LT("<", (v, s) -> v.compareTo(Version.fromString(s.substring(1))) < 0),
|
||||
NOT("!", (v, s) -> v.compareTo(Version.fromString(s.substring(1))) != 0),
|
||||
E("=", (v, s) -> v.compareTo(Version.fromString(s.substring(1))) == 0);
|
||||
|
||||
private final String match;
|
||||
private final BiFunction<Version, String, Boolean> function;
|
||||
|
||||
private static boolean check(@NotNull String versionRange, @NotNull Version mcVer) {
|
||||
boolean passes = true;
|
||||
versions:
|
||||
for (String exp : versionRange.split(" ")) {
|
||||
for (ExpressionType type : values()) {
|
||||
if (exp.trim().startsWith(type.match)) {
|
||||
passes = passes && type.function.apply(mcVer, exp.trim());
|
||||
continue versions;
|
||||
}
|
||||
}
|
||||
passes = passes && mcVer.compareTo(Version.fromString(exp.trim())) == 0;
|
||||
}
|
||||
return passes;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCompatibleWith(@NotNull Version version) {
|
||||
return ExpressionType.check(minecraftVersionRange, version);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface DataVersionSupplier {
|
||||
|
||||
int VERSION1_16_5 = 2586;
|
||||
int VERSION1_17_1 = 2730;
|
||||
int VERSION1_18_2 = 2975;
|
||||
int VERSION1_19_2 = 3120;
|
||||
int VERSION1_19_4 = 3337;
|
||||
int VERSION1_20_1 = 3465;
|
||||
int VERSION1_20_2 = 3578;
|
||||
int VERSION1_20_4 = 3700;
|
||||
int VERSION1_20_5 = 3837;
|
||||
int VERSION1_21_1 = 3955;
|
||||
int VERSION1_21_3 = 4082;
|
||||
int VERSION1_21_4 = 4189;
|
||||
int VERSION1_21_5 = 4323;
|
||||
int VERSION1_21_6 = 4435;
|
||||
int VERSION1_21_7 = 4438;
|
||||
int VERSION1_21_8 = 4438;
|
||||
int VERSION1_21_9 = 4554;
|
||||
int VERSION1_21_10 = 4556;
|
||||
int VERSION1_21_11 = 4671;
|
||||
|
||||
/**
|
||||
* Returns the data version for a Minecraft version
|
||||
*
|
||||
* @param mcVersion the Minecraft version
|
||||
* @return the data version int
|
||||
*/
|
||||
default int getDataVersion(@NotNull Version mcVersion) {
|
||||
return switch (mcVersion.toStringWithoutMetadata()) {
|
||||
case "1.16", "1.16.1", "1.16.2", "1.16.3", "1.16.4", "1.16.5" -> VERSION1_16_5;
|
||||
case "1.17", "1.17.1" -> VERSION1_17_1;
|
||||
case "1.18", "1.18.1", "1.18.2" -> VERSION1_18_2;
|
||||
case "1.19", "1.19.1", "1.19.2" -> VERSION1_19_2;
|
||||
case "1.19.4" -> VERSION1_19_4;
|
||||
case "1.20", "1.20.1" -> VERSION1_20_1;
|
||||
case "1.20.2" -> VERSION1_20_2;
|
||||
case "1.20.4" -> VERSION1_20_4;
|
||||
case "1.20.5", "1.20.6" -> VERSION1_20_5;
|
||||
case "1.21", "1.21.1" -> VERSION1_21_1;
|
||||
case "1.21.2", "1.21.3" -> VERSION1_21_3;
|
||||
case "1.21.4" -> VERSION1_21_4;
|
||||
case "1.21.5" -> VERSION1_21_5;
|
||||
case "1.21.6" -> VERSION1_21_6;
|
||||
case "1.21.7" -> VERSION1_21_7;
|
||||
case "1.21.8" -> VERSION1_21_8;
|
||||
case "1.21.9" -> VERSION1_21_9;
|
||||
case "1.21.10" -> VERSION1_21_10;
|
||||
case "1.21.11" -> VERSION1_21_11;
|
||||
default -> VERSION1_21_11; // Latest supported version
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
@@ -100,6 +100,8 @@ public interface DumpProvider {
|
||||
Map.entry("Redis Version", StatusLine.REDIS_VERSION.getValue(getPlugin())),
|
||||
Map.entry("Redis Latency", StatusLine.REDIS_LATENCY.getValue(getPlugin())),
|
||||
Map.entry("Redis Sentinel", StatusLine.USING_REDIS_SENTINEL.getValue(getPlugin())),
|
||||
Map.entry("Redis Database", StatusLine.REDIS_DATABASE.getValue(getPlugin())),
|
||||
Map.entry("Redis User", StatusLine.USING_REDIS_USER.getValue(getPlugin())),
|
||||
Map.entry("Redis Password", StatusLine.USING_REDIS_PASSWORD.getValue(getPlugin())),
|
||||
Map.entry("Redis SSL", StatusLine.REDIS_USING_SSL.getValue(getPlugin())),
|
||||
Map.entry("Redis Local", StatusLine.IS_REDIS_LOCAL.getValue(getPlugin()))
|
||||
|
||||
@@ -60,6 +60,10 @@ public enum StatusLine {
|
||||
USING_REDIS_SENTINEL(plugin -> getBoolean(
|
||||
!plugin.getSettings().getRedis().getSentinel().getMaster().isBlank()
|
||||
)),
|
||||
REDIS_DATABASE(plugin -> Component.text(plugin.getSettings().getRedis().getCredentials().getDatabase())),
|
||||
USING_REDIS_USER(plugin -> getBoolean(
|
||||
!plugin.getSettings().getRedis().getCredentials().getUser().isBlank()
|
||||
)),
|
||||
USING_REDIS_PASSWORD(plugin -> getBoolean(
|
||||
!plugin.getSettings().getRedis().getCredentials().getPassword().isBlank()
|
||||
)),
|
||||
|
||||
@@ -12,7 +12,7 @@ locales:
|
||||
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_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'
|
||||
@@ -22,8 +22,8 @@ locales:
|
||||
data_manager_advancements_preview_remaining: 'さらに %1% 件…'
|
||||
data_list_title: '[%1% のユーザーデータスナップショット:](#00fb9a) [(%4%件中](#00fb9a bold) [%2%-%3%件](#00fb9a)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7%2% のユーザーデータスナップショット&8⚡ %4% run_command=/husksync:userdata view %2% %3%) [%5%](#d8ff2b show_text=&7ピン留め:\n&8ピン留めされたスナップショットは自動的にローテーションしません。 run_command=/husksync:userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7バージョンタイムスタンプ:&7\n&8データの保存時期\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7保存理由:\n&8データが保存された理由 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7スナップショットサイズ:&7\n&8スナップショットの推定ファイルサイズ (単位:KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7%2% のユーザーデータスナップショット\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7ピン留め:\n&8ピン留めされたスナップショットは自動的にローテーションしません。 suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&無効なデータのスナップショット\n&#ff7e5e&クリックで消去\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_saved: '[%1% の現在のユーザーデータのスナップショットを正常に保存しました。](#00fb9a)'
|
||||
data_deleted: '[❌](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [の消去に成功しました。](#00fb9a)'
|
||||
data_restored: '[⏪](#00fb9a) [スナップショット](#00fb9a) [%3%](#00fb9a show_text=&7Version UUID:\n&8%4%) [から](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [の現在のユーザーデータの復元に成功しました。](#00fb9a)'
|
||||
data_pinned: '[※](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [のピン留めに成功しました。](#00fb9a)'
|
||||
@@ -37,35 +37,35 @@ locales:
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: 'disconnect'
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_save_command: 'save command'
|
||||
save_cause_dump_command: 'dump command'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
save_cause_disconnect: '切断'
|
||||
save_cause_world_save: 'ワールド保存'
|
||||
save_cause_death: '死亡'
|
||||
save_cause_server_shutdown: 'サーバーシャットダウン'
|
||||
save_cause_save_command: 'セーブコマンド'
|
||||
save_cause_dump_command: 'ダンプコマンド'
|
||||
save_cause_inventory_command: 'インベントリコマンド'
|
||||
save_cause_enderchest_command: 'エンダーチェストコマンド'
|
||||
save_cause_backup_restore: 'バックアップ復元'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB migration'
|
||||
save_cause_legacy_migration: 'legacy migration'
|
||||
save_cause_converted_from_v2: 'converted from v2'
|
||||
save_cause_mpdb_migration: 'MPDB移行'
|
||||
save_cause_legacy_migration: 'レガシー移行'
|
||||
save_cause_converted_from_v2: 'v2からの変換'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| HuskSyncの最新バージョンを実行しています(v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| HuskSyncの最新バージョンが更新されています: v%1% (実行中: v%2%).](#ff7e5e)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| HuskSyncの新バージョンが利用可能になりました: v%1% (実行中: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 設定ファイルとメッセージファイルを再読み込みしました。](#00fb9a)\n[⚠ すべてのサーバーで設定ファイルが最新であることを確認してください!](#00fb9a)\n[設定の変更を有効にするには再起動が必要です。](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||
error_invalid_syntax: '[Error:](#ff3300) [構文が正しくありません。使用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&クリックでサジェスト suggest_command=%1%)'
|
||||
error_invalid_player: '[Error:](#ff3300) [そのプレイヤーは見つかりませんでした](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[Error:](#ff3300) [このコマンドを実行する権限がありません](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [そのコマンドは%1%コンソールからのみ実行できます](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: そのコマンドはゲーム内でしか使えません。'
|
||||
error_no_data_to_display: '[Error:](#ff3300) [表示するユーザーデータが見つかりませんでした。](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Error:](#ff3300) [そのバージョンUUIDのユーザーデータが見つかりませんでした。](#ff7e5e)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| システムステータスレポート:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| システムダンプを作成しますか? 含まれる内容:](#00fb9a)\n[• 最新のサーバーログと HuskSync の設定ファイル](gray)\n[• 現在のプラグインシステムの状態](gray)\n[• Java と Minecraft サーバー環境の情報](gray)\n[• 現在インストールされている他のプラグイン一覧](gray)\n[確認するには、次を実行してください:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7クリックでダンプを準備 run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| システムステータスダンプを準備中です。お待ちください…](#00fb9a)'
|
||||
system_dump_ready: '[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) [そのコマンドは%1%コンソールからのみ実行できます](#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: 'プレーヤーのユーザーデータを表示・管理・復元する'
|
||||
userdata_command_description: 'プレイヤーのユーザーデータを表示・管理・復元する'
|
||||
inventory_command_description: 'プレイヤーのインベントリを閲覧・編集する'
|
||||
enderchest_command_description: 'プレイヤーのエンダーチェストを閲覧・編集する'
|
||||
|
||||
@@ -23,7 +23,7 @@ locales:
|
||||
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_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||
data_saved: '[已成功保存 %1% 的当前用户数据快照.](#00fb9a)'
|
||||
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%)'
|
||||
@@ -41,8 +41,8 @@ locales:
|
||||
save_cause_world_save: '保存世界'
|
||||
save_cause_death: '死亡'
|
||||
save_cause_server_shutdown: '服务器关闭'
|
||||
save_cause_save_command: 'save command'
|
||||
save_cause_dump_command: 'dump command'
|
||||
save_cause_save_command: '保存命令'
|
||||
save_cause_dump_command: '转储命令'
|
||||
save_cause_inventory_command: '背包命令'
|
||||
save_cause_enderchest_command: '末影箱命令'
|
||||
save_cause_backup_restore: '备份还原'
|
||||
@@ -54,9 +54,9 @@ locales:
|
||||
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)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| 准备系统转储? 这将包括:](#00fb9a)\n[• 您最新的服务器日志和 HuskSync 配置文件](gray)\n[• 当前插件系统状态信息](gray)\n[• 有关您的 Java 和 Minecraft 服务器环境的信息](gray)\n[• 其他当前安装的插件列表](gray)\n[要确认, 请执行命令:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7点击以准备转储 run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| 正在准备系统状态转储,请稍候...](#00fb9a)'
|
||||
system_dump_ready: '[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%)'
|
||||
|
||||
@@ -23,7 +23,7 @@ locales:
|
||||
data_list_title: '[%1% 的玩家資料快照:](#00fb9a) [(%2%-%3% 共](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7玩家資料快照 %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7已標記:\n&8標記的快照將不會自動輪換。 run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7版本時間戳:\n&8資料儲存時間\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7儲存原因:\n&8觸發儲存的原因 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7快照大小:\n&8快照的預估檔案大小(KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7玩家資料快照 %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7已標記:\n&8標記的快照將不會自動輪換。 suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&無效的資料快照\n&#ff7e5e&點擊刪除\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
|
||||
data_saved: '[✅ 成功儲存 %1% 的目前使用者資料快照。](#00fb9a)'
|
||||
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%)'
|
||||
@@ -41,8 +41,8 @@ locales:
|
||||
save_cause_world_save: '世界儲存'
|
||||
save_cause_death: '死亡'
|
||||
save_cause_server_shutdown: '伺服器關閉'
|
||||
save_cause_save_command: 'save command'
|
||||
save_cause_dump_command: 'dump command'
|
||||
save_cause_save_command: '儲存指令'
|
||||
save_cause_dump_command: '導出指令'
|
||||
save_cause_inventory_command: '背包指令'
|
||||
save_cause_enderchest_command: '終界箱指令'
|
||||
save_cause_backup_restore: '備份還原'
|
||||
@@ -54,9 +54,9 @@ locales:
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| 發現可用的新版本: v%1% (running: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 配置和語言文件已重新加載。](#00fb9a)\n[⚠ 確保所有伺服器上的配置文件都是最新的!](#00fb9a)\n[重啟後配置變更才會生效。](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| 系統狀態報告:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
|
||||
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
|
||||
system_dump_confirm: '[HuskSync](#00fb9a bold) [| 要產生系統狀態紀錄檔嗎?這將包含以下內容:](#00fb9a)\n[• 最近的伺服器日誌與 HuskSync 設定檔](gray)\n[• 插件目前的系統狀態資訊](gray)\n[• 有關您的 Java 與 Minecraft 伺服器環境的資訊](gray)\n[• 目前已安裝的其他插件清單](gray)\n[若要確認,請輸入:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7點擊以產生紀錄檔 run_command=/husksync dump confirm)'
|
||||
system_dump_started: '[HuskSync](#00fb9a bold) [| 正在產生系統狀態紀錄檔,請稍候…](#00fb9a)'
|
||||
system_dump_ready: '[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%)'
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import net.william278.desertwell.util.Version;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
@DisplayName("Compatibility Checker Tests")
|
||||
public class CompatibilityCheckerTests {
|
||||
|
||||
@ParameterizedTest(name = "Ver: {0}, Range: {1}")
|
||||
@DisplayName("Test Compatibility Checker")
|
||||
@CsvSource({
|
||||
"1.20.1, 1.21.1, false",
|
||||
"1.21.1, 1.20.1, false",
|
||||
"1.7.2, 1.21.5, false",
|
||||
"1.19.4, 1.21.1, false",
|
||||
"1.21.3, 1.21.3, true",
|
||||
"1.20.1, 1.20.1, true",
|
||||
"1.21.7, 1.21.7, true",
|
||||
"1.21.8, >=1.21.7, true",
|
||||
"1.21.8, >1.21.7, true",
|
||||
"1.0, <1.21.7, true",
|
||||
"1.17.1, !1.17.1, false",
|
||||
"1.21.7, '>=1.21.7 <=1.21.8', true",
|
||||
"1.21.8, '>=1.21.7 <=1.21.8', true",
|
||||
"1.21.5, '>=1.21.7 <=1.21.8', false",
|
||||
})
|
||||
public void testCompatibilityChecker(@NotNull String mcVer, @NotNull String range, boolean exp) {
|
||||
final Version version = Version.fromString(mcVer);
|
||||
Assertions.assertNotNull(version, "Version should not be null");
|
||||
|
||||
final CompatibilityChecker.CompatibilityConfig config = new CompatibilityChecker.CompatibilityConfig(range);
|
||||
Assertions.assertEquals(exp, config.isCompatibleWith(version), "Checker should return " + exp);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -52,7 +52,7 @@ Add the repository to your `pom.xml` as per below. You can alternatively specify
|
||||
</repository>
|
||||
</repositories>
|
||||
```
|
||||
Add the dependency to your `pom.xml` as per below. Replace `HUSKSYNC_VERSION` with the latest version of HuskSync (without the v): . and `MINECRAFT_VERSION` with the version of Minecraft you want to target (e.g. `1.20.1`). A correctly formed version target should look like: `3.7+1.20.1`. Omit the plus symbol and Minecraft version if you are targeting the `common` platform.
|
||||
Add the dependency to your `pom.xml` as per below. Replace `HUSKSYNC_VERSION` with the latest version of HuskSync (without the v): . and `MINECRAFT_VERSION` with the version of Minecraft you want to target (e.g. `1.21.10`). A correctly formed version target should look like: `3.7+1.21.10`. Omit the plus symbol and Minecraft version if you are targeting the `common` platform.
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>net.william278.husksync</groupId>
|
||||
@@ -75,7 +75,7 @@ allprojects {
|
||||
}
|
||||
}
|
||||
```
|
||||
Add the dependency as per below. Replace `HUSKSYNC_VERSION` with the latest version of HuskSync (without the v): . and `MINECRAFT_VERSION` with the version of Minecraft you want to target (e.g. `1.20.1`). A correctly formed version target should look like: `3.7+1.20.1`. Omit the plus symbol and Minecraft version if you are targeting the `common` platform.
|
||||
Add the dependency as per below. Replace `HUSKSYNC_VERSION` with the latest version of HuskSync (without the v): . and `MINECRAFT_VERSION` with the version of Minecraft you want to target (e.g. `1.21.10`). A correctly formed version target should look like: `3.7+1.21.10`. Omit the plus symbol and Minecraft version if you are targeting the `common` platform.
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
HuskSync supports the following versions of Minecraft. Since v3.7, you must download the correct version of HuskSync for your server:
|
||||
|
||||
| Minecraft | Latest HuskSync | Java Version | Platforms | Support Status |
|
||||
|:---------------:|:---------------:|:------------:|:--------------|:-----------------------------|
|
||||
| 1.21.4 | _latest_ | 21 | Paper, Fabric | ✅ **Active Release** |
|
||||
| 1.21.3 | 3.7.1 | 21 | Paper, Fabric | 🗃️ Archived (December 2024) |
|
||||
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **November 2025** (LTS) |
|
||||
| 1.20.6 | 3.6.8 | 17 | Paper | 🗃️ Archived (October 2024) |
|
||||
| 1.20.4 | 3.6.8 | 17 | Paper | 🗃️ Archived (July 2024) |
|
||||
| 1.20.1 | _latest_ | 17 | Paper, Fabric | ✅ **November 2025** (LTS) |
|
||||
| 1.17.1 - 1.19.4 | 3.6.8 | 17 | Paper | 🗃️ Archived |
|
||||
| 1.16.5 | 3.2.1 | 16 | Paper | 🗃️ Archived |
|
||||
| Minecraft | Latest HuskSync | Java Version | Platforms | Support Status |
|
||||
|:---------------:|:---------------:|:------------:|:--------------|:------------------------------|
|
||||
| 1.21.10 | _latest_ | 21 | Paper | ✅ **Active Release** |
|
||||
| 1.21.7/8 | _latest_ | 21 | Paper, Fabric | ✅ **August 2026** |
|
||||
| 1.21.6 | 3.8.5 | 21 | Paper | 🗃️ Archived (July 2025) |
|
||||
| 1.21.5 | _latest_ | 21 | Paper | ✅ **February 2026** (Non-LTS) |
|
||||
| 1.21.4 | _latest_ | 21 | Paper, Fabric | ✅ **February 2026** (Non-LTS) |
|
||||
| 1.21.3 | 3.7.1 | 21 | Paper, Fabric | 🗃️ Archived (December 2024) |
|
||||
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **May 2026** (LTS) |
|
||||
| 1.20.6 | 3.6.8 | 17 | Paper | 🗃️ Archived (October 2024) |
|
||||
| 1.20.4 | 3.6.8 | 17 | Paper | 🗃️ Archived (July 2024) |
|
||||
| 1.20.1 | 3.8.7 | 17 | Paper, Fabric | 🗃️ Archived (November 2024) |
|
||||
| 1.17.1 - 1.19.4 | 3.6.8 | 17 | Paper | 🗃️ Archived |
|
||||
| 1.16.5 | 3.2.1 | 16 | Paper | 🗃️ Archived |
|
||||
|
||||
HuskSync is primarily developed against the latest release. Old Minecraft versions are allocated a support channel based on popularity, mod support, etc:
|
||||
|
||||
@@ -31,5 +35,5 @@ This plugin does not support the following software-Minecraft version combinatio
|
||||
## Incompatible plugins / mods
|
||||
Please note the following plugins / mods can cause issues with HuskSync:
|
||||
|
||||
* Restart plugins / mods are not supported. These will cause [player data to not save correctly when your server restarts](troubleshooting#issues-with-player-data-going-out-of-sync-during-a-server-restart) due to the way these plugins utilise bash scripts. It's important to understand that restart plugins don't actually restart yur server, they just trigger some (often unstable) process-killing scripting logic to occur!
|
||||
* Restart plugins / mods are not supported. These will cause [player data to not save correctly when your server restarts](troubleshooting#issues-with-player-data-going-out-of-sync-during-a-server-restart) due to the way these plugins utilise bash scripts. It's important to understand that restart plugins don't actually restart your server, they just trigger some (often unstable) process-killing scripting logic to occur!
|
||||
* Combat logging plugins / mods are not supported. Some have built-in support for HuskSync and should work as expected, but for others you may wish to modify the [[Event Priorities]]
|
||||
@@ -65,10 +65,15 @@ database:
|
||||
user_data: husksync_user_data
|
||||
# Redis settings
|
||||
redis:
|
||||
# Specify the credentials of your Redis server here. Set "password" to '' if you don't have one
|
||||
# Specify the credentials of your Redis server here.
|
||||
# Set "user" to '' if you don't have one or would like to use the default user.
|
||||
# Set "password" to '' if you don't have one.
|
||||
credentials:
|
||||
host: localhost
|
||||
port: 6379
|
||||
# Only change the database if you know what you are doing. The default is 0.
|
||||
database: 0
|
||||
user: ''
|
||||
password: ''
|
||||
use_ssl: false
|
||||
# Options for if you're using Redis sentinel. Don't modify this unless you know what you're doing!
|
||||
|
||||
@@ -16,10 +16,15 @@ To configure Redis, navigate to your [`config.yml`](Config-File) file and modify
|
||||
```yaml
|
||||
# Redis settings
|
||||
redis:
|
||||
# Specify the credentials of your Redis server here. Set "password" to '' if you don't have one
|
||||
# Specify the credentials of your Redis server here.
|
||||
# Set "user" to '' if you don't have one or would like to use the default user.
|
||||
# Set "password" to '' if you don't have one.
|
||||
credentials:
|
||||
host: localhost
|
||||
port: 6379
|
||||
# Only change the database if you know what you are doing. The default is 0.
|
||||
database: 0
|
||||
user: ''
|
||||
password: ''
|
||||
use_ssl: false
|
||||
# Options for if you're using Redis sentinel. Don't modify this unless you know what you're doing!
|
||||
@@ -33,8 +38,9 @@ redis:
|
||||
</details>
|
||||
|
||||
### Credentials
|
||||
Enter the hostname, port, and default user password of your Redis server.
|
||||
Enter the hostname, port, user, and password of your Redis server.
|
||||
|
||||
If you don't have a Redis user, just use the default user password and leave the user field empty (`user: ''`).
|
||||
If your Redis default user doesn't have a password, leave the password field blank (`password: ''`') and the plugin will attempt to connect without a password.
|
||||
|
||||
### Default user password
|
||||
|
||||
@@ -26,6 +26,19 @@ If you are hosting your [[Redis]] server on the same node as your servers, you n
|
||||
### Database connection problems on Pterodactyl / Pelican
|
||||
If you have more than one [[Database]] server connected to your panel, you may need to set `useSSL=true` in the parameters.
|
||||
|
||||
### Unable to reset my server / wipe all player data
|
||||
The following steps are required to completely wipe all HuskSync data and prepare your server for a reset. HuskSync stores data in MySQL and caches it in Redis, so if you are experiencing players getting their items back when they shouldn't be it's because data wasn't cleared in one of the two.
|
||||
|
||||
- Turn OFF ALL Minecraft servers and proxy
|
||||
- Turn OFF your Redis server completely.
|
||||
- It MUST be completely offline.
|
||||
- Make sure data persistence is OFF and that it does not restore state following a reboot
|
||||
- Access your MySQL Database using MySQL Workbench or similar. DROP your `husksync` database, or at the very least DROP ALL husksync tables.
|
||||
- Double check you have done this
|
||||
- On ALL Minecraft servers, DELETE the `playerdata` and `advancement` data directories WITHIN EVERY WORLD folder
|
||||
- Deleting the world folders themselves works too if you are resetting these as well.
|
||||
- ONLY THEN can you finally re-start ALL servers
|
||||
|
||||
### Issues with player data going out of sync during a server restart
|
||||
This can happen due to the way in which your server restarts. If your server uses either:
|
||||
|
||||
@@ -38,4 +51,4 @@ These are **not compatible** with HuskSync in most cases due to the way in which
|
||||
* A cronjob to send a stop command / Power Action program stopcode, listen for the service to fully terminate, and then execute your startup command
|
||||
* For manual restarts, executing `/stop` and starting your server up with the startup command is totally fine.
|
||||
|
||||
It's not a great idea to use a plugin to handle restarts. Plugins are only able to operate when your server is turned on and must rely on scripts which don't safely shutdown servers when restarting.
|
||||
It's not a great idea to use a plugin to handle restarts. Plugins are only able to operate when your server is turned on and must rely on scripts which don't safely shutdown servers when restarting.
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
essential.defaults.loom.mappings=net.fabricmc:yarn:1.20.1+build.10:v2
|
||||
|
||||
fabric_loader_version=0.15.11
|
||||
fabric_api_version=0.92.2+1.20.1
|
||||
fabric_permissions_api_version=0.2-SNAPSHOT
|
||||
fabric_adventure_platform_version=5.9.0
|
||||
fabric_sgui_version=1.2.2+1.20
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"required": true,
|
||||
"minVersion": "0.8",
|
||||
"package": "net.william278.husksync.mixins",
|
||||
"compatibilityLevel": "JAVA_17",
|
||||
"server": [
|
||||
"ItemEntityMixin",
|
||||
"PlayerEntityMixin",
|
||||
"ServerPlayerEntityMixin",
|
||||
"ServerPlayNetworkHandlerMixin",
|
||||
"ServerWorldMixin"
|
||||
],
|
||||
"client": [],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
essential.defaults.loom.mappings=net.fabricmc:yarn:1.21.1+build.3:v2
|
||||
|
||||
minecraft_version_range=1.21.1
|
||||
|
||||
fabric_loader_version=0.16.10
|
||||
fabric_api_version=0.107.0+1.21.1
|
||||
fabric_permissions_api_version=0.3.1
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
essential.defaults.loom.mappings=net.fabricmc:yarn:1.21.4+build.4:v2
|
||||
|
||||
minecraft_version_range=1.21.4
|
||||
|
||||
fabric_loader_version=0.16.10
|
||||
fabric_api_version=0.115.0+1.21.4
|
||||
fabric_api_version=0.116.1+1.21.4
|
||||
fabric_permissions_api_version=0.3.3
|
||||
fabric_adventure_platform_version=6.2.0
|
||||
fabric_adventure_platform_version=6.3.0
|
||||
fabric_sgui_version=1.8.2+1.21.4
|
||||
9
fabric/1.21.5/gradle.properties
Normal file
9
fabric/1.21.5/gradle.properties
Normal file
@@ -0,0 +1,9 @@
|
||||
essential.defaults.loom.mappings=net.fabricmc:yarn:1.21.5+build.1:v2
|
||||
|
||||
minecraft_version_range=1.21.5
|
||||
|
||||
fabric_loader_version=0.16.14
|
||||
fabric_api_version=0.122.0+1.21.5
|
||||
fabric_permissions_api_version=0.3.3
|
||||
fabric_adventure_platform_version=6.4.0
|
||||
fabric_sgui_version=1.9.0+1.21.5
|
||||
9
fabric/1.21.8/gradle.properties
Normal file
9
fabric/1.21.8/gradle.properties
Normal file
@@ -0,0 +1,9 @@
|
||||
essential.defaults.loom.mappings=net.fabricmc:yarn:1.21.8+build.1:v2
|
||||
|
||||
minecraft_version_range=>=1.21.7 <=1.21.8
|
||||
|
||||
fabric_loader_version=0.17.2
|
||||
fabric_api_version=0.131.0+1.21.8
|
||||
fabric_permissions_api_version=0.4.1
|
||||
fabric_adventure_platform_version=6.6.0
|
||||
fabric_sgui_version=1.10.0+1.21.6
|
||||
@@ -14,22 +14,29 @@ dependencies {
|
||||
modImplementation include("net.kyori:adventure-platform-fabric:${fabric_adventure_platform_version}")
|
||||
modImplementation include("me.lucko:fabric-permissions-api:${fabric_permissions_api_version}")
|
||||
modImplementation include("eu.pb4:sgui:${fabric_sgui_version}")
|
||||
modImplementation include("net.william278.uniform:uniform-fabric:1.3.1+${project.name}")
|
||||
modImplementation include("net.william278.toilet:toilet-fabric:1.0.12+${project.name}")
|
||||
modImplementation include("net.william278.uniform:uniform-fabric:1.3.9+${project.name}")
|
||||
modImplementation include("net.william278.toilet:toilet-fabric:1.0.16+${project.name}")
|
||||
modImplementation "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}"
|
||||
|
||||
// Manually include config deps due to the way including api deps works
|
||||
implementation include("de.exlll:configlib-core:4.6.3")
|
||||
implementation include("org.snakeyaml:snakeyaml-engine:2.10")
|
||||
implementation include('org.apache.commons:commons-pool2:2.12.1')
|
||||
|
||||
// Include driver deps due to no runtime dep loading support
|
||||
implementation include("com.mysql:mysql-connector-j:$mysql_driver_version")
|
||||
implementation include("org.postgresql:postgresql:$postgres_driver_version")
|
||||
implementation include("org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version")
|
||||
implementation include("redis.clients:jedis:$jedis_version")
|
||||
implementation include("org.xerial.snappy:snappy-java:$snappy_version")
|
||||
implementation include("redis.clients:jedis:$jedis_version")
|
||||
implementation include("redis.clients.authentication:redis-authx-core:0.1.1-beta2") // Redis dep
|
||||
implementation include('org.apache.commons:commons-pool2:2.12.1') // Redis dep
|
||||
|
||||
compileOnly 'net.william278:DesertWell:2.0.4'
|
||||
compileOnly 'org.jetbrains:annotations:26.0.2'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.36'
|
||||
compileOnly 'org.jetbrains:annotations:26.0.2-1'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.42'
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.36'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.42'
|
||||
|
||||
implementation include(project(path: ":common"))
|
||||
project(":common").configurations.api.dependencies.each { dependency ->
|
||||
@@ -42,7 +49,8 @@ processResources {
|
||||
expand([
|
||||
version: version,
|
||||
fabric_loader_version: fabric_loader_version,
|
||||
fabric_minecraft_version: project.name
|
||||
fabric_minecraft_version: project.name,
|
||||
minecraft_version_range: minecraft_version_range
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.21.4
|
||||
1.21.8
|
||||
@@ -3,11 +3,13 @@ plugins {
|
||||
}
|
||||
|
||||
preprocess {
|
||||
def fabric12108 = createNode("1.21.8", 12108, "yarn")
|
||||
def fabric12105 = createNode("1.21.5", 12105, "yarn")
|
||||
def fabric12104 = createNode("1.21.4", 12104, "yarn")
|
||||
def fabric12101 = createNode("1.21.1", 12101, "yarn")
|
||||
def fabric12001 = createNode("1.20.1", 12001, "yarn")
|
||||
|
||||
strictExtraMappings.set(true)
|
||||
fabric12101.link(fabric12104, null)
|
||||
fabric12001.link(fabric12104, null)
|
||||
fabric12105.link(fabric12108, null)
|
||||
fabric12104.link(fabric12108, null)
|
||||
fabric12101.link(fabric12108, null)
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import net.fabricmc.api.DedicatedServerModInitializer;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.fabricmc.loader.api.ModContainer;
|
||||
//#if MC==12104
|
||||
//#if MC>=12104
|
||||
import net.kyori.adventure.platform.modcommon.MinecraftServerAudiences;
|
||||
//#else
|
||||
//$$ import net.kyori.adventure.platform.fabric.FabricServerAudiences;
|
||||
@@ -54,7 +54,6 @@ import net.william278.husksync.database.PostgresDatabase;
|
||||
import net.william278.husksync.event.FabricEventDispatcher;
|
||||
import net.william278.husksync.event.ModLoadedCallback;
|
||||
import net.william278.husksync.hook.PlanHook;
|
||||
import net.william278.husksync.listener.EventListener;
|
||||
import net.william278.husksync.listener.FabricEventListener;
|
||||
import net.william278.husksync.listener.LockedHandler;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
@@ -89,26 +88,12 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
|
||||
|
||||
private static final String PLATFORM_TYPE_ID = "fabric";
|
||||
|
||||
private static final int VERSION1_16_5 = 2586;
|
||||
private static final int VERSION1_17_1 = 2730;
|
||||
private static final int VERSION1_18_2 = 2975;
|
||||
private static final int VERSION1_19_2 = 3120;
|
||||
private static final int VERSION1_19_4 = 3337;
|
||||
private static final int VERSION1_20_1 = 3465;
|
||||
private static final int VERSION1_20_2 = 3578;
|
||||
private static final int VERSION1_20_4 = 3700;
|
||||
private static final int VERSION1_20_5 = 3837;
|
||||
private static final int VERSION1_21_1 = 3955;
|
||||
private static final int VERSION1_21_3 = 4082;
|
||||
private static final int VERSION1_21_4 = 4189; // Current
|
||||
|
||||
private final TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap(
|
||||
SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR
|
||||
);
|
||||
private final HashMap<Identifier, Serializer<? extends Data>> serializers = Maps.newHashMap();
|
||||
private final Map<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap();
|
||||
private final Map<String, Boolean> permissions = Maps.newHashMap();
|
||||
private final List<Migrator> availableMigrators = Lists.newArrayList();
|
||||
private final Set<UUID> lockedPlayers = Sets.newConcurrentHashSet();
|
||||
private final Set<UUID> disconnectingPlayers = Sets.newConcurrentHashSet();
|
||||
private final Map<UUID, FabricUser> playerMap = Maps.newConcurrentMap();
|
||||
|
||||
private Logger logger;
|
||||
@@ -116,7 +101,7 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
|
||||
private MinecraftServer minecraftServer;
|
||||
private boolean disabling;
|
||||
private Gson gson;
|
||||
//#if MC==12104
|
||||
//#if MC>=12104
|
||||
private MinecraftServerAudiences audiences;
|
||||
//#else
|
||||
//$$ private FabricServerAudiences audiences;
|
||||
@@ -167,7 +152,7 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
|
||||
|
||||
private void onEnable() {
|
||||
// Audiences
|
||||
//#if MC==12104
|
||||
//#if MC>=12104
|
||||
this.audiences = MinecraftServerAudiences.of(minecraftServer);
|
||||
//#else
|
||||
//$$ this.audiences = FabricServerAudiences.of(minecraftServer);
|
||||
@@ -373,30 +358,6 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
|
||||
return Version.fromString(minecraftServer.getVersion());
|
||||
}
|
||||
|
||||
public int getDataVersion(@NotNull Version mcVersion) {
|
||||
return switch (mcVersion.toStringWithoutMetadata()) {
|
||||
case "1.16", "1.16.1", "1.16.2", "1.16.3", "1.16.4", "1.16.5" -> VERSION1_16_5;
|
||||
case "1.17", "1.17.1" -> VERSION1_17_1;
|
||||
case "1.18", "1.18.1", "1.18.2" -> VERSION1_18_2;
|
||||
case "1.19", "1.19.1", "1.19.2" -> VERSION1_19_2;
|
||||
case "1.19.4" -> VERSION1_19_4;
|
||||
case "1.20", "1.20.1" -> VERSION1_20_1;
|
||||
case "1.20.2" -> VERSION1_20_2;
|
||||
case "1.20.4" -> VERSION1_20_4;
|
||||
case "1.20.5", "1.20.6" -> VERSION1_20_5;
|
||||
case "1.21", "1.21.1" -> VERSION1_21_1;
|
||||
case "1.21.2", "1.21.3" -> VERSION1_21_3;
|
||||
case "1.21.4" -> VERSION1_21_4;
|
||||
//#if MC==12104
|
||||
default -> VERSION1_21_4;
|
||||
//#elseif MC==12101
|
||||
//$$ default -> VERSION1_21_1;
|
||||
//#elseif MC==12001
|
||||
//$$ default -> VERSION1_20_1;
|
||||
//#endif
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getPlatformType() {
|
||||
|
||||
@@ -26,12 +26,7 @@ import com.google.gson.annotations.SerializedName;
|
||||
import lombok.*;
|
||||
import net.minecraft.advancement.AdvancementProgress;
|
||||
import net.minecraft.advancement.PlayerAdvancementTracker;
|
||||
//#if MC==12001
|
||||
//$$ import net.minecraft.enchantment.EnchantmentHelper;
|
||||
//$$ import net.minecraft.nbt.NbtCompound;
|
||||
//#else
|
||||
import net.minecraft.component.DataComponentTypes;
|
||||
//#endif
|
||||
import net.minecraft.entity.attribute.EntityAttribute;
|
||||
import net.minecraft.entity.attribute.EntityAttributeInstance;
|
||||
import net.minecraft.entity.attribute.EntityAttributeModifier;
|
||||
@@ -54,7 +49,7 @@ import net.william278.husksync.FabricHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.config.Settings.SynchronizationSettings.AttributeSettings;
|
||||
//#if MC==12104
|
||||
//#if MC>=12104
|
||||
import net.william278.husksync.mixins.HungerManagerMixin;
|
||||
//#endif
|
||||
import net.william278.husksync.user.FabricUser;
|
||||
@@ -95,16 +90,6 @@ public abstract class FabricData implements Data {
|
||||
stack.getItem().toString(),
|
||||
stack.getCount(),
|
||||
stack.getName().getString(),
|
||||
//#if MC==12001
|
||||
//$$ Optional.ofNullable(stack.getSubNbt(ItemStack.DISPLAY_KEY))
|
||||
//$$ .flatMap(display -> Optional.ofNullable(display.get(ItemStack.LORE_KEY))
|
||||
//$$ .map(lore -> ((List<String>) lore).stream().toList()))
|
||||
//$$ .orElse(null),
|
||||
//$$ stack.getEnchantments().stream()
|
||||
//$$ .map(element -> EnchantmentHelper.getIdFromNbt((NbtCompound) element))
|
||||
//$$ .filter(Objects::nonNull).map(Identifier::toString)
|
||||
//$$ .toList()
|
||||
//#else
|
||||
stack.getComponents().get(DataComponentTypes.LORE).lines().stream()
|
||||
.map(Text::getString)
|
||||
.toList(),
|
||||
@@ -112,7 +97,6 @@ public abstract class FabricData implements Data {
|
||||
.map(RegistryEntry::getIdAsString)
|
||||
.filter(Objects::nonNull)
|
||||
.toList()
|
||||
//#endif
|
||||
) : null)
|
||||
.toArray(Stack[]::new);
|
||||
}
|
||||
@@ -178,7 +162,7 @@ public abstract class FabricData implements Data {
|
||||
@Override
|
||||
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
|
||||
final ServerPlayerEntity player = user.getPlayer();
|
||||
//#if MC==12104
|
||||
//#if MC>=12104
|
||||
player.playerScreenHandler.getCraftingInput().clear();
|
||||
//#else
|
||||
//$$ player.playerScreenHandler.clearCraftingSlots();
|
||||
@@ -188,7 +172,11 @@ public abstract class FabricData implements Data {
|
||||
for (int slot = 0; slot < player.getInventory().size(); slot++) {
|
||||
player.getInventory().setStack(slot, items[slot] == null ? ItemStack.EMPTY : items[slot]);
|
||||
}
|
||||
player.getInventory().selectedSlot = heldItemSlot;
|
||||
//#if MC<12105
|
||||
//$$ player.getInventory().selectedSlot = heldItemSlot;
|
||||
//#else
|
||||
player.getInventory().setSelectedSlot(heldItemSlot);
|
||||
//#endif
|
||||
player.playerScreenHandler.sendContentUpdates();
|
||||
player.getInventory().updateItems();
|
||||
}
|
||||
@@ -270,11 +258,7 @@ public abstract class FabricData implements Data {
|
||||
.map(effect -> {
|
||||
final StatusEffect type = matchEffectType(effect.type());
|
||||
return type != null ? new StatusEffectInstance(
|
||||
//#if MC==12001
|
||||
//$$ type,
|
||||
//#else
|
||||
RegistryEntry.of(type),
|
||||
//#endif
|
||||
effect.duration(),
|
||||
effect.amplifier(),
|
||||
effect.isAmbient(),
|
||||
@@ -296,16 +280,10 @@ public abstract class FabricData implements Data {
|
||||
@Override
|
||||
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
|
||||
final ServerPlayerEntity player = user.getPlayer();
|
||||
//#if MC==12001
|
||||
//$$ final List<StatusEffect> effectsToRemove = player.getActiveStatusEffects().entrySet().stream()
|
||||
//$$ .filter(e -> !e.getValue().isAmbient()).map(Map.Entry::getKey).toList();
|
||||
//$$ effectsToRemove.forEach(player::removeStatusEffect);
|
||||
//#else
|
||||
//todo ambient check
|
||||
final List<StatusEffect> effectsToRemove = new ArrayList<>(player.getActiveStatusEffects().keySet().stream()
|
||||
.map(RegistryEntry::value).toList());
|
||||
effectsToRemove.forEach(effect -> player.removeStatusEffect(RegistryEntry.of(effect)));
|
||||
//#endif
|
||||
getEffects().forEach(player::addStatusEffect);
|
||||
}
|
||||
|
||||
@@ -315,11 +293,7 @@ public abstract class FabricData implements Data {
|
||||
public List<Effect> getActiveEffects() {
|
||||
return effects.stream()
|
||||
.map(potionEffect -> {
|
||||
//#if MC==12001
|
||||
//$$ final String key = getEffectId(potionEffect.getEffectType());
|
||||
//#else
|
||||
final String key = getEffectId(potionEffect.getEffectType().value());
|
||||
//#endif
|
||||
return key != null ? new Effect(
|
||||
key,
|
||||
potionEffect.getAmplifier(),
|
||||
@@ -353,21 +327,13 @@ public abstract class FabricData implements Data {
|
||||
|
||||
advancementProgress.getObtainedCriteria().forEach((criteria) -> awardedCriteria.put(
|
||||
criteria,
|
||||
//#if MC==12001
|
||||
//$$ advancementProgress.getEarliestProgressObtainDate()
|
||||
//#else
|
||||
Date.from(advancementProgress.getEarliestProgressObtainDate())
|
||||
//#endif
|
||||
));
|
||||
|
||||
// Only save the advancement if criteria has been completed
|
||||
if (!awardedCriteria.isEmpty()) {
|
||||
advancements.add(Advancement.adapt(
|
||||
//#if MC==12001
|
||||
//$$ advancementEntry.getId().toString(),
|
||||
//#else
|
||||
advancementEntry.id().asString(),
|
||||
//#endif
|
||||
awardedCriteria
|
||||
));
|
||||
}
|
||||
@@ -388,11 +354,7 @@ public abstract class FabricData implements Data {
|
||||
final AdvancementProgress progress = player.getAdvancementTracker().getProgress(advancementEntry);
|
||||
final Optional<Advancement> record = completed.stream()
|
||||
.filter(r -> r.getKey().equals(
|
||||
//#if MC==12001
|
||||
//$$ advancementEntry.getId().toString()
|
||||
//#else
|
||||
advancementEntry.id().asString()
|
||||
//#endif
|
||||
))
|
||||
.findFirst();
|
||||
if (record.isEmpty()) {
|
||||
@@ -410,11 +372,7 @@ public abstract class FabricData implements Data {
|
||||
}
|
||||
|
||||
private void setAdvancement(@NotNull FabricHuskSync plugin,
|
||||
//#if MC==12001
|
||||
//$$ @NotNull net.minecraft.advancement.Advancement advancementEntry,
|
||||
//#else
|
||||
@NotNull net.minecraft.advancement.AdvancementEntry advancementEntry,
|
||||
//#endif
|
||||
@NotNull ServerPlayerEntity player,
|
||||
@NotNull FabricUser user,
|
||||
@NotNull List<String> toAward,
|
||||
@@ -441,11 +399,7 @@ public abstract class FabricData implements Data {
|
||||
// Performs a consuming function for every advancement entry registered on the server
|
||||
private static void forEachAdvancementEntry(
|
||||
@NotNull MinecraftServer server,
|
||||
//#if MC==12001
|
||||
//$$ @NotNull ThrowingConsumer<net.minecraft.advancement.Advancement> con
|
||||
//#else
|
||||
@NotNull ThrowingConsumer<net.minecraft.advancement.AdvancementEntry> con
|
||||
//#endif
|
||||
) {
|
||||
server.getAdvancementLoader().getAdvancements().forEach(con);
|
||||
}
|
||||
@@ -478,11 +432,7 @@ public abstract class FabricData implements Data {
|
||||
|
||||
@NotNull
|
||||
public static FabricData.Location adapt(@NotNull ServerPlayerEntity player) {
|
||||
//#if MC==12001
|
||||
//$$ final String worldName = player.getWorld().getDimensionKey().getValue().toString();
|
||||
//#else
|
||||
final String worldName = player.getWorld().getDimensionEntry().getIdAsString();
|
||||
//#endif
|
||||
return from(
|
||||
player.getX(),
|
||||
player.getY(),
|
||||
@@ -513,7 +463,7 @@ public abstract class FabricData implements Data {
|
||||
// Apply teleport
|
||||
try {
|
||||
player.dismountVehicle();
|
||||
//#if MC==12104
|
||||
//#if MC>=12104
|
||||
player.teleport(target, x, y, z, Set.of(), yaw, pitch, true);
|
||||
//#else
|
||||
//$$ player.teleport(target, x, y, z, yaw, pitch);
|
||||
@@ -552,13 +502,13 @@ public abstract class FabricData implements Data {
|
||||
// This is necessary to prevent weird re-mappings with Registry#getKey()
|
||||
//#if MC>0
|
||||
//$$ final Registry<?> registry = stat.getValue().getRegistry();
|
||||
//$$ final String registryId = registry.getKey().getValue().toString();
|
||||
//$$ final String registryId = registry.getKey().getValue().value();
|
||||
//$$ if (registryId.equals("custom_stat")) {
|
||||
//$$ return;
|
||||
//$$ }
|
||||
//#else
|
||||
final Registry<?> registry = stat.getValue().getRegistry();
|
||||
final String registryId = registry.getKey().getValue().toString();
|
||||
final String registryId = registry.getKey().getValue().value();
|
||||
if (registryId.equals("custom_stat")) {
|
||||
return;
|
||||
}
|
||||
@@ -655,21 +605,6 @@ public abstract class FabricData implements Data {
|
||||
final List<Attribute> attributes = Lists.newArrayList();
|
||||
final AttributeSettings settings = plugin.getSettings().getSynchronization().getAttributes();
|
||||
Registries.ATTRIBUTE.forEach(id -> {
|
||||
//#if MC==12001
|
||||
//$$ final EntityAttributeInstance instance = player.getAttributeInstance(id);
|
||||
//$$ final Identifier key = Registries.ATTRIBUTE.getId(id);
|
||||
//$$ if (instance == null || key == null || settings.isIgnoredAttribute(key.asString())) {
|
||||
//$$ return;
|
||||
//$$ }
|
||||
//$$ final Set<Modifier> modifiers = Sets.newHashSet();
|
||||
//$$ instance.getModifiers().forEach(modifier -> modifiers.add(new Modifier(
|
||||
//$$ modifier.getId(),
|
||||
//$$ modifier.getName(),
|
||||
//$$ modifier.getValue(),
|
||||
//$$ modifier.getOperation().getId(),
|
||||
//$$ -1
|
||||
//$$ )));
|
||||
//#else
|
||||
final EntityAttributeInstance instance = player.getAttributeInstance(RegistryEntry.of(id));
|
||||
final Identifier key = Registries.ATTRIBUTE.getId(id);
|
||||
if (instance == null || key == null || settings.isIgnoredAttribute(key.asString())) {
|
||||
@@ -682,7 +617,6 @@ public abstract class FabricData implements Data {
|
||||
modifier.operation().getId(),
|
||||
Modifier.ANY_EQUIPMENT_SLOT_GROUP
|
||||
)));
|
||||
//#endif
|
||||
attributes.add(new Attribute(
|
||||
key.toString(),
|
||||
instance.getBaseValue(),
|
||||
@@ -715,11 +649,7 @@ public abstract class FabricData implements Data {
|
||||
return;
|
||||
}
|
||||
applyAttribute(
|
||||
//#if MC==12001
|
||||
//$$ user.getPlayer().getAttributeInstance(id),
|
||||
//#else
|
||||
user.getPlayer().getAttributeInstance(RegistryEntry.of(id)),
|
||||
//#endif
|
||||
getAttribute(id).orElse(null)
|
||||
);
|
||||
});
|
||||
@@ -804,7 +734,7 @@ public abstract class FabricData implements Data {
|
||||
@NotNull
|
||||
public static FabricData.Hunger adapt(@NotNull ServerPlayerEntity player) {
|
||||
final HungerManager hunger = player.getHungerManager();
|
||||
//#if MC==12104
|
||||
//#if MC>=12104
|
||||
float exhaustion = ((HungerManagerMixin) hunger).getExhaustion();
|
||||
//#else
|
||||
//$$ float exhaustion = hunger.getExhaustion();
|
||||
@@ -823,7 +753,7 @@ public abstract class FabricData implements Data {
|
||||
final HungerManager hunger = player.getHungerManager();
|
||||
hunger.setFoodLevel(foodLevel);
|
||||
hunger.setSaturationLevel(saturation);
|
||||
//#if MC==12104
|
||||
//#if MC>=12104
|
||||
((HungerManagerMixin) hunger).setExhaustion(exhaustion);
|
||||
//#else
|
||||
//$$ hunger.setExhaustion(exhaustion);
|
||||
@@ -888,7 +818,11 @@ public abstract class FabricData implements Data {
|
||||
|
||||
@Override
|
||||
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
|
||||
user.getPlayer().changeGameMode(net.minecraft.world.GameMode.byName(gameMode));
|
||||
//#if MC<12105
|
||||
//$$ user.getPlayer().changeGameMode(net.minecraft.world.GameMode.byName(gameMode));
|
||||
//#else
|
||||
user.getPlayer().changeGameMode(net.minecraft.world.GameMode.byId(gameMode));
|
||||
//#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,9 +26,6 @@ import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import net.minecraft.datafixer.TypeReferences;
|
||||
import net.minecraft.item.ItemStack;
|
||||
//#if MC==12001
|
||||
//$$ import net.minecraft.nbt.NbtCompound;
|
||||
//#endif
|
||||
import net.minecraft.nbt.*;
|
||||
import net.minecraft.registry.DynamicRegistryManager;
|
||||
import net.william278.desertwell.util.Version;
|
||||
@@ -39,6 +36,7 @@ import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -75,17 +73,29 @@ public abstract class FabricSerializer {
|
||||
final FabricHuskSync plugin = (FabricHuskSync) getPlugin();
|
||||
final NbtCompound root;
|
||||
try {
|
||||
root = StringNbtReader.parse(serialized);
|
||||
//#if MC<12105
|
||||
//$$ root = StringNbtReader.parse(serialized);
|
||||
//#else
|
||||
root = StringNbtReader.readCompound(serialized);
|
||||
//#endif
|
||||
} catch (Throwable e) {
|
||||
throw new DeserializationException("Failed to read item NBT from string (%s)".formatted(serialized), e);
|
||||
}
|
||||
|
||||
// Deserialize the inventory data
|
||||
final NbtCompound items = root.contains(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null;
|
||||
//#if MC<12105
|
||||
//$$ final NbtCompound items = root.contains(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null;
|
||||
//$$ return FabricData.Items.Inventory.from(
|
||||
//$$ items != null ? getItems(items, dataMcVersion, plugin) : new ItemStack[INVENTORY_SLOT_COUNT],
|
||||
//$$ root.contains(HELD_ITEM_SLOT_TAG) ? root.getInt(HELD_ITEM_SLOT_TAG) : 0
|
||||
//$$ );
|
||||
//#else
|
||||
final NbtCompound items = root.contains(ITEMS_TAG) ? root.getCompoundOrEmpty(ITEMS_TAG) : null;
|
||||
return FabricData.Items.Inventory.from(
|
||||
items != null ? getItems(items, dataMcVersion, plugin) : new ItemStack[INVENTORY_SLOT_COUNT],
|
||||
root.contains(HELD_ITEM_SLOT_TAG) ? root.getInt(HELD_ITEM_SLOT_TAG) : 0
|
||||
items != null ? getItems(items, dataMcVersion, plugin) : new ItemStack[INVENTORY_SLOT_COUNT],
|
||||
root.getInt(HELD_ITEM_SLOT_TAG, 0)
|
||||
);
|
||||
//#endif
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -120,7 +130,11 @@ public abstract class FabricSerializer {
|
||||
throws DeserializationException {
|
||||
final FabricHuskSync plugin = (FabricHuskSync) getPlugin();
|
||||
try {
|
||||
final NbtCompound items = StringNbtReader.parse(serialized);
|
||||
//#if MC<12105
|
||||
//$$ final NbtCompound items = StringNbtReader.parse(serialized);
|
||||
//#else
|
||||
final NbtCompound items = StringNbtReader.readCompound(serialized);
|
||||
//#endif
|
||||
return FabricData.Items.EnderChest.adapt(getItems(items, dataMcVersion, plugin));
|
||||
} catch (Throwable e) {
|
||||
throw new DeserializationException("Failed to read item NBT from string (%s)".formatted(serialized), e);
|
||||
@@ -152,14 +166,26 @@ public abstract class FabricSerializer {
|
||||
return upgradeItemStacks(tag, mcVersion, plugin);
|
||||
}
|
||||
|
||||
final ItemStack[] contents = new ItemStack[tag.getInt("size")];
|
||||
final NbtList itemList = tag.getList("items", NbtElement.COMPOUND_TYPE);
|
||||
final DynamicRegistryManager registryManager = plugin.getMinecraftServer().getRegistryManager();
|
||||
//#if MC<12105
|
||||
//$$ final ItemStack[] contents = new ItemStack[tag.getInt("size")];
|
||||
//$$ final NbtList itemList = tag.getList("items", NbtElement.COMPOUND_TYPE);
|
||||
//$$ itemList.forEach(element -> {
|
||||
//$$ final NbtCompound compound = (NbtCompound) element;
|
||||
//$$ contents[compound.getInt("Slot")] = decodeNbt(element, registryManager);
|
||||
//$$ });
|
||||
//#else
|
||||
final ItemStack[] contents = new ItemStack[tag.getInt("size", 0)];
|
||||
final NbtList itemList = tag.getListOrEmpty("items");
|
||||
itemList.forEach(element -> {
|
||||
final NbtCompound compound = (NbtCompound) element;
|
||||
contents[compound.getInt("Slot")] = decodeNbt(element, registryManager);
|
||||
int i = compound.getInt("Slot", -1);
|
||||
if (i >= 0) {
|
||||
contents[i] = decodeNbt(element, registryManager);
|
||||
}
|
||||
});
|
||||
plugin.debug(Arrays.toString(contents));
|
||||
//#endif
|
||||
|
||||
return contents;
|
||||
} catch (Throwable e) {
|
||||
throw new Serializer.DeserializationException("Failed to read item NBT string (%s)".formatted(tag), e);
|
||||
@@ -173,16 +199,24 @@ public abstract class FabricSerializer {
|
||||
container.putInt("size", items.length);
|
||||
final NbtList itemList = new NbtList();
|
||||
final DynamicRegistryManager registryManager = plugin.getMinecraftServer().getRegistryManager();
|
||||
final List<ItemStack> skipped = new ArrayList<>();
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
final ItemStack item = items[i];
|
||||
if (item == null || item.isEmpty()) {
|
||||
if (item == null || item.isEmpty() || item.getCount() < 1 || item.getCount() > 99) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final NbtCompound entry = encodeNbt(item, registryManager);
|
||||
if (entry == null) {
|
||||
skipped.add(item);
|
||||
continue;
|
||||
}
|
||||
entry.putInt("Slot", i);
|
||||
itemList.add(entry);
|
||||
}
|
||||
if (!skipped.isEmpty()) {
|
||||
plugin.debug("Skipped serializing items in array: %s".formatted(Arrays.toString(skipped.toArray())));
|
||||
}
|
||||
container.put(ITEMS_TAG, itemList);
|
||||
return container;
|
||||
}
|
||||
@@ -190,19 +224,37 @@ public abstract class FabricSerializer {
|
||||
@NotNull
|
||||
private ItemStack @NotNull [] upgradeItemStacks(@NotNull NbtCompound items, @NotNull Version mcVersion,
|
||||
@NotNull FabricHuskSync plugin) {
|
||||
final int size = items.getInt("size");
|
||||
final NbtList list = items.getList("items", NbtElement.COMPOUND_TYPE);
|
||||
//#if MC<12105
|
||||
//$$ final int size = items.getInt("size");
|
||||
//$$ final NbtList list = items.getList("items", NbtElement.COMPOUND_TYPE);
|
||||
//$$ final ItemStack[] itemStacks = new ItemStack[size];
|
||||
//$$ final DynamicRegistryManager registryManager = plugin.getMinecraftServer().getRegistryManager();
|
||||
//$$ Arrays.fill(itemStacks, ItemStack.EMPTY);
|
||||
//$$ for (int i = 0; i < size; i++) {
|
||||
//$$ if (list.getCompound(i) == null) {
|
||||
//$$ continue;
|
||||
//$$ }
|
||||
//$$ final NbtCompound compound = list.getCompound(i);
|
||||
//$$ final int slot = compound.getInt("Slot");
|
||||
//$$ itemStacks[slot] = decodeNbt(upgradeItemData(list.getCompound(i), mcVersion, plugin), registryManager);
|
||||
//$$ }
|
||||
//#else
|
||||
final int size = items.getInt("size", 0);
|
||||
final NbtList list = items.getListOrEmpty("items");
|
||||
final ItemStack[] itemStacks = new ItemStack[size];
|
||||
final DynamicRegistryManager registryManager = plugin.getMinecraftServer().getRegistryManager();
|
||||
Arrays.fill(itemStacks, ItemStack.EMPTY);
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (list.getCompound(i) == null) {
|
||||
final NbtCompound compound = list.getCompoundOrEmpty(i);
|
||||
if (compound.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
final NbtCompound compound = list.getCompound(i);
|
||||
final int slot = compound.getInt("Slot");
|
||||
itemStacks[slot] = decodeNbt(upgradeItemData(list.getCompound(i), mcVersion, plugin), registryManager);
|
||||
final int slot = compound.getInt("Slot", -1);
|
||||
if (slot >= 0) {
|
||||
itemStacks[slot] = decodeNbt(upgradeItemData(compound, mcVersion, plugin), registryManager);
|
||||
}
|
||||
}
|
||||
//#endif
|
||||
return itemStacks;
|
||||
}
|
||||
|
||||
@@ -216,28 +268,30 @@ public abstract class FabricSerializer {
|
||||
).getValue();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private NbtCompound encodeNbt(@NotNull ItemStack item, @NotNull DynamicRegistryManager registryManager) {
|
||||
//#if MC==12104
|
||||
return (NbtCompound) item.toNbt(registryManager);
|
||||
//#elseif MC==12101
|
||||
//$$ return (NbtCompound) item.encode(registryManager);
|
||||
//#elseif MC==12001
|
||||
//$$ final NbtCompound compound = new NbtCompound();
|
||||
//$$ item.writeNbt(compound);
|
||||
//$$ return compound;
|
||||
//#endif
|
||||
@Nullable
|
||||
private NbtCompound encodeNbt(@NotNull ItemStack item, @NotNull DynamicRegistryManager reg) {
|
||||
try {
|
||||
//#if MC>=12108
|
||||
return (NbtCompound) ItemStack.CODEC.encodeStart(reg.getOps(NbtOps.INSTANCE), item).getOrThrow();
|
||||
//#elseif MC>=12104
|
||||
//$$ return (NbtCompound) item.toNbt(reg);
|
||||
//#elseif MC==12101
|
||||
//$$ return (NbtCompound) item.encode(reg);
|
||||
//#endif
|
||||
} catch (Throwable e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ItemStack decodeNbt(@NotNull NbtElement item, @NotNull DynamicRegistryManager registryManager) {
|
||||
//#if MC==12001
|
||||
//$$ final @Nullable ItemStack stack = ItemStack.fromNbt((NbtCompound) item);
|
||||
private ItemStack decodeNbt(@NotNull NbtElement item, @NotNull DynamicRegistryManager reg) {
|
||||
//#if MC>=12108
|
||||
final @Nullable ItemStack stack = ItemStack.CODEC.decode(reg.getOps(NbtOps.INSTANCE), item).getOrThrow().getFirst();
|
||||
//#else
|
||||
final @Nullable ItemStack stack = ItemStack.fromNbt(registryManager, item).orElse(null);
|
||||
//$$ final @Nullable ItemStack stack = ItemStack.fromNbt(reg, item).orElse(null);
|
||||
//#endif
|
||||
if (stack == null) {
|
||||
throw new IllegalStateException("Failed to decode item NBT (got null 'fromNbt'): (%s)".formatted(item));
|
||||
throw new IllegalStateException("Failed to decode item NBT (decode got null): (%s)".formatted(item));
|
||||
}
|
||||
return stack;
|
||||
}
|
||||
|
||||
@@ -34,29 +34,31 @@ public interface FabricUserDataHolder extends UserDataHolder {
|
||||
|
||||
@Override
|
||||
default Optional<? extends Data> getData(@NotNull Identifier id) {
|
||||
if (!id.isCustom()) {
|
||||
try {
|
||||
return switch (id.getKeyValue()) {
|
||||
case "inventory" -> getInventory();
|
||||
case "ender_chest" -> getEnderChest();
|
||||
case "potion_effects" -> getPotionEffects();
|
||||
case "advancements" -> getAdvancements();
|
||||
case "location" -> getLocation();
|
||||
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));
|
||||
};
|
||||
} catch (Throwable e) {
|
||||
getPlugin().debug("Failed to get data for key: " + id.getKeyValue(), e);
|
||||
}
|
||||
if (id.isCustom()) {
|
||||
return Optional.ofNullable(getCustomDataStore().get(id));
|
||||
}
|
||||
|
||||
try {
|
||||
return switch (id.getKeyValue()) {
|
||||
case "inventory" -> getInventory();
|
||||
case "ender_chest" -> getEnderChest();
|
||||
case "potion_effects" -> getPotionEffects();
|
||||
case "advancements" -> getAdvancements();
|
||||
case "location" -> getLocation();
|
||||
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));
|
||||
};
|
||||
} catch (Throwable e) {
|
||||
getPlugin().debug("Failed to get data for key: " + id.asMinimalString(), e);
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable(getCustomDataStore().get(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -77,38 +79,48 @@ public interface FabricUserDataHolder extends UserDataHolder {
|
||||
final PlayerInventory inventory = getPlayer().getInventory();
|
||||
return Optional.of(FabricData.Items.Inventory.from(
|
||||
getCombinedInventory(inventory),
|
||||
inventory.selectedSlot
|
||||
//#if MC<12105
|
||||
//$$ inventory.selectedSlot
|
||||
//#else
|
||||
inventory.getSelectedSlot()
|
||||
//#endif
|
||||
));
|
||||
}
|
||||
|
||||
// Gets the player's combined inventory; their inventory, plus offhand and armor.
|
||||
@Nullable
|
||||
private ItemStack @NotNull [] getCombinedInventory(@NotNull PlayerInventory inv) {
|
||||
final ItemStack[] combined = new ItemStack[inv.main.size() + inv.armor.size() + inv.offHand.size()];
|
||||
System.arraycopy(
|
||||
inv.main.toArray(new ItemStack[0]), 0, combined,
|
||||
0, inv.main.size()
|
||||
);
|
||||
System.arraycopy(
|
||||
inv.armor.toArray(new ItemStack[0]), 0, combined,
|
||||
inv.main.size(), inv.armor.size()
|
||||
);
|
||||
System.arraycopy(
|
||||
inv.offHand.toArray(new ItemStack[0]), 0, combined,
|
||||
inv.main.size() + inv.armor.size(), inv.offHand.size()
|
||||
);
|
||||
//#if MC<12105
|
||||
//$$ final ItemStack[] combined = new ItemStack[inv.main.size() + inv.armor.size() + inv.offHand.size()];
|
||||
//$$ System.arraycopy(
|
||||
//$$ inv.main.toArray(new ItemStack[0]), 0, combined,
|
||||
//$$ 0, inv.main.size()
|
||||
//$$ );
|
||||
//$$ System.arraycopy(
|
||||
//$$ inv.armor.toArray(new ItemStack[0]), 0, combined,
|
||||
//$$ inv.main.size(), inv.armor.size()
|
||||
//$$ );
|
||||
//$$ System.arraycopy(
|
||||
//$$ inv.offHand.toArray(new ItemStack[0]), 0, combined,
|
||||
//$$ inv.main.size() + inv.armor.size(), inv.offHand.size()
|
||||
//$$ );
|
||||
//$$ return combined;
|
||||
//#else
|
||||
final ItemStack[] combined = new ItemStack[inv.size()];
|
||||
int slot = 0;
|
||||
for (ItemStack itemStack : inv) {
|
||||
combined[slot] = itemStack;
|
||||
slot++;
|
||||
}
|
||||
return combined;
|
||||
//#endif
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Items.EnderChest> getEnderChest() {
|
||||
return Optional.of(FabricData.Items.EnderChest.adapt(
|
||||
//#if MC==12001
|
||||
//$$ getPlayer().getEnderChestInventory().stacks
|
||||
//#else
|
||||
getPlayer().getEnderChestInventory().getHeldStacks()
|
||||
//#endif
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ public class FabricEventListener extends EventListener implements LockedHandler
|
||||
return (cancelPlayerEvent(player.getUuid())) ? ActionResult.FAIL : ActionResult.PASS;
|
||||
}
|
||||
|
||||
//#if MC==12104
|
||||
//#if MC>=12104
|
||||
private ActionResult handleItemInteract(PlayerEntity player, World world, Hand hand) {
|
||||
return (cancelPlayerEvent(player.getUuid())) ? ActionResult.FAIL : ActionResult.PASS;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//#if MC==12104
|
||||
//#if MC>=12104
|
||||
package net.william278.husksync.mixins;
|
||||
|
||||
import net.minecraft.entity.player.HungerManager;
|
||||
|
||||
@@ -82,13 +82,9 @@ public class PlayerEntityMixin {
|
||||
|
||||
@Unique
|
||||
private boolean hasVanishingCurse(@NotNull ItemStack stack) {
|
||||
//#if MC==12001
|
||||
//$$ return EnchantmentHelper.hasVanishingCurse(stack);
|
||||
//#else
|
||||
return EnchantmentHelper.hasAnyEnchantmentsIn(
|
||||
stack, TagKey.of(Enchantments.VANISHING_CURSE.getRegistryRef(), Enchantments.VANISHING_CURSE.getValue())
|
||||
);
|
||||
//#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -71,7 +71,11 @@ public abstract class ServerPlayNetworkHandlerMixin {
|
||||
|
||||
@Inject(method = "onClickSlot", at = @At("HEAD"), cancellable = true)
|
||||
public void onClickSlot(ClickSlotC2SPacket packet, CallbackInfo ci) {
|
||||
int slot = packet.getSlot();
|
||||
//#if MC<12105
|
||||
//$$ int slot = packet.getSlot();
|
||||
//#else
|
||||
int slot = packet.slot();
|
||||
//#endif
|
||||
if (slot < 0) {
|
||||
return;
|
||||
}
|
||||
@@ -88,11 +92,7 @@ public abstract class ServerPlayNetworkHandlerMixin {
|
||||
|
||||
@Inject(method = "onCreativeInventoryAction", at = @At("HEAD"), cancellable = true)
|
||||
public void onCreativeInventoryAction(CreativeInventoryActionC2SPacket packet, CallbackInfo ci) {
|
||||
//#if MC==12001
|
||||
//$$ int slot = packet.getSlot();
|
||||
//#else
|
||||
int slot = packet.slot();
|
||||
//#endif
|
||||
if (slot < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public class ServerWorldMixin {
|
||||
@Shadow
|
||||
private MinecraftServer server;
|
||||
|
||||
//#if MC==12104
|
||||
//#if MC>=12104
|
||||
@Inject(method = "savePersistentState", at = @At("HEAD"))
|
||||
//#else
|
||||
//$$ @Inject(method = "saveLevel", at = @At("HEAD"))
|
||||
|
||||
@@ -25,7 +25,7 @@ import eu.pb4.sgui.api.elements.GuiElementInterface;
|
||||
import eu.pb4.sgui.api.gui.SimpleGui;
|
||||
import me.lucko.fabric.api.permissions.v0.Permissions;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
//#if MC==12104
|
||||
//#if MC>=12104
|
||||
import net.kyori.adventure.platform.modcommon.MinecraftServerAudiences;
|
||||
//#else
|
||||
//$$ import net.kyori.adventure.platform.fabric.FabricServerAudiences;
|
||||
@@ -64,8 +64,9 @@ public class FabricUser extends OnlineUser implements FabricUserDataHolder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOffline() {
|
||||
return player == null || player.isDisconnected();
|
||||
public boolean hasDisconnected() {
|
||||
return getPlugin().getDisconnectingPlayers().contains(getUuid())
|
||||
|| player == null || player.isDisconnected();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -79,7 +80,7 @@ public class FabricUser extends OnlineUser implements FabricUserDataHolder {
|
||||
public void sendToast(@NotNull MineDown title, @NotNull MineDown description, @NotNull String iconMaterial,
|
||||
@NotNull String backgroundType) {
|
||||
plugin.log(Level.WARNING, "Toast notifications are deprecated. " +
|
||||
"Please change your notification display slot to CHAT, ACTION_BAR or NONE.");
|
||||
"Please change your notification display slot to CHAT, ACTION_BAR or NONE.");
|
||||
this.sendActionBar(title);
|
||||
}
|
||||
|
||||
@@ -106,7 +107,7 @@ public class FabricUser extends OnlineUser implements FabricUserDataHolder {
|
||||
this.editable = editable;
|
||||
|
||||
// Set title, items
|
||||
//#if MC==12104
|
||||
//#if MC>=12104
|
||||
this.setTitle(((MinecraftServerAudiences) plugin.getAudiences()).asNative(title.toComponent()));
|
||||
//#else
|
||||
//$$ this.setTitle(((FabricServerAudiences) plugin.getAudiences()).toNative(title.toComponent()));
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# File used for checking Minecraft server compatibility with this version of HuskSync
|
||||
minecraft_version: '${fabric_minecraft_version}'
|
||||
minecraft_version_range: '${minecraft_version_range}'
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"depends": {
|
||||
"fabricloader": ">=${fabric_loader_version}",
|
||||
"minecraft": "${fabric_minecraft_version}",
|
||||
"minecraft": "${minecraft_version_range}",
|
||||
"fabric-api": "*"
|
||||
},
|
||||
"suggests": {
|
||||
|
||||
@@ -4,17 +4,18 @@ org.gradle.daemon=true
|
||||
javaVersion=21
|
||||
|
||||
# Plugin metadata
|
||||
plugin_version=3.8
|
||||
plugin_version=3.8.8
|
||||
plugin_archive=husksync
|
||||
plugin_description=A modern, cross-server player data synchronization system
|
||||
|
||||
# General settings
|
||||
jedis_version=5.2.0
|
||||
mysql_driver_version=9.2.0
|
||||
mariadb_driver_version=3.5.1
|
||||
jedis_version=6.0.0
|
||||
mysql_driver_version=9.3.0
|
||||
mariadb_driver_version=3.5.3
|
||||
postgres_driver_version=42.7.5
|
||||
mongodb_driver_version=5.3.1
|
||||
mongodb_driver_version=5.5.0
|
||||
snappy_version=1.1.10.7
|
||||
|
||||
# Fabric settings
|
||||
fabric_loom_version=1.9-SNAPSHOT
|
||||
loom.ignoreDependencyLoomVersionValidation=true
|
||||
loom.disableUnpick=true
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
6
gradlew
vendored
6
gradlew
vendored
@@ -114,7 +114,7 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -205,7 +205,7 @@ fi
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
@@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
4
gradlew.bat
vendored
4
gradlew.bat
vendored
@@ -70,11 +70,11 @@ goto fail
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
@@ -5,11 +5,11 @@ pluginManagement {
|
||||
maven { url 'https://maven.fabricmc.net/' }
|
||||
maven { url 'https://maven.architectury.dev/' }
|
||||
maven { url 'https://maven.minecraftforge.net' }
|
||||
maven { url 'https://repo.essential.gg/repository/maven-public' }
|
||||
maven { url 'https://repo.essential.gg/public' }
|
||||
}
|
||||
|
||||
plugins {
|
||||
def egtVersion = "0.6.5"
|
||||
def egtVersion = "0.6.10"
|
||||
id("gg.essential.defaults") version egtVersion
|
||||
id("gg.essential.multi-version.root") version egtVersion
|
||||
}
|
||||
|
||||
@@ -66,6 +66,9 @@ redis:
|
||||
credentials:
|
||||
host: localhost
|
||||
port: 6379
|
||||
# Only change the database if you know what you are doing. The default is 0.
|
||||
database: 0
|
||||
user: ''
|
||||
password: ''
|
||||
use_ssl: false
|
||||
|
||||
|
||||
@@ -2,6 +2,6 @@ certifi==2024.7.4
|
||||
charset-normalizer==3.2.0
|
||||
colorama==0.4.6
|
||||
idna==3.7
|
||||
requests==2.32.0
|
||||
requests==2.32.4
|
||||
tqdm==4.66.3
|
||||
urllib3==2.2.2
|
||||
urllib3==2.6.0
|
||||
|
||||
@@ -13,7 +13,7 @@ from tqdm import tqdm
|
||||
class Parameters:
|
||||
root_dir = './servers/'
|
||||
proxy_version = "3.4.0-SNAPSHOT"
|
||||
minecraft_version = '1.21.4'
|
||||
minecraft_version = '1.21.5'
|
||||
eula_agreement = 'true'
|
||||
|
||||
backend_names = ['alpha', 'beta']
|
||||
|
||||
Reference in New Issue
Block a user