9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-28 02:59:13 +00:00

Compare commits

...

61 Commits

Author SHA1 Message Date
William278
2ada0497ec docs: update compatibility 2025-06-25 18:55:44 +01:00
William278
e9f2856040 feat: add support for Minecraft 1.21.6 2025-06-23 00:15:11 +01:00
William278
6050c584c0 refactor(paper): improve handling of locked maps in item frames 2025-06-22 14:23:52 +01:00
William278
7ebf91bfae refactor(paper): further refactors to locked maps 2025-06-22 00:24:36 +01:00
William278
2d7799628a refactor(paper): avoid use of default maven central URL 2025-06-21 15:32:02 +01:00
William278
1627de732b refactor: enable check-in petitions by default 2025-06-21 15:09:17 +01:00
William278
fea882c642 fix(paper): locked maps losing data on restart, close #498 2025-06-21 15:08:16 +01:00
dependabot[bot]
8b749357f7 build(deps): bump urllib3 from 2.2.2 to 2.5.0 in /test (#528)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.2 to 2.5.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.2.2...2.5.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.5.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-19 23:34:42 +01:00
dependabot[bot]
e4ff7e6d6c deps: bump org.ajoberstar.grgit from 5.3.0 to 5.3.2 (#526)
Bumps [org.ajoberstar.grgit](https://github.com/ajoberstar/grgit) from 5.3.0 to 5.3.2.
- [Release notes](https://github.com/ajoberstar/grgit/releases)
- [Commits](https://github.com/ajoberstar/grgit/compare/5.3.0...5.3.2)

---
updated-dependencies:
- dependency-name: org.ajoberstar.grgit
  dependency-version: 5.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-19 23:34:33 +01:00
William278
396630821f fix: return false if user is checked out if CIPs are off 2025-06-18 22:19:54 +01:00
William278
70f65d126b build: bump adventure platform to 6.4.0 2025-06-15 16:28:00 +01:00
dependabot[bot]
e8925a0d79 build(deps): bump requests from 2.32.0 to 2.32.4 in /test (#523)
Bumps [requests](https://github.com/psf/requests) from 2.32.0 to 2.32.4.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.0...v2.32.4)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.32.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-14 13:28:07 +01:00
dependabot[bot]
2a3cf9be7d deps: bump org.junit:junit-bom from 5.12.2 to 5.13.1 (#519)
Bumps [org.junit:junit-bom](https://github.com/junit-team/junit5) from 5.12.2 to 5.13.1.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.12.2...r5.13.1)

---
updated-dependencies:
- dependency-name: org.junit:junit-bom
  dependency-version: 5.13.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-14 13:27:59 +01:00
dependabot[bot]
971d3f5167 deps: bump net.kyori:adventure-api from 4.20.0 to 4.21.0 (#520)
Bumps [net.kyori:adventure-api](https://github.com/KyoriPowered/adventure) from 4.20.0 to 4.21.0.
- [Release notes](https://github.com/KyoriPowered/adventure/releases)
- [Commits](https://github.com/KyoriPowered/adventure/compare/v4.20.0...v4.21.0)

---
updated-dependencies:
- dependency-name: net.kyori:adventure-api
  dependency-version: 4.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-14 13:27:51 +01:00
dependabot[bot]
99da65a4d8 deps: bump org.snakeyaml:snakeyaml-engine from 2.7 to 2.9 (#522)
Bumps [org.snakeyaml:snakeyaml-engine](https://bitbucket.org/snakeyaml/snakeyaml-engine) from 2.7 to 2.9.
- [Commits](https://bitbucket.org/snakeyaml/snakeyaml-engine/branches/compare/snakeyaml-engine-2.9..snakeyaml-engine-2.7)

---
updated-dependencies:
- dependency-name: org.snakeyaml:snakeyaml-engine
  dependency-version: '2.9'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-14 13:27:39 +01:00
William278
25744b4ef7 Merge remote-tracking branch 'origin/master' 2025-06-13 19:25:31 +01:00
William278
8f2d1c7298 refactor: place checkin petitions behind experimental setting 2025-06-13 17:51:09 +01:00
William278
a9aa93a682 build: bump nbt-api to 2.15.1-SNAPSHOT 2025-06-07 13:51:06 +01:00
William278
ef340840ab build: bump deps 2025-06-07 13:48:57 +01:00
Marlon Pohl
cf08015961 feat: make redis user and database configurable (#518)
* Make redis user and database configurable

* Update documentation
2025-06-07 01:46:17 +01:00
小蔡
cb09e0cfb2 locales: update zh-tw.yml (#512) 2025-06-01 11:04:27 +01:00
William278
554fac89c0 build(fabric): bundle redis-authx-entraid:0.1.1-beta2 dep 2025-05-30 19:16:40 +01:00
William278
215bed9908 docs: fix typo 2025-05-30 18:40:26 +01:00
William278
935aafa74a fix(fabric): fix issues with Fabric 1.21.5 2025-05-26 21:18:45 +01:00
William278
c51ba85f38 fix: updates for Fabric 1.21.5 2025-05-26 20:47:26 +01:00
William278
6a67d1bbe0 build: support Fabric 1.21.5 2025-05-26 20:23:02 +01:00
William278
20bc76a768 fix: /enderchest command not working 2025-05-26 20:13:23 +01:00
William278
6928f97dff build: bump Plan to 5.6-2965 2025-05-26 19:47:05 +01:00
William278
06742fb848 build(fabric): include config deps 2025-05-26 19:45:35 +01:00
dependabot[bot]
759983b000 deps: bump de.exlll:configlib-yaml from 4.5.0 to 4.6.1 (#508)
Bumps [de.exlll:configlib-yaml](https://github.com/Exlll/ConfigLib) from 4.5.0 to 4.6.1.
- [Release notes](https://github.com/Exlll/ConfigLib/releases)
- [Commits](https://github.com/Exlll/ConfigLib/compare/v4.5.0...v4.6.1)

---
updated-dependencies:
- dependency-name: de.exlll:configlib-yaml
  dependency-version: 4.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-26 19:44:02 +01:00
William278
5556e3b6ce build: bump to 3.8.2 2025-05-26 19:42:55 +01:00
William278
bcffcb1f64 deps: bump net.kyori:adventure-platform-api from 4.3.4 to 4.4.0 2025-05-26 19:42:45 +01:00
dependabot[bot]
fa77e6e418 deps: bump net.kyori:adventure-text-serializer-plain (#503)
Bumps [net.kyori:adventure-text-serializer-plain](https://github.com/KyoriPowered/adventure) from 4.20.0 to 4.21.0.
- [Release notes](https://github.com/KyoriPowered/adventure/releases)
- [Commits](https://github.com/KyoriPowered/adventure/compare/v4.20.0...v4.21.0)

---
updated-dependencies:
- dependency-name: net.kyori:adventure-text-serializer-plain
  dependency-version: 4.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-26 19:42:21 +01:00
dependabot[bot]
c8aa29c82f deps: bump net.william278.uniform:uniform-paper from 1.3.3 to 1.3.4 (#509)
Bumps [net.william278.uniform:uniform-paper](https://github.com/WiIIiam278/Uniform) from 1.3.3 to 1.3.4.
- [Release notes](https://github.com/WiIIiam278/Uniform/releases)
- [Commits](https://github.com/WiIIiam278/Uniform/compare/1.3.3...1.3.4)

---
updated-dependencies:
- dependency-name: net.william278.uniform:uniform-paper
  dependency-version: 1.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-26 19:42:14 +01:00
William278
51cf982359 fix: remove debug message in /inventory 2025-05-12 17:39:56 +01:00
William278
f6d860335f fix: ensure map view for ID is set when applying, close #498 2025-05-10 23:45:05 +01:00
William278
5cea4665a1 fix: don't update ItemMeta of empty containers, close #499
This causes a `minecraft:block_entity_data` component to be added to the item, which causes stuff not to stack.
2025-05-10 23:27:52 +01:00
William278
34b183a35e build: bump runtime deps 2025-05-10 22:55:35 +01:00
William278
61298c24bb refactor: improve data identifier map structure, fix #492 2025-05-10 13:57:03 +01:00
William278
af9d32895e build: bump dependencies 2025-05-10 13:40:21 +01:00
ilightwas
52fa67432c fix: enable the userdata view command with no snapshot uuid args (#491)
Documentation states it should show the latest snapshot
2025-05-10 13:39:04 +01:00
William278
404f18d81f fix: add mojang-mapped, preload NBT-API
improves compatibility with 1.21.5/paper
2025-04-21 00:17:08 +01:00
William278
9ee8ea1c84 feat: auto-upgrade legacy map data, close #490 2025-04-17 20:54:31 +01:00
William278
64f845e293 build: bump nbt-api to 2.15.0 2025-04-17 20:31:04 +01:00
William278
30d1acc67e test: bump test suite to 1.21.5 2025-04-16 18:37:26 +01:00
William278
8d047d8892 build: add 1.21.5 properly to buildscript 2025-04-16 18:25:10 +01:00
William278
cb49ab8d73 build: bump deps 2025-04-16 18:12:09 +01:00
William278
436e85dada feat: add support for Paper 1.21.5 2025-04-16 18:03:00 +01:00
William278
223333882d refactor: remove debug message 2025-04-13 22:05:38 +01:00
ilightwas
06d8dda7dd fix: sql syntax in getUnpinnedSnapshotCount (#485)
An AND on a FROM clause
2025-04-11 14:41:33 +01:00
William278
805ffb19c2 build: bump lombok to 1.18.38 2025-04-09 19:07:27 +01:00
William278
cd3e4ef063 fix: sql syntax error with getUnpinnedSnapshotCount 2025-04-09 19:05:55 +01:00
dependabot[bot]
557b738511 deps: bump com.google.guava:guava from 33.4.5-jre to 33.4.6-jre (#473)
Bumps [com.google.guava:guava](https://github.com/google/guava) from 33.4.5-jre to 33.4.6-jre.
- [Release notes](https://github.com/google/guava/releases)
- [Commits](https://github.com/google/guava/commits)

---
updated-dependencies:
- dependency-name: com.google.guava:guava
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-05 23:34:33 +01:00
dependabot[bot]
8ee6b7a199 deps: bump com.zaxxer:HikariCP from 6.2.1 to 6.3.0 (#477)
Bumps [com.zaxxer:HikariCP](https://github.com/brettwooldridge/HikariCP) from 6.2.1 to 6.3.0.
- [Changelog](https://github.com/brettwooldridge/HikariCP/blob/dev/CHANGES)
- [Commits](https://github.com/brettwooldridge/HikariCP/compare/HikariCP-6.2.1...HikariCP-6.3.0)

---
updated-dependencies:
- dependency-name: com.zaxxer:HikariCP
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-05 23:34:26 +01:00
William278
dc880bc37f refactor: optimize rotateSnapshots
Don't pull all snapshots when rotating!
2025-03-30 14:48:37 +01:00
dependabot[bot]
c419587933 deps: bump com.google.guava:guava from 33.4.0-jre to 33.4.5-jre (#470)
Bumps [com.google.guava:guava](https://github.com/google/guava) from 33.4.0-jre to 33.4.5-jre.
- [Release notes](https://github.com/google/guava/releases)
- [Commits](https://github.com/google/guava/commits)

---
updated-dependencies:
- dependency-name: com.google.guava:guava
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-29 16:32:46 +00:00
jhqwqmc
afb4fdd5d5 locales: update zh-cn.yml (#472) 2025-03-29 16:32:02 +00:00
William278
bf8474e02d fix: uncomment petitionServerCheckin method call 2025-03-23 16:18:27 +00:00
William278
937ea9bc8e feat: improve data syncing with checkin petitions
This improves data fetching speed in cases where a user logs out during sync application; when they log back in, the server will petition the server they are checked out on to check them out.

We also now unlock users after saving sync on a server to accommodate this, and track user disconnection status to avoid inconsistencies with what platforms return for `isOnline`
2025-03-23 16:15:00 +00:00
William278
ef7b3c4f32 test: update to junit 5.12.1, use bill of materials 2025-03-23 13:30:04 +00:00
William278
370712c5b2 feat: skip offline users on user data apply 2025-03-20 19:53:31 +00:00
63 changed files with 882 additions and 361 deletions

View File

@@ -54,13 +54,21 @@ jobs:
paper-1.20.1
paper-1.21.1
paper-1.21.4
paper-1.21.5
paper-1.21.6
fabric-1.20.1
fabric-1.21.1
fabric-1.21.4
fabric-1.21.5
fabric-1.21.6
distro-groups: |
paper
paper
paper
paper
paper
fabric
fabric
fabric
fabric
fabric
@@ -68,13 +76,21 @@ jobs:
Paper 1.20.1
Paper 1.21.1
Paper 1.21.4
Paper 1.21.5
Paper 1.21.6
Fabric 1.20.1
Fabric 1.21.1
Fabric 1.21.4
Fabric 1.21.5
Fabric 1.21.6
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-Bukkit-${{ env.version_name }}+mc.1.21.5.jar
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.6.jar
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.20.1.jar
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.1.jar
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.4.jar
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.6.jar

View File

@@ -43,13 +43,21 @@ jobs:
paper-1.20.1
paper-1.21.1
paper-1.21.4
paper-1.21.5
paper-1.21.6
fabric-1.20.1
fabric-1.21.1
fabric-1.21.4
fabric-1.21.5
fabric-1.21.6
distro-groups: |
paper
paper
paper
paper
paper
fabric
fabric
fabric
fabric
fabric
@@ -57,13 +65,21 @@ jobs:
Paper 1.20.1
Paper 1.21.1
Paper 1.21.4
Paper 1.21.5
Paper 1.21.6
Fabric 1.20.1
Fabric 1.21.1
Fabric 1.21.4
Fabric 1.21.5
Fabric 1.21.6
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-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.5.jar
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.6.jar
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.20.1.jar
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.1.jar
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.4.jar
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.6.jar

View File

@@ -46,16 +46,18 @@
## 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.6 | _latest_ | 21 | Paper | ✅ **Active Release** |
| 1.21.5 | _latest_ | 21 | Paper | ✅ **January 2026** (Non-LTS) |
| 1.21.4 | _latest_ | 21 | Paper, Fabric | ✅ **November 2025** (Non-LTS) |
| 1.21.3 | 3.7.1 | 21 | Paper, Fabric | 🗃️ Archived (December 2024) |
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **November 2025** (LTS) |
| 1.20.6 | 3.6.8 | 17 | Paper | 🗃️ Archived (October 2024) |
| 1.20.4 | 3.6.8 | 17 | Paper | 🗃️ Archived (July 2024) |
| 1.20.1 | _latest_ | 17 | Paper, Fabric | ✅ **November 2025** (LTS) |
| 1.17.1 - 1.19.4 | 3.6.8 | 17 | Paper | 🗃️ Archived |
| 1.16.5 | 3.2.1 | 16 | Paper | 🗃️ Archived |
HuskSync is primarily developed against the latest release. Old Minecraft versions are allocated a support channel based on popularity, mod support, etc:

View File

@@ -3,9 +3,9 @@ import org.apache.tools.ant.filters.ReplaceTokens
plugins {
id 'com.gradleup.shadow' version '8.3.6'
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,9 +89,9 @@ 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'
testImplementation(platform("org.junit:junit-bom:5.13.1"))
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testCompileOnly 'org.jetbrains:annotations:26.0.2'
}
@@ -137,7 +137,7 @@ subprojects {
version += "+mc.${project.name}"
if (project.parent?.name?.equals('fabric')) {
apply plugin: 'fabric-loom'
apply plugin: 'dev.architectury.loom'
}
}

View File

@@ -0,0 +1,3 @@
minecraft_version_numeric=12105
minecraft_api_version=1.21
paper_api_version=1.21.5-R0.1-SNAPSHOT

View File

@@ -0,0 +1,3 @@
minecraft_version_numeric=12106
minecraft_api_version=1.21
paper_api_version=1.21.6-R0.1-SNAPSHOT

View File

@@ -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.5'
implementation 'net.william278.uniform:uniform-paper:1.3.5'
implementation 'net.william278.toilet:toilet-bukkit:1.0.14'
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.0'
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.1-SNAPSHOT'
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.8.0'
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.38'
compileOnly 'commons-io:commons-io:2.19.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.1'
compileOnly 'com.zaxxer:HikariCP:6.3.0'
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.38'
}
processResources {
@@ -93,5 +93,9 @@ shadowJar {
tasks {
runServer {
minecraftVersion(project.name)
downloadPlugins {
github("plan-player-analytics", "Plan", "5.6.2965", "Plan-5.6-build-2965.jar")
}
}
}

View File

@@ -23,6 +23,7 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import de.tr7zw.changeme.nbtapi.NBT;
import de.tr7zw.changeme.nbtapi.utils.DataFixerUtil;
import lombok.AccessLevel;
import lombok.Getter;
@@ -92,13 +93,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 +148,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)));
@@ -348,8 +354,11 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
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*/;
case "1.21.2" -> DataFixerUtil.VERSION1_21_2;
case "1.21.3" -> DataFixerUtil.VERSION1_21_3;
case "1.21.4" -> DataFixerUtil.VERSION1_21_4;
case "1.21.5" -> DataFixerUtil.VERSION1_21_5;
case "1.21.6" -> 4435;
default -> DataFixerUtil.getCurrentVersion();
};
}

View File

@@ -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()) {

View File

@@ -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

View File

@@ -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()));
}
}

View File

@@ -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());
}
}

View File

@@ -44,6 +44,7 @@ public class PaperEventListener extends BukkitEventListener {
}
@Override
@SuppressWarnings("RedundantMethodOverride")
public void onEnable() {
getPlugin().getServer().getPluginManager().registerEvents(this, getPlugin());
lockedHandler.onEnable();

View File

@@ -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,7 +44,9 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.*;
import java.util.function.Function;
@@ -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);
}
@@ -248,97 +254,139 @@ 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 @Nullable Map.Entry<MapData, Boolean> readMapData = readMapData(originServerName, originalMapId);
if (readMapData == null) {
getPlugin().debug("Read pixel data was not found in database, skipping...");
return;
}
// Add a renderer to the map with the data and save to file
final MapData canvasData = Objects.requireNonNull(readMapData, "Pixel data null!").getKey();
final MapView view = generateRenderedMap(canvasData);
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...");
Map.Entry<MapData, Boolean> 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(Objects.requireNonNull(mapData).getKey(), 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...");
Map.Entry<MapData, Boolean> 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!").getKey());
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.NORMAL);
view.setTrackingPosition(false);
view.setUnlimitedTracking(false);
return;
}
Map.Entry<MapData, Boolean> 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.getKey());
}
// 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.setTrackingPosition(false);
view.setUnlimitedTracking(false);
// Set the view to the map and return it
setMapView(view);
return view;
}
@NotNull
@@ -420,6 +468,36 @@ public interface BukkitMapHandler {
);
}
// Legacy - read maps from item stacks
@Nullable
@Blocking
private Map.Entry<MapData, Boolean> readLegacyMapItemData(@NotNull ReadableItemNBT nbt) {
final int dataVer = getPlugin().getDataVersion(getPlugin().getMinecraftVersion());
try {
return new AbstractMap.SimpleImmutableEntry<>(MapData.fromByteArray(dataVer,
Objects.requireNonNull(nbt.getByteArray(MAP_LEGACY_PIXEL_DATA_KEY))), false);
} catch (IOException e) {
getPlugin().log(Level.WARNING, "Failed to read legacy map data", e);
return null;
}
}
// Legacy - read maps from files
@Nullable
private Map.Entry<MapData, Boolean> 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 new AbstractMap.SimpleImmutableEntry<>(MapData.fromNbt(file), false);
} 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}
*/

View File

@@ -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

View File

@@ -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.19.0'
api 'org.apache.commons:commons-text:1.13.1'
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.1'
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.1'
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:6.3.0') {
exclude module: 'slf4j-api'
}
compileOnlyApi 'net.william278.toilet:toilet-common:1.0.12'
compileOnlyApi 'net.william278.toilet:toilet-common:1.0.14'
compileOnly 'net.william278.uniform:uniform-common:1.3.1'
compileOnly 'net.william278.uniform:uniform-common:1.3.5'
compileOnly 'com.mojang:brigadier:1.1.8'
compileOnly 'org.projectlombok:lombok:1.18.36'
compileOnly 'org.projectlombok:lombok:1.18.38'
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 'net.kyori:adventure-api:4.23.0'
compileOnly 'net.kyori:adventure-platform-api:4.4.0'
compileOnly "net.kyori:adventure-text-serializer-plain:4.23.0"
compileOnly 'com.google.guava:guava:33.4.8-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'
testImplementation 'com.google.guava:guava:33.4.8-jre'
testImplementation 'com.github.plan-player-analytics:Plan:5.6.2965'
testCompileOnly 'de.exlll:configlib-yaml:4.6.1'
testCompileOnly 'org.jetbrains:annotations:26.0.2'
annotationProcessor 'org.projectlombok:lombok:1.18.36'
annotationProcessor 'org.projectlombok:lombok:1.18.38'
}

View File

@@ -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;
@@ -140,7 +141,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;
}
@@ -315,6 +316,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);
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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,6 +170,9 @@ 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 = "";
private boolean useSsl = false;
}
@@ -321,6 +326,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 = true;
public boolean doAutoPin(@NotNull DataSnapshot.SaveCause cause) {
return autoPinnedSaveCauses.contains(cause.name());
}

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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,19 +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 key.toString().hashCode();
return asMinimalString().hashCode();
}
// Get the config entry for the identifier
@@ -245,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>

View File

@@ -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

View File

@@ -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;

View File

@@ -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,7 +274,7 @@ 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 Map.Entry (key: map data, value: is from current world)
*/
@Blocking
public abstract @Nullable Map.Entry<byte[], Boolean> getMapData(@NotNull String serverName, int mapId);
@@ -274,7 +284,7 @@ public abstract class Database {
*
* @param serverName Name of the server the map originates from
* @param mapId Original map ID
* @return Map.Entry (key: server name, value: map ID)
* @return Map.Entry (key: server name, value: map ID)
*/
@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);

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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.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) {
@@ -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();
}

View File

@@ -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
));

View File

@@ -65,9 +65,11 @@ public class RedisManager extends JedisPubSub {
@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
@@ -79,9 +81,20 @@ public class RedisManager extends JedisPubSub {
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);
DefaultJedisClientConfig.Builder clientConfigBuilder = DefaultJedisClientConfig.builder()
.ssl(useSSL)
.database(database)
.timeoutMillis(0);
if (!user.isEmpty()) {
clientConfigBuilder.user(user);
}
if (!password.isEmpty()) {
clientConfigBuilder.password(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()
@@ -158,7 +171,7 @@ 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 {
@@ -170,16 +183,31 @@ public class RedisManager extends JedisPubSub {
}
}
);
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)
);
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]);
boolean online = plugin.getDisconnectingPlayers().contains(user.getUuid())
|| plugin.getOnlineUser(user.getUuid()).isEmpty();
if (!online && !plugin.isLocked(user.getUuid())) {
plugin.debug("[%s] Received check-in petition for online/unlocked user, ignoring".formatted(user.getName()));
return;
}
plugin.getRedisManager().setUserCheckedOut(user, false);
plugin.debug("[%s] Received petition for offline user, checking them 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 +216,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,11 +239,17 @@ 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);
}
@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,
@@ -421,7 +455,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;
}
}

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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());
}
);
});
}

View File

@@ -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());
}
));
}

View File

@@ -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())
);
});
}

View File

@@ -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()))

View File

@@ -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()
)),

View File

@@ -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%)'

View File

@@ -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%)'

View File

@@ -1,15 +1,17 @@
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.6 | _latest_ | 21 | Paper | ✅ **Active Release** |
| 1.21.5 | _latest_ | 21 | Paper | ✅ **January 2026** (Non-LTS) |
| 1.21.4 | _latest_ | 21 | Paper, Fabric | ✅ **November 2025** (Non-LTS) |
| 1.21.3 | 3.7.1 | 21 | Paper, Fabric | 🗃️ Archived (December 2024) |
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **November 2025** (LTS) |
| 1.20.6 | 3.6.8 | 17 | Paper | 🗃️ Archived (October 2024) |
| 1.20.4 | 3.6.8 | 17 | Paper | 🗃️ Archived (July 2024) |
| 1.20.1 | _latest_ | 17 | Paper, Fabric | ✅ **November 2025** (LTS) |
| 1.17.1 - 1.19.4 | 3.6.8 | 17 | Paper | 🗃️ Archived |
| 1.16.5 | 3.2.1 | 16 | Paper | 🗃️ Archived |
HuskSync is primarily developed against the latest release. Old Minecraft versions are allocated a support channel based on popularity, mod support, etc:
@@ -31,5 +33,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]]

View File

@@ -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!

View File

@@ -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

View File

@@ -1,7 +1,7 @@
essential.defaults.loom.mappings=net.fabricmc:yarn:1.21.4+build.4:v2
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

View File

@@ -0,0 +1,7 @@
essential.defaults.loom.mappings=net.fabricmc:yarn:1.21.5+build.1:v2
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

View File

@@ -0,0 +1,7 @@
essential.defaults.loom.mappings=net.fabricmc:yarn:1.21.6+build.1:v2
fabric_loader_version=0.16.14
fabric_api_version=0.127.1+1.21.6
fabric_permissions_api_version=0.4.0
fabric_adventure_platform_version=6.5.0-SNAPSHOT
fabric_sgui_version=1.10.0+1.21.6

View File

@@ -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.5+${project.name}")
modImplementation include("net.william278.toilet:toilet-fabric:1.0.14+${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.1")
implementation include("org.snakeyaml:snakeyaml-engine:2.9")
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.projectlombok:lombok:1.18.38'
annotationProcessor 'org.projectlombok:lombok:1.18.36'
annotationProcessor 'org.projectlombok:lombok:1.18.38'
implementation include(project(path: ":common"))
project(":common").configurations.api.dependencies.each { dependency ->

View File

@@ -1 +1 @@
1.21.4
1.21.6

View File

@@ -3,11 +3,15 @@ plugins {
}
preprocess {
def fabric12106 = createNode("1.21.6", 12106, "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(fabric12106, null)
fabric12104.link(fabric12106, null)
fabric12101.link(fabric12106, null)
fabric12001.link(fabric12106, null)
}

View File

@@ -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;
@@ -100,15 +99,16 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
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 static final int VERSION1_21_4 = 4189;
private static final int VERSION1_21_5 = 4323;
private static final int VERSION1_21_6 = 4435;
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 +116,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 +167,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);
@@ -387,8 +387,14 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
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;
case "1.21.5" -> VERSION1_21_5;
case "1.21.6" -> VERSION1_21_6;
//#if MC==12106
default -> VERSION1_21_6;
//#elseif MC==12105
//$$ default -> VERSION1_21_5;
//#elseif MC==12104
//$$ default -> VERSION1_21_4;
//#elseif MC==12101
//$$ default -> VERSION1_21_1;
//#elseif MC==12001

View File

@@ -54,7 +54,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;
@@ -178,7 +178,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 +188,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();
}
@@ -513,7 +517,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 +556,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;
}
@@ -804,7 +808,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 +827,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 +892,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
}
}

View File

@@ -76,17 +76,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
@@ -121,7 +133,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);
@@ -153,14 +169,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);
@@ -199,19 +227,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;
}
@@ -226,12 +272,14 @@ public abstract class FabricSerializer {
}
@Nullable
private NbtCompound encodeNbt(@NotNull ItemStack item, @NotNull DynamicRegistryManager registryManager) {
private NbtCompound encodeNbt(@NotNull ItemStack item, @NotNull DynamicRegistryManager reg) {
try {
//#if MC==12104
return (NbtCompound) item.toNbt(registryManager);
//#if MC>=12106
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(registryManager);
//$$ return (NbtCompound) item.encode(reg);
//#elseif MC==12001
//$$ final NbtCompound compound = new NbtCompound();
//$$ item.writeNbt(compound);
@@ -243,14 +291,16 @@ public abstract class FabricSerializer {
}
@NotNull
private ItemStack decodeNbt(@NotNull NbtElement item, @NotNull DynamicRegistryManager registryManager) {
//#if MC==12001
private ItemStack decodeNbt(@NotNull NbtElement item, @NotNull DynamicRegistryManager reg) {
//#if MC>=12106
final @Nullable ItemStack stack = ItemStack.CODEC.decode(reg.getOps(NbtOps.INSTANCE), item).getOrThrow().getFirst();
//#elseif MC>12001
//$$ final @Nullable ItemStack stack = ItemStack.fromNbt(reg, item).orElse(null);
//#elseif MC==12001
//$$ final @Nullable ItemStack stack = ItemStack.fromNbt((NbtCompound) item);
//#else
final @Nullable ItemStack stack = ItemStack.fromNbt(registryManager, 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;
}

View File

@@ -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,27 +79,41 @@ 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

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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"))

View File

@@ -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()));

View File

@@ -4,17 +4,17 @@ org.gradle.daemon=true
javaVersion=21
# Plugin metadata
plugin_version=3.8
plugin_version=3.8.5
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

View File

@@ -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

View File

@@ -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.5.0

View File

@@ -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']