9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-23 16:49:19 +00:00

Compare commits

..

103 Commits
3.6.1 ... 3.7

Author SHA1 Message Date
William
099a258cf8 docs: update compatibility table 2024-10-03 15:06:22 +01:00
William
480f59a166 build: fix build, getEnv -> getProperty 2024-10-03 15:03:53 +01:00
William
45c2f5350f Merge remote-tracking branch 'origin/master' 2024-10-03 14:58:25 +01:00
William
ed88d77852 build: bump guava, junit and HikariCP 2024-10-03 14:58:12 +01:00
dependabot[bot]
e7fc9f015e deps: bump com.zaxxer:HikariCP from 5.1.0 to 6.0.0 (#388)
Bumps [com.zaxxer:HikariCP](https://github.com/brettwooldridge/HikariCP) from 5.1.0 to 6.0.0.
- [Changelog](https://github.com/brettwooldridge/HikariCP/blob/dev/CHANGES)
- [Commits](https://github.com/brettwooldridge/HikariCP/compare/HikariCP-5.1.0...HikariCP-6.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-03 14:57:06 +01:00
William
cabde9e8d8 fix: version_uuid instead of id in PSQL rotateSnapshots 2024-09-29 15:33:32 +01:00
William
4df7d2def4 fix: missing placeholder %s in postgres 2024-09-29 14:58:09 +01:00
William
59ed77c169 fix: Compat issues with Postgres queries, close #383 2024-09-29 14:32:57 +01:00
William
53da3bd40c docs: update README workflow badge 2024-09-29 14:21:13 +01:00
William
abdf8223fc refactor: adjust postgres statements 2024-09-29 13:35:28 +01:00
William
a5efeecad3 fix: NPE on startup due to compat check, close #384 2024-09-29 13:17:42 +01:00
William
4d26b24d13 Merge remote-tracking branch 'origin/master' 2024-09-28 19:01:53 +01:00
William
29b3a60c64 fix: sync default valued attributes 2024-09-28 19:01:46 +01:00
dependabot[bot]
da894f57c4 deps: bump com.gradleup.shadow from 8.3.0 to 8.3.2 (#380)
Bumps [com.gradleup.shadow](https://github.com/GradleUp/shadow) from 8.3.0 to 8.3.2.
- [Release notes](https://github.com/GradleUp/shadow/releases)
- [Commits](https://github.com/GradleUp/shadow/compare/8.3.0...8.3.2)

---
updated-dependencies:
- dependency-name: com.gradleup.shadow
  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>
2024-09-28 14:33:23 +01:00
dependabot[bot]
1bd703641b deps: bump org.bstats:bstats-bukkit from 3.0.3 to 3.1.0 (#379)
Bumps [org.bstats:bstats-bukkit](https://github.com/Bastian/bStats-Metrics) from 3.0.3 to 3.1.0.
- [Release notes](https://github.com/Bastian/bStats-Metrics/releases)
- [Commits](https://github.com/Bastian/bStats-Metrics/compare/v3.0.3...v3.1.0)

---
updated-dependencies:
- dependency-name: org.bstats:bstats-bukkit
  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>
2024-09-28 14:33:14 +01:00
dependabot[bot]
1b1d4c8e8d deps: bump commons-io:commons-io from 2.16.1 to 2.17.0 (#381)
Bumps commons-io:commons-io from 2.16.1 to 2.17.0.

---
updated-dependencies:
- dependency-name: commons-io:commons-io
  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>
2024-09-28 14:33:07 +01:00
Coded
842ec0e28d feat: add create_tables config option to disable automatic DDL operations (#377)
* Add config option for creating tables

* Move createTables config to a better position
2024-09-28 14:32:53 +01:00
William
2d5648408e build: account for differences in commit version names 2024-09-05 19:14:38 +01:00
William
41b3240741 ci: force copy files, make dir first 2024-09-05 17:50:11 +01:00
William
bc03e8f3e3 ci: recursive copy target jar folder 2024-09-05 17:46:11 +01:00
William
86799f4c08 ci: try copying outputs first 2024-09-05 17:43:06 +01:00
William
a3e004cf71 ci: adjust output paths 2024-09-05 17:37:43 +01:00
William
a7aeb1de21 fix: remove debug logging 2024-09-05 17:35:20 +01:00
William
1a703102c3 ci: More adjustments to CI script 2024-09-05 17:26:23 +01:00
William
368c68f42b fix: item and block stats not syncing, close #362 2024-09-05 17:25:03 +01:00
William
e191713bdc fix: attribute syncing setting bad base value 2024-09-05 17:07:50 +01:00
William
1604338498 ci: Use correct environment variable for workspace 2024-09-05 14:18:31 +01:00
William
c223797bf4 ci: Adjust glob build file output paths 2024-09-05 14:12:21 +01:00
William
9b10adc8e4 ci: fix version checking paths 2024-09-05 14:05:11 +01:00
William
5935f1ab5f ci: fix build version checking 2024-09-05 14:00:49 +01:00
William
3455b10a20 docs: update compatibility matrix 2024-09-05 13:58:32 +01:00
William
34e08b712d build: update CI to publish all active versions 2024-09-05 13:49:12 +01:00
William
605d314a58 refactor: make attributes allow-listed instead of deny-listed
This is a better default - as a number of attributes are primarily synced through other means (potions, items), or were applied from a context-sensitive action that does not warrant syncing across server contexts (sprinting, flying)
2024-09-05 13:37:53 +01:00
dependabot[bot]
daaf5147a7 deps: bump org.bstats:bstats-bukkit from 3.0.2 to 3.0.3 (#370)
Bumps [org.bstats:bstats-bukkit](https://github.com/Bastian/bStats-Metrics) from 3.0.2 to 3.0.3.
- [Release notes](https://github.com/Bastian/bStats-Metrics/releases)
- [Commits](https://github.com/Bastian/bStats-Metrics/compare/v3.0.2...v3.0.3)

---
updated-dependencies:
- dependency-name: org.bstats:bstats-bukkit
  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>
2024-09-03 22:20:26 +01:00
William
50eb9a7543 feat: add legacy upgrade command 2024-08-26 16:00:21 +01:00
William
7d8ef7b6b3 refactor: update map persistence logic
Stop using deprecated methods
2024-08-26 15:32:28 +01:00
William
347d2d0a8f refactor: adjust modifier slot group methods 2024-08-26 14:10:02 +01:00
William
bd560fcc99 refactor: use getter and setter for payload bytes in RedisMessage 2024-08-26 14:08:49 +01:00
William
b68aedc99a fix: correct compat version check 2024-08-26 14:08:25 +01:00
William
47373d8974 refactor: lock user before updating user data on command 2024-08-26 14:08:11 +01:00
William
a57b8df994 refactor: clean up 1.21.1 compatibility issues
Close #368
2024-08-26 13:44:19 +01:00
William
17235637a5 fix: exception loading compatibility config 2024-08-26 12:35:21 +01:00
William
cd5abd5a65 build(deps): bump dependencies 2024-08-25 21:05:17 +01:00
William
5c6631cdcf docs: clarify release channels in compat table 2024-08-25 21:03:19 +01:00
William
621afcd5c6 feat: support Minecraft 1.21.1 on Fabric 2024-08-25 20:58:26 +01:00
William
112a974a6c feat: Target Minecraft 1.21.1 with new release system 2024-08-25 20:45:25 +01:00
William
f9d46b4aff Merge remote-tracking branch 'origin/master' 2024-08-25 20:38:31 +01:00
William
dfd828bca1 feat: introduce new versioning & Minecraft compatibility system 2024-08-25 20:07:04 +01:00
dependabot[bot]
2df9fd897a deps: bump net.kyori:adventure-platform-bukkit from 4.3.3 to 4.3.4 (#360)
Bumps [net.kyori:adventure-platform-bukkit](https://github.com/KyoriPowered/adventure-platform) from 4.3.3 to 4.3.4.
- [Release notes](https://github.com/KyoriPowered/adventure-platform/releases)
- [Commits](https://github.com/KyoriPowered/adventure-platform/compare/v4.3.3...v4.3.4)

---
updated-dependencies:
- dependency-name: net.kyori:adventure-platform-bukkit
  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>
2024-08-14 13:39:00 +01:00
dependabot[bot]
ff2531539e deps: bump net.kyori:adventure-platform-api from 4.3.3 to 4.3.4 (#361)
Bumps [net.kyori:adventure-platform-api](https://github.com/KyoriPowered/adventure-platform) from 4.3.3 to 4.3.4.
- [Release notes](https://github.com/KyoriPowered/adventure-platform/releases)
- [Commits](https://github.com/KyoriPowered/adventure-platform/compare/v4.3.3...v4.3.4)

---
updated-dependencies:
- dependency-name: net.kyori:adventure-platform-api
  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>
2024-08-14 13:38:50 +01:00
William
52ec138273 fix: suppress map cursor paper exception 2024-08-11 12:52:23 +01:00
William
0f7a866652 test: bump test deps to 1.21.1 2024-08-11 12:25:43 +01:00
William
eeb52ac41e deps: bump item-nbt-api to 2.13.2
support MC 1.21.1
2024-08-11 12:25:03 +01:00
William
4c7ec9ec21 docs: update config file 2024-08-09 16:53:26 +01:00
William
2f9064c4c6 refactor: revert "disable attributes/potion effects by default" 2024-08-09 16:48:40 +01:00
William
5c234cdb1d feat: improve server version status text 2024-08-09 15:03:23 +01:00
William
7d8a74381b build: bump runtime dependencies 2024-08-09 14:49:53 +01:00
William
04a7793585 refactor: auto-reformat code 2024-08-09 14:43:54 +01:00
William
ea068529f6 fix: stop syncing ambient effects, close #289
Effects from beacons, conduits, and The Warden will no longer sync.
2024-08-09 14:39:56 +01:00
William
fead3df0d8 fix: add boot warning to fabric 2024-08-09 14:25:17 +01:00
William
0c5a42a344 fix: Cancel outbound PacketEvents packets, close #344 2024-08-09 14:22:53 +01:00
William
75a2378ea8 feat: deprecate Toast notifications 2024-08-09 14:19:34 +01:00
William
662fc96ad5 refactor: disable potion effects & attributes by default 2024-08-09 14:11:19 +01:00
Dong Heon Hee
f456443da0 Merge branch 'master' into master 2024-08-05 07:39:13 +09:00
William
07da1c04ce fix: don't apply <1.21 attribute modifiers on >1.21 servers 2024-08-02 18:07:28 +01:00
William
845abf370a fix: more tweaks to fix attribute issues 2024-07-28 18:07:34 +01:00
William
83b5209a75 fix: "attribute modifier already applied" error, close #348 2024-07-26 16:44:04 +01:00
William
8e9850dd19 refactor: make potion effects an optional dep of attributes 2024-07-26 16:35:43 +01:00
William
1d24209b68 feat: add attribute config, don't sync potion modifiers, close #349 2024-07-26 14:26:52 +01:00
William
da70a54d78 ci: add bones publishing to CI 2024-07-24 23:49:20 +01:00
동헌희
fc7330213a fix: build error due to rebase with master branch 2024-07-25 00:15:15 +09:00
동헌희
d8272ba52d docs: update minimum required mc&jvm version 2024-07-23 08:50:32 +09:00
동헌희
315f0eeb2f ci: Update jvm version(21) 2024-07-23 08:50:32 +09:00
동헌희
8e83617ac4 ci: Update required jvm version(21) 2024-07-23 08:50:32 +09:00
동헌희
212bb0beb8 fix: Forgot to update upgradeItemStacks 2024-07-23 08:50:32 +09:00
동헌희
c16231b12b feat: Support fabric with 1.21 2024-07-23 08:50:32 +09:00
동헌희
93f7294859 fix: Breaking chage in fabric API 2024-07-23 08:50:32 +09:00
Preva1l
32ac57e2a4 fix: cme on potion effect syncing (#354)
* Started impl for mongo

* fix silly mistake with postgresql

* fix: race condition
2024-07-21 15:14:48 +01:00
William
c949c976d6 fix: more checkout key debug logging 2024-07-21 01:11:44 +01:00
William
ab736829f2 refactor: clarify data syncer method names 2024-07-21 01:04:14 +01:00
William
4433926ce7 build: bump to 3.6.7 2024-07-19 17:35:25 +01:00
William
f819fd4d5e fix: Fabric thread exhaustion/deadlock causing crashes 2024-07-19 17:33:04 +01:00
Stampede
e7659255fe feat: add ModLoaded callback event on Fabric (#346)
Co-authored-by: William <will27528@gmail.com>
2024-07-14 17:09:26 +01:00
Code Toad
0dee2e8319 build: correct grgit null reference in git-less environments, close #345 (#347)
Issue Summary:
when sources are downloaded (not through git) the build fails because of a null reference.
Fix:
replace null reference with empty string

Co-authored-by: codetoad <softwareenginer@pm.me>
2024-07-14 17:08:03 +01:00
William
7b35c47315 fix: wrong syntax processing on husksync migrate set 2024-07-11 13:12:44 +01:00
William
5056a794d8 fix: Set execution scopes in commands 2024-07-06 14:19:04 +01:00
William
5e6068431a build: bump to 3.6.6 2024-07-06 14:11:34 +01:00
William
8d69508689 Merge remote-tracking branch 'origin/master' 2024-07-06 14:11:26 +01:00
William
efb6d8a7de deps: bump lombok and commons 2024-07-06 14:11:20 +01:00
William
79d9778378 deps: bump Uniform to 1.2.1 2024-07-06 14:10:27 +01:00
dependabot[bot]
6a6695e447 build(deps): bump certifi from 2023.7.22 to 2024.7.4 in /test (#337)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.7.22 to 2024.7.4.
- [Commits](https://github.com/certifi/python-certifi/compare/2023.07.22...2024.07.04)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-06 14:10:01 +01:00
William
8862e6cd70 feat: don't check if dependency is loaded 2024-06-24 19:53:13 +01:00
William
0b29de9efc fix: update documentation, help menu access for migrators 2024-06-23 15:49:28 +01:00
William
962cdfce0b build: bundle postgres in HuskSync for Fabric 2024-06-22 18:15:01 +01:00
William
0c527202e5 fix: NMS exceptions being thrown when applying modifiers
Spigot's validation for this is like my hoover: it sucks.
2024-06-22 18:07:53 +01:00
William
d4e33aa9d2 fix: ensure data version is passed to deserialize methods
Fixes an issue where upgraded stacks would only have a size of 1
2024-06-22 18:06:17 +01:00
William
2fcd58fc18 feat: correctly apply keyed attribute modifiers, close #326
We need to construct attributes with their key if possible to avoid stacking. Uses reflection :( to do this.

Also adds a bit of error checking to health scale syncing
2024-06-21 13:17:53 +01:00
William
3d10b2324f feat: update DataFixer 2024-06-21 11:56:49 +01:00
William
31419f3b97 deps: bump Item NBT API to 2.13.1 2024-06-21 11:56:01 +01:00
William
8105ac27fc deps: bump Uniform to 1.1.8
Fixes startup NPE fetching usage text
2024-06-19 12:49:55 +01:00
William
44f251a948 deps: bump Uniform to 1.1.7
Adds usage text to bukkit & legacy Paper commands
2024-06-19 12:45:50 +01:00
William
463e707d27 deps: bump Uniform to 1.1.6 2024-06-19 12:23:40 +01:00
William
2d85910744 deps: bump Uniform to 1.1.5 2024-06-18 23:47:33 +01:00
77 changed files with 1380 additions and 808 deletions

View File

@@ -1,44 +0,0 @@
name: CI Tests
on:
push:
branches: [ 'master' ]
paths-ignore:
- 'docs/**'
- 'workflows/**'
- 'README.md'
permissions:
contents: read
checks: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 'Checkout for CI 🛎️'
uses: actions/checkout@v4
- name: 'Set up JDK 17 📦'
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: 'Build with Gradle 🏗️'
uses: gradle/gradle-build-action@v3
with:
arguments: build test publish
env:
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
- name: 'Publish Test Report 📊'
uses: mikepenz/action-junit-report@v4
if: success() || failure() # Continue on failure
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
- name: 'Fetch Version Name 📝'
run: |
echo "::set-output name=VERSION_NAME::$(${{github.workspace}}/gradlew properties --no-daemon --console=plain -q | grep "^version:" | awk '{printf $2}')"
id: fetch-version
- name: Get Version
run: |
echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV

70
.github/workflows/ci_1.20.1.yml vendored Normal file
View File

@@ -0,0 +1,70 @@
name: CI Tests
on:
push:
branches: [ 'minecraft/1.20.1' ]
paths-ignore:
- 'docs/**'
- 'workflows/**'
- 'README.md'
permissions:
contents: read
checks: write
jobs:
build:
name: 'Build - 1.20.1'
runs-on: ubuntu-latest
steps:
- name: 'Setup JDK 21 📦'
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: 'Setup Gradle 8.8 🏗️'
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: '8.8'
- name: 'Checkout for CI 🛎️'
uses: actions/checkout@v4
with:
ref: 'minecraft/1.20.1'
env:
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
- name: '[Current - 1.20.1] Build 🛎️'
run: |
./gradlew clean build publish
- name: 'Publish Test Report 📊'
uses: mikepenz/action-junit-report@v4
if: success() || failure() # Continue on failure
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
- name: 'Fetch Version String 📝'
run: |
echo "::set-output name=VERSION_NAME::$(./gradlew properties --no-daemon --console=plain -q | grep "^version:" | awk '{printf $2}')"
id: fetch-version
- name: 'Set Version Variable 📝'
run: |
echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV
- name: 'Publish to William278.net 🚀'
uses: WiIIiam278/bones-publish-action@v1
with:
api-key: ${{ secrets.BONES_API_KEY }}
project: 'husksync'
channel: 'alpha'
version: ${{ env.version_name }}
changelog: ${{ github.event.head_commit.message }}
distro-names: |
paper-1.20.1
fabric-1.20.1
distro-groups: |
paper
fabric
distro-descriptions: |
Paper 1.20.1
Fabric 1.20.1
files: |
target/HuskSync-Paper-${{ env.version_name }}+mc.1.20.1.jar
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.20.1.jar

68
.github/workflows/ci_1.21.1.yml vendored Normal file
View File

@@ -0,0 +1,68 @@
name: CI Tests
on:
push:
branches: [ 'master' ]
paths-ignore:
- 'docs/**'
- 'workflows/**'
- 'README.md'
permissions:
contents: read
checks: write
jobs:
build:
name: 'Build - 1.21.1'
runs-on: ubuntu-latest
steps:
- name: 'Setup JDK 21 📦'
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: 'Setup Gradle 8.8 🏗️'
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: '8.8'
- name: 'Checkout for CI 🛎️'
uses: actions/checkout@v4
env:
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
- name: '[Current - 1.21.1] Build 🛎️'
run: |
./gradlew clean build publish
- name: 'Publish Test Report 📊'
uses: mikepenz/action-junit-report@v4
if: success() || failure() # Continue on failure
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
- name: 'Fetch Version String 📝'
run: |
echo "::set-output name=VERSION_NAME::$(./gradlew properties --no-daemon --console=plain -q | grep "^version:" | awk '{printf $2}')"
id: fetch-version
- name: 'Set Version Variable 📝'
run: |
echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV
- name: 'Publish to William278.net 🚀'
uses: WiIIiam278/bones-publish-action@v1
with:
api-key: ${{ secrets.BONES_API_KEY }}
project: 'husksync'
channel: 'alpha'
version: ${{ env.version_name }}
changelog: ${{ github.event.head_commit.message }}
distro-names: |
paper-1.21.1
fabric-1.21.1
distro-groups: |
paper
fabric
distro-descriptions: |
Paper 1.21.1
Fabric 1.21.1
files: |
target/HuskSync-Paper-${{ env.version_name }}+mc.1.21.1.jar
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.1.jar

View File

@@ -14,10 +14,10 @@ jobs:
steps: steps:
- name: 'Checkout for CI 🛎' - name: 'Checkout for CI 🛎'
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: 'Set up JDK 17 📦' - name: 'Set up JDK 21 📦'
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: '17' java-version: '21'
distribution: 'temurin' distribution: 'temurin'
- name: 'Build with Gradle 🏗️' - name: 'Build with Gradle 🏗️'
uses: gradle/gradle-build-action@v3 uses: gradle/gradle-build-action@v3

View File

@@ -8,26 +8,76 @@ permissions:
contents: read contents: read
checks: write checks: write
jobs: jobs:
build: build:
name: 'Publish Release'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout for CI 🛎️' - name: 'Setup JDK 21 📦'
uses: actions/checkout@v4
- name: 'Set up JDK 17 📦'
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: '17' java-version: '21'
distribution: 'temurin' distribution: 'temurin'
- name: 'Build with Gradle 🏗️' - name: 'Setup Gradle 8.8 🏗️'
uses: gradle/gradle-build-action@v3 uses: gradle/actions/setup-gradle@v4
with: with:
arguments: build test publish gradle-version: '8.8'
- name: '[Current - 1.21.1] Checkout for CI 🛎️'
uses: actions/checkout@v4
with:
path: '1_21_1'
- name: '[LTS - 1.20.1] Checkout for CI 🛎️'
uses: actions/checkout@v4
with:
ref: 'minecraft/1.20.1'
path: '1_20_1'
env: env:
RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
RELEASES_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} RELEASES_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
- name: '[Current - 1.21.1] Build 🛎️'
run: |
mkdir target
cd 1_21_1
./gradlew clean build publish -Dforce-hide-version-meta=1
cp -rf target/* ../target/
cd ..
- name: '[LTS - 1.20.1] Build 🛎️'
run: |
cd 1_20_1
./gradlew clean build publish -Dforce-hide-version-meta=1
cp -rf target/* ../target/
cd ..
- name: 'Publish Test Report 📊' - name: 'Publish Test Report 📊'
uses: mikepenz/action-junit-report@v4 uses: mikepenz/action-junit-report@v4
if: success() || failure() # Continue on failure if: success() || failure() # Continue on failure
with: with:
report_paths: '**/build/test-results/test/TEST-*.xml' report_paths: '**/build/test-results/test/TEST-*.xml'
- name: 'Publish to William278.net 🚀'
uses: WiIIiam278/bones-publish-action@v1
with:
api-key: ${{ secrets.BONES_API_KEY }}
project: 'husksync'
channel: 'release'
version: ${{ github.event.release.tag_name }}
changelog: ${{ github.event.release.body }}
distro-names: |
paper-1.21.1
fabric-1.21.1
paper-1.20.1
fabric-1.20.1
distro-groups: |
paper
fabric
paper
fabric
distro-descriptions: |
Paper 1.21.1
Fabric 1.21.1
Paper 1.20.1
Fabric 1.20.1
files: |
target/HuskSync-Paper-${{ github.event.release.tag_name }}+mc.1.21.1.jar
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.1.jar
target/HuskSync-Paper-${{ github.event.release.tag_name }}+mc.1.20.1.jar
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.20.1.jar

View File

@@ -13,7 +13,8 @@ permissions:
contents: write contents: write
jobs: jobs:
deploy-wiki: update-docs:
name: 'Update Docs'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout for CI 🛎️' - name: 'Checkout for CI 🛎️'

View File

@@ -1,8 +1,8 @@
<!--suppress ALL --> <!--suppress ALL -->
<p align="center"> <p align="center">
<img src="images/banner.png" alt="HuskSync" /> <img src="images/banner.png" alt="HuskSync" />
<a href="https://github.com/WiIIiam278/HuskSync/actions/workflows/ci.yml"> <a href="https://github.com/WiIIiam278/HuskSync/actions/workflows/ci_1.21.1.yml">
<img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/ci.yml?branch=master&logo=github"/> <img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/ci_1.21.1.yml?branch=master&logo=github"/>
</a> </a>
<a href="https://repo.william278.net/#/releases/net/william278/husksync/"> <a href="https://repo.william278.net/#/releases/net/william278/husksync/">
<img src="https://repo.william278.net/api/badge/latest/releases/net/william278/husksync/husksync-common?color=00fb9a&name=Maven&prefix=v" /> <img src="https://repo.william278.net/api/badge/latest/releases/net/william278/husksync/husksync-common?color=00fb9a&name=Maven&prefix=v" />
@@ -43,8 +43,27 @@
**Ready?** [It's syncing time!](https://william278.net/docs/husksync/setup) **Ready?** [It's syncing time!](https://william278.net/docs/husksync/setup)
## Compatibility
HuskSync supports the following [compatible versions](https://william278.net/docs/husksync/compatibility) of Minecraft. Since v3.7, you must download the correct version of HuskSync for your server:
| Minecraft | Latest HuskSync | Java Version | Platforms | Support Ends |
|:---------------:|:---------------:|:------------:|:--------------|:--------------------------|
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **Active Release** |
| 1.20.6 | 3.6.8 | 17 | Paper | ❌ _October 2024_ |
| 1.20.4 | 3.6.8 | 17 | Paper | ❌ _July 2024_ |
| 1.20.1 | _latest_ | 17 | Paper, Fabric | ✅ **November 2025** (LTS) |
| 1.17.1 - 1.19.4 | 3.6.8 | 17 | Paper | ❌ _Support ended_ |
| 1.16.5 | 3.2.1 | 16 | Paper | ❌ _Support ended_ |
HuskSync is primarily developed against the latest release. Old Minecraft versions are allocated a support channel based on popularity, mod support, etc:
* Long Term Support (LTS) &ndash; Supported for up to 12-18 months
* Non-Long Term Support (Non-LTS) &ndash; Supported for 3-6 months
Verify your purchase on Discord and [Download HuskSync](https://william278.net/project/husksync#download) for your server.
## Setup ## Setup
Requires a MySQL/Mongo/PostgreSQL database, a Redis (v5.0+) server and a network of Spigot (1.17.1+) or Fabric (1.20.1) Minecraft servers, running Java 17+. Requires a MySQL/Mongo/PostgreSQL database, a Redis (v5.0+) server and a network of Spigot or Fabric Minecraft servers (see [Compatibility](#compatibility)).
1. Place the plugin jar file in the `/plugins` or `/mods` directory of each Spigot/Fabric server. You do not need to install HuskSync as a proxy plugin. 1. Place the plugin jar file in the `/plugins` or `/mods` directory of each Spigot/Fabric server. You do not need to install HuskSync as a proxy plugin.
2. Start, then stop every server to let HuskSync generate the config file. 2. Start, then stop every server to let HuskSync generate the config file.
@@ -52,7 +71,7 @@ Requires a MySQL/Mongo/PostgreSQL database, a Redis (v5.0+) server and a network
4. Start every server again and synchronization will begin. 4. Start every server again and synchronization will begin.
## Development ## Development
To build HuskSync, simply run the following in the root of the repository (building requires Java 17). Builds will be output in `/target`: To build HuskSync, simply run the following in the root of the repository (building requires Java 21). Builds will be output in `/target`:
```bash ```bash
./gradlew clean build ./gradlew clean build

View File

@@ -1,7 +1,7 @@
import org.apache.tools.ant.filters.ReplaceTokens import org.apache.tools.ant.filters.ReplaceTokens
plugins { plugins {
id 'com.github.johnrengelman.shadow' version '8.1.1' id 'com.gradleup.shadow' version '8.3.2'
id 'org.cadixdev.licenser' version '0.6.1' apply false id 'org.cadixdev.licenser' version '0.6.1' apply false
id 'fabric-loom' version '1.7-SNAPSHOT' apply false id 'fabric-loom' version '1.7-SNAPSHOT' apply false
id 'org.ajoberstar.grgit' version '5.2.2' id 'org.ajoberstar.grgit' version '5.2.2'
@@ -18,6 +18,7 @@ ext {
set 'version', version.toString() set 'version', version.toString()
set 'description', description.toString() set 'description', description.toString()
set 'minecraft_version', minecraft_version.toString()
set 'jedis_version', jedis_version.toString() set 'jedis_version', jedis_version.toString()
set 'mysql_driver_version', mysql_driver_version.toString() set 'mysql_driver_version', mysql_driver_version.toString()
set 'mariadb_driver_version', mariadb_driver_version.toString() set 'mariadb_driver_version', mariadb_driver_version.toString()
@@ -58,12 +59,12 @@ publishing {
} }
allprojects { allprojects {
apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'com.gradleup.shadow'
apply plugin: 'org.cadixdev.licenser' apply plugin: 'org.cadixdev.licenser'
apply plugin: 'java' apply plugin: 'java'
compileJava.options.encoding = 'UTF-8' compileJava.options.encoding = 'UTF-8'
compileJava.options.release.set 17 compileJava.options.release.set Integer.parseInt(rootProject.ext.javaVersion)
javadoc.options.encoding = 'UTF-8' javadoc.options.encoding = 'UTF-8'
javadoc.options.addStringOption('Xdoclint:none', '-quiet') javadoc.options.addStringOption('Xdoclint:none', '-quiet')
@@ -73,6 +74,7 @@ allprojects {
maven { url 'https://repo.william278.net/releases/' } maven { url 'https://repo.william278.net/releases/' }
maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
maven { url 'https://repo.papermc.io/repository/maven-public/' }
maven { url "https://repo.dmulloy2.net/repository/public/" } maven { url "https://repo.dmulloy2.net/repository/public/" }
maven { url 'https://repo.codemc.io/repository/maven-public/' } maven { url 'https://repo.codemc.io/repository/maven-public/' }
maven { url 'https://repo.minebench.de/' } maven { url 'https://repo.minebench.de/' }
@@ -83,9 +85,9 @@ allprojects {
} }
dependencies { dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.1'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.1'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.1'
} }
test { test {
@@ -99,9 +101,11 @@ allprojects {
} }
processResources { processResources {
def tokenMap = rootProject.ext.properties
tokenMap.merge("grgit",'',(s, s2) -> s)
filesMatching(['**/*.json', '**/*.yml']) { filesMatching(['**/*.json', '**/*.yml']) {
filter ReplaceTokens as Class, beginToken: '${', endToken: '}', filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
tokens: rootProject.ext.properties tokens: tokenMap
} }
} }
} }
@@ -123,9 +127,9 @@ subprojects {
archiveClassifier.set('') archiveClassifier.set('')
} }
// Append the Minecraft to the version for Fabric projects // Append the compatible Minecraft version to the version
if (project.name == 'fabric') { if (['bukkit', 'paper', 'fabric'].contains(project.name)) {
version += "+mc.${fabric_minecraft_version}" version += "+mc.${minecraft_version}"
} }
// API publishing // API publishing
@@ -161,7 +165,7 @@ subprojects {
mavenJavaBukkit(MavenPublication) { mavenJavaBukkit(MavenPublication) {
groupId = 'net.william278.husksync' groupId = 'net.william278.husksync'
artifactId = 'husksync-bukkit' artifactId = 'husksync-bukkit'
version = "$rootProject.version" version = "$rootProject.version+${minecraft_version}"
artifact shadowJar artifact shadowJar
artifact sourcesJar artifact sourcesJar
artifact javadocJar artifact javadocJar
@@ -174,7 +178,7 @@ subprojects {
mavenJavaFabric(MavenPublication) { mavenJavaFabric(MavenPublication) {
groupId = 'net.william278.husksync' groupId = 'net.william278.husksync'
artifactId = 'husksync-fabric' artifactId = 'husksync-fabric'
version = "$rootProject.version+${fabric_minecraft_version}" version = "$rootProject.version+${minecraft_version}"
artifact remapJar artifact remapJar
artifact sourcesJar artifact sourcesJar
artifact javadocJar artifact javadocJar
@@ -188,10 +192,15 @@ subprojects {
clean.delete "$rootDir/target" clean.delete "$rootDir/target"
} }
logger.lifecycle("Building HuskSync ${version} by William278") logger.lifecycle("Building HuskSync ${version} by William278 for Minecraft ${minecraft_version}")
@SuppressWarnings('GrMethodMayBeStatic') @SuppressWarnings('GrMethodMayBeStatic')
def versionMetadata() { def versionMetadata() {
// If the force-hide-version-meta environment variable is set, return ''
if (System.getProperty('force-hide-version-meta') != null) {
return ''
}
// Require grgit // Require grgit
if (grgit == null) { if (grgit == null) {
return '-unknown' return '-unknown'

View File

@@ -1,31 +1,30 @@
dependencies { dependencies {
implementation project(path: ':common') implementation project(path: ':common')
implementation 'net.william278.uniform:uniform-bukkit:1.1.4' implementation 'net.william278.uniform:uniform-bukkit:1.2.1'
implementation 'net.william278:mpdbdataconverter:1.0.1' implementation 'net.william278:mpdbdataconverter:1.0.1'
implementation 'net.william278:hsldataconverter:1.0' implementation 'net.william278:hsldataconverter:1.0'
implementation 'net.william278:mapdataapi:1.0.3' implementation 'net.william278:mapdataapi:1.0.3'
implementation 'net.william278:andjam:1.0.2' implementation 'org.bstats:bstats-bukkit:3.1.0'
implementation 'org.bstats:bstats-bukkit:3.0.2' implementation 'net.kyori:adventure-platform-bukkit:4.3.4'
implementation 'net.kyori:adventure-platform-bukkit:4.3.3'
implementation 'dev.triumphteam:triumph-gui:3.1.10' implementation 'dev.triumphteam:triumph-gui:3.1.10'
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4' implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
implementation 'de.tr7zw:item-nbt-api:2.13.1-SNAPSHOT' implementation 'de.tr7zw:item-nbt-api:2.13.2'
compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT' compileOnly "org.spigotmc:spigot-api:${bukkit_spigot_api}"
compileOnly 'com.github.retrooper.packetevents:spigot:2.3.0' compileOnly 'com.github.retrooper.packetevents:spigot:2.3.0'
compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0' compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0'
compileOnly 'org.projectlombok:lombok:1.18.32' compileOnly 'org.projectlombok:lombok:1.18.34'
compileOnly 'commons-io:commons-io:2.16.1' compileOnly 'commons-io:commons-io:2.17.0'
compileOnly 'org.json:json:20240303' compileOnly 'org.json:json:20240303'
compileOnly 'net.william278:minedown:1.8.2' compileOnly 'net.william278:minedown:1.8.2'
compileOnly 'de.exlll:configlib-yaml:4.5.0' compileOnly 'de.exlll:configlib-yaml:4.5.0'
compileOnly 'com.zaxxer:HikariCP:5.1.0' compileOnly 'com.zaxxer:HikariCP:6.0.0'
compileOnly 'net.william278:DesertWell:2.0.4' compileOnly 'net.william278:DesertWell:2.0.4'
compileOnly 'net.william278:AdvancementAPI:97a9583413' compileOnly 'net.william278:AdvancementAPI:97a9583413'
compileOnly "redis.clients:jedis:$jedis_version" compileOnly "redis.clients:jedis:$jedis_version"
annotationProcessor 'org.projectlombok:lombok:1.18.32' annotationProcessor 'org.projectlombok:lombok:1.18.34'
} }
shadowJar { shadowJar {
@@ -46,7 +45,6 @@ shadowJar {
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell' relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown' relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi' relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
relocate 'net.william278.andjam', 'net.william278.husksync.libraries.andjam'
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter' relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter' relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
relocate 'org.json', 'net.william278.husksync.libraries.json' relocate 'org.json', 'net.william278.husksync.libraries.json'

View File

@@ -137,6 +137,9 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
public void onEnable() { public void onEnable() {
this.audiences = BukkitAudiences.create(this); this.audiences = BukkitAudiences.create(this);
// Check compatibility
checkCompatibility();
// Register commands // Register commands
initialize("commands", (plugin) -> getUniform().register(PluginCommand.Type.create(this))); initialize("commands", (plugin) -> getUniform().register(PluginCommand.Type.create(this)));
@@ -290,7 +293,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@Override @Override
public boolean isDependencyLoaded(@NotNull String name) { public boolean isDependencyLoaded(@NotNull String name) {
final Plugin plugin = getServer().getPluginManager().getPlugin(name); final Plugin plugin = getServer().getPluginManager().getPlugin(name);
return plugin != null && plugin.isEnabled(); return plugin != null;
} }
// Register bStats metrics // Register bStats metrics
@@ -333,6 +336,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
return PLATFORM_TYPE_ID; return PLATFORM_TYPE_ID;
} }
@Override
@NotNull
public String getServerVersion() {
return String.format("%s/%s", getServer().getName(), getServer().getVersion());
}
@Override @Override
public Optional<LegacyConverter> getLegacyConverter() { public Optional<LegacyConverter> getLegacyConverter() {
return Optional.of(legacyConverter); return Optional.of(legacyConverter);

View File

@@ -62,7 +62,7 @@ public class BukkitHuskSyncAPI extends HuskSyncAPI {
public static BukkitHuskSyncAPI getInstance() { public static BukkitHuskSyncAPI getInstance() {
if (!JavaPlugin.getProvidingPlugin(BukkitHuskSyncAPI.class).getName().equals("HuskSync")) { if (!JavaPlugin.getProvidingPlugin(BukkitHuskSyncAPI.class).getName().equals("HuskSync")) {
throw new NotRegisteredException("This is likely because you have shaded HuskSync into your plugin JAR " + throw new NotRegisteredException("This is likely because you have shaded HuskSync into your plugin JAR " +
"and need to fix your maven/gradle/build script so that it *compiles against* HuskSync instead."); "and need to fix your maven/gradle/build script so that it *compiles against* HuskSync instead.");
} }
if (instance == null) { if (instance == null) {
throw new NotRegisteredException(); throw new NotRegisteredException();

View File

@@ -26,21 +26,19 @@ import de.tr7zw.changeme.nbtapi.NBTCompound;
import de.tr7zw.changeme.nbtapi.NBTPersistentDataContainer; import de.tr7zw.changeme.nbtapi.NBTPersistentDataContainer;
import lombok.*; import lombok.*;
import net.william278.desertwell.util.ThrowingConsumer; import net.william278.desertwell.util.ThrowingConsumer;
import net.william278.desertwell.util.Version;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.Adaptable; import net.william278.husksync.adapter.Adaptable;
import net.william278.husksync.config.Settings.SynchronizationSettings.AttributeSettings;
import net.william278.husksync.user.BukkitUser; import net.william278.husksync.user.BukkitUser;
import org.bukkit.Bukkit; import org.bukkit.*;
import org.bukkit.Material;
import org.bukkit.Registry;
import org.bukkit.Statistic;
import org.bukkit.advancement.AdvancementProgress; import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.attribute.AttributeInstance; import org.bukkit.attribute.AttributeInstance;
import org.bukkit.attribute.AttributeModifier; import org.bukkit.attribute.AttributeModifier;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.EquipmentSlotGroup;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffect;
@@ -48,6 +46,7 @@ import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range; import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable;
import java.util.*; import java.util.*;
import java.util.logging.Level; import java.util.logging.Level;
@@ -156,20 +155,17 @@ public abstract class BukkitData implements Data {
this.clearInventoryCraftingSlots(player); this.clearInventoryCraftingSlots(player);
player.setItemOnCursor(null); player.setItemOnCursor(null);
player.getInventory().setContents(plugin.setMapViews(getContents())); player.getInventory().setContents(plugin.setMapViews(getContents()));
player.updateInventory();
player.getInventory().setHeldItemSlot(heldItemSlot); player.getInventory().setHeldItemSlot(heldItemSlot);
//noinspection UnstableApiUsage
player.updateInventory();
} }
private void clearInventoryCraftingSlots(@NotNull Player player) { private void clearInventoryCraftingSlots(@NotNull Player player) {
try { final org.bukkit.inventory.Inventory inventory = player.getOpenInventory().getTopInventory();
final org.bukkit.inventory.Inventory inventory = player.getOpenInventory().getTopInventory(); if (inventory.getType() == InventoryType.CRAFTING) {
if (inventory.getType() == InventoryType.CRAFTING) { for (int slot = 0; slot < 5; slot++) {
for (int slot = 0; slot < 5; slot++) { inventory.setItem(slot, null);
inventory.setItem(slot, null);
}
} }
} catch (Throwable e) {
// Ignore any exceptions
} }
} }
@@ -236,8 +232,9 @@ public abstract class BukkitData implements Data {
private final Collection<PotionEffect> effects; private final Collection<PotionEffect> effects;
@NotNull @NotNull
public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> effects) { public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> sei) {
return new BukkitData.PotionEffects(effects); return new BukkitData.PotionEffects(Lists.newArrayList(sei.stream().filter(e -> !e.isAmbient()).toList()));
} }
@NotNull @NotNull
@@ -261,7 +258,7 @@ public abstract class BukkitData implements Data {
@NotNull @NotNull
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static BukkitData.PotionEffects empty() { public static BukkitData.PotionEffects empty() {
return new BukkitData.PotionEffects(List.of()); return new BukkitData.PotionEffects(Lists.newArrayList());
} }
@Override @Override
@@ -277,10 +274,11 @@ public abstract class BukkitData implements Data {
@NotNull @NotNull
@Override @Override
@Unmodifiable
public List<Effect> getActiveEffects() { public List<Effect> getActiveEffects() {
return effects.stream() return effects.stream()
.map(potionEffect -> new Effect( .map(potionEffect -> new Effect(
potionEffect.getType().getName().toLowerCase(Locale.ENGLISH), potionEffect.getType().getKey().toString(),
potionEffect.getAmplifier(), potionEffect.getAmplifier(),
potionEffect.getDuration(), potionEffect.getDuration(),
potionEffect.isAmbient(), potionEffect.isAmbient(),
@@ -364,7 +362,7 @@ public abstract class BukkitData implements Data {
// Set player experience and level (prevent advancement awards applying twice), reset game rule // Set player experience and level (prevent advancement awards applying twice), reset game rule
if (!toAward.isEmpty() if (!toAward.isEmpty()
&& (player.getLevel() != expLevel || player.getExp() != expProgress)) { && (player.getLevel() != expLevel || player.getExp() != expProgress)) {
player.setLevel(expLevel); player.setLevel(expLevel);
player.setExp(expProgress); player.setExp(expProgress);
} }
@@ -455,9 +453,10 @@ public abstract class BukkitData implements Data {
Registry.STATISTIC.forEach(id -> { Registry.STATISTIC.forEach(id -> {
switch (id.getType()) { switch (id.getType()) {
case UNTYPED -> addStatistic(player, id, generic); case UNTYPED -> addStatistic(player, id, generic);
case BLOCK -> addMaterialStatistic(player, id, blocks, true); // Todo - Future - Use BLOCK and ITEM registries when API stabilizes
case ITEM -> addMaterialStatistic(player, id, items, false); case BLOCK -> addStatistic(player, id, Registry.MATERIAL, blocks);
case ENTITY -> addEntityStatistic(player, id, entities); case ITEM -> addStatistic(player, id, Registry.MATERIAL, items);
case ENTITY -> addStatistic(player, id, Registry.ENTITY_TYPE, entities);
} }
}); });
return new BukkitData.Statistics(generic, blocks, items, entities); return new BukkitData.Statistics(generic, blocks, items, entities);
@@ -478,43 +477,31 @@ public abstract class BukkitData implements Data {
} }
} }
private static void addMaterialStatistic(@NotNull Player p, @NotNull Statistic id, private static <R extends Keyed> void addStatistic(@NotNull Player p, @NotNull Statistic id,
@NotNull Map<String, Map<String, Integer>> map, boolean isBlock) { @NotNull Registry<R> registry,
Registry.MATERIAL.forEach(material -> { @NotNull Map<String, Map<String, Integer>> map) {
if ((material.isBlock() && !isBlock) || (material.isItem() && isBlock)) { registry.forEach(i -> {
return; try {
} final int stat = i instanceof Material m ? p.getStatistic(id, m) :
final int stat = p.getStatistic(id, material); (i instanceof EntityType e ? p.getStatistic(id, e) : -1);
if (stat != 0) { if (stat != 0) {
map.computeIfAbsent(id.getKey().getKey(), k -> Maps.newHashMap()) map.compute(id.getKey().getKey(), (k, v) -> v == null ? Maps.newHashMap() : v)
.put(material.getKey().getKey(), stat); .put(i.getKey().getKey(), stat);
} }
}); } catch (IllegalStateException ignored) {
}
private static void addEntityStatistic(@NotNull Player p, @NotNull Statistic id,
@NotNull Map<String, Map<String, Integer>> map) {
Registry.ENTITY_TYPE.forEach(entity -> {
if (!entity.isAlive()) {
return;
}
final int stat = p.getStatistic(id, entity);
if (stat != 0) {
map.computeIfAbsent(id.getKey().getKey(), k -> Maps.newHashMap())
.put(entity.getKey().getKey(), stat);
} }
}); });
} }
@Override @Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) { public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync p) {
genericStatistics.forEach((id, v) -> applyStat(user, id, Statistic.Type.UNTYPED, v)); genericStatistics.forEach((k, v) -> applyStat(p, user, k, Statistic.Type.UNTYPED, v));
blockStatistics.forEach((id, m) -> m.forEach((b, v) -> applyStat(user, id, Statistic.Type.BLOCK, v, b))); blockStatistics.forEach((k, m) -> m.forEach((b, v) -> applyStat(p, user, k, Statistic.Type.BLOCK, v, b)));
itemStatistics.forEach((id, m) -> m.forEach((i, v) -> applyStat(user, id, Statistic.Type.ITEM, v, i))); itemStatistics.forEach((k, m) -> m.forEach((i, v) -> applyStat(p, user, k, Statistic.Type.ITEM, v, i)));
entityStatistics.forEach((id, m) -> m.forEach((e, v) -> applyStat(user, id, Statistic.Type.ENTITY, v, e))); entityStatistics.forEach((k, m) -> m.forEach((e, v) -> applyStat(p, user, k, Statistic.Type.ENTITY, v, e)));
} }
private void applyStat(@NotNull UserDataHolder user, @NotNull String id, private void applyStat(@NotNull HuskSync plugin, @NotNull UserDataHolder user, @NotNull String id,
@NotNull Statistic.Type type, int value, @NotNull String... key) { @NotNull Statistic.Type type, int value, @NotNull String... key) {
final Player player = ((BukkitUser) user).getPlayer(); final Player player = ((BukkitUser) user).getPlayer();
final Statistic stat = matchStatistic(id); final Statistic stat = matchStatistic(id);
@@ -528,7 +515,8 @@ public abstract class BukkitData implements Data {
case BLOCK, ITEM -> player.setStatistic(stat, Objects.requireNonNull(matchMaterial(key[0])), value); case BLOCK, ITEM -> player.setStatistic(stat, Objects.requireNonNull(matchMaterial(key[0])), value);
case ENTITY -> player.setStatistic(stat, Objects.requireNonNull(matchEntityType(key[0])), value); case ENTITY -> player.setStatistic(stat, Objects.requireNonNull(matchEntityType(key[0])), value);
} }
} catch (Throwable ignored) { } catch (Throwable a) {
plugin.log(Level.WARNING, "Failed to apply statistic " + id, a);
} }
} }
@@ -563,6 +551,7 @@ public abstract class BukkitData implements Data {
@Getter @Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE) @AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings("UnstableApiUsage")
public static class Attributes extends BukkitData implements Data.Attributes, Adaptable { public static class Attributes extends BukkitData implements Data.Attributes, Adaptable {
private List<Attribute> attributes; private List<Attribute> attributes;
@@ -570,14 +559,13 @@ public abstract class BukkitData implements Data {
@NotNull @NotNull
public static BukkitData.Attributes adapt(@NotNull Player player, @NotNull HuskSync plugin) { public static BukkitData.Attributes adapt(@NotNull Player player, @NotNull HuskSync plugin) {
final List<Attribute> attributes = Lists.newArrayList(); final List<Attribute> attributes = Lists.newArrayList();
final AttributeSettings settings = plugin.getSettings().getSynchronization().getAttributes();
Registry.ATTRIBUTE.forEach(id -> { Registry.ATTRIBUTE.forEach(id -> {
final AttributeInstance instance = player.getAttribute(id); final AttributeInstance instance = player.getAttribute(id);
if (instance == null || instance.getValue() == instance.getDefaultValue() || plugin if (settings.isIgnoredAttribute(id.getKey().toString()) || instance == null) {
.getSettings().getSynchronization().isIgnoredAttribute(id.getKey().toString())) { return; // We don't sync attributes not marked as to be synced
// We don't sync unmodified or disabled attributes
return;
} }
attributes.add(adapt(instance, plugin.getMinecraftVersion())); attributes.add(adapt(instance, settings));
}); });
return new BukkitData.Attributes(attributes); return new BukkitData.Attributes(attributes);
} }
@@ -596,47 +584,63 @@ public abstract class BukkitData implements Data {
} }
@NotNull @NotNull
private static Attribute adapt(@NotNull AttributeInstance instance, @NotNull Version version) { private static Attribute adapt(@NotNull AttributeInstance instance, @NotNull AttributeSettings settings) {
return new Attribute( return new Attribute(
instance.getAttribute().getKey().toString(), instance.getAttribute().getKey().toString(),
instance.getBaseValue(), instance.getBaseValue(),
instance.getModifiers().stream().map(m -> adapt(m, version)).collect(Collectors.toSet()) instance.getModifiers().stream()
.filter(modifier -> !settings.isIgnoredModifier(modifier.getName()))
.filter(modifier -> modifier.getSlotGroup() != EquipmentSlotGroup.ANY)
.map(BukkitData.Attributes::adapt).collect(Collectors.toSet())
); );
} }
@NotNull @NotNull
private static Modifier adapt(@NotNull AttributeModifier modifier, @NotNull Version version) { private static Modifier adapt(@NotNull AttributeModifier modifier) {
return new Modifier( return new Modifier(
version.compareTo(Version.fromString("1.21")) >= 0 ? null : modifier.getUniqueId(), modifier.getKey().toString(),
modifier.getName(),
modifier.getAmount(), modifier.getAmount(),
modifier.getOperation().ordinal(), modifier.getOperation().ordinal(),
modifier.getSlot() != null ? modifier.getSlot().ordinal() : -1 modifier.getSlotGroup().toString()
); );
} }
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
Registry.ATTRIBUTE.forEach(id -> applyAttribute(user.getPlayer().getAttribute(id), getAttribute(id).orElse(null)));
}
private static void applyAttribute(@Nullable AttributeInstance instance, @Nullable Attribute attribute) { private static void applyAttribute(@Nullable AttributeInstance instance, @Nullable Attribute attribute) {
if (instance == null) { if (instance == null) {
return; return;
} }
instance.setBaseValue(attribute == null ? instance.getDefaultValue() : attribute.baseValue());
instance.getModifiers().forEach(instance::removeModifier); instance.getModifiers().forEach(instance::removeModifier);
instance.setBaseValue(attribute == null ? instance.getValue() : attribute.baseValue());
if (attribute != null) { if (attribute != null) {
attribute.modifiers().forEach(modifier -> instance.addModifier(new AttributeModifier( attribute.modifiers().stream()
modifier.uuid(), .filter(mod -> instance.getModifiers().stream().map(AttributeModifier::getName)
modifier.name(), .noneMatch(n -> n.equals(mod.name())))
modifier.amount(), .distinct().filter(mod -> !mod.hasUuid())
AttributeModifier.Operation.values()[modifier.operationType()], .forEach(mod -> instance.addModifier(adapt(mod)));
modifier.equipmentSlot() != -1 ? EquipmentSlot.values()[modifier.equipmentSlot()] : null
)));
} }
} }
@NotNull
private static AttributeModifier adapt(@NotNull Modifier modifier) {
return new AttributeModifier(
Objects.requireNonNull(NamespacedKey.fromString(modifier.name())),
modifier.amount(),
AttributeModifier.Operation.values()[modifier.operation()],
Optional.ofNullable(EquipmentSlotGroup.getByName(modifier.slotGroup())).orElse(EquipmentSlotGroup.ANY)
);
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
final AttributeSettings settings = plugin.getSettings().getSynchronization().getAttributes();
Registry.ATTRIBUTE.forEach(id -> {
if (settings.isIgnoredAttribute(id.getKey().toString())) {
return;
}
applyAttribute(user.getPlayer().getAttribute(id), getAttribute(id).orElse(null));
});
}
} }
@Getter @Getter
@@ -696,11 +700,12 @@ public abstract class BukkitData implements Data {
} }
// Set health scale // Set health scale
double scale = healthScale <= 0 ? player.getMaxHealth() : healthScale;
try { try {
player.setHealthScale(healthScale); player.setHealthScale(scale);
player.setHealthScaled(isHealthScaled); player.setHealthScaled(isHealthScaled);
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.WARNING, "Error setting %s's health scale to %s".formatted(player.getName(), healthScale), e); plugin.log(Level.WARNING, "Error setting %s's health scale to %s".formatted(player.getName(), scale), e);
} }
} }

View File

@@ -165,6 +165,7 @@ public class BukkitSerializer {
case "1.20", "1.20.1", "1.20.2" -> DataFixerUtil.VERSION1_20_2; case "1.20", "1.20.1", "1.20.2" -> DataFixerUtil.VERSION1_20_2;
case "1.20.3", "1.20.4" -> DataFixerUtil.VERSION1_20_4; case "1.20.3", "1.20.4" -> DataFixerUtil.VERSION1_20_4;
case "1.20.5", "1.20.6" -> DataFixerUtil.VERSION1_20_5; case "1.20.5", "1.20.6" -> DataFixerUtil.VERSION1_20_5;
case "1.21" -> DataFixerUtil.VERSION1_21;
default -> DataFixerUtil.getCurrentVersion(); default -> DataFixerUtil.getCurrentVersion();
}; };
} }

View File

@@ -23,6 +23,7 @@ import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketListenerAbstract; import com.github.retrooper.packetevents.event.PacketListenerAbstract;
import com.github.retrooper.packetevents.event.PacketListenerPriority; import com.github.retrooper.packetevents.event.PacketListenerPriority;
import com.github.retrooper.packetevents.event.PacketReceiveEvent; import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType; import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder; import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
@@ -78,7 +79,20 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
@Override @Override
public void onPacketReceive(PacketReceiveEvent event) { public void onPacketReceive(PacketReceiveEvent event) {
if(!(event.getPacketType() instanceof PacketType.Play.Client client)) { if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
return;
}
if (!CANCEL_PACKETS.contains(client)) {
return;
}
if (listener.cancelPlayerEvent(event.getUser().getUUID())) {
event.setCancelled(true);
}
}
@Override
public void onPacketSend(PacketSendEvent event) {
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
return; return;
} }
if (!CANCEL_PACKETS.contains(client)) { if (!CANCEL_PACKETS.contains(client)) {

View File

@@ -204,10 +204,10 @@ public class LegacyMigrator extends Migrator {
}) { }) {
plugin.log(Level.INFO, getHelpMenu()); plugin.log(Level.INFO, getHelpMenu());
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " + plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1])); obfuscateDataString(args[1]));
} else { } else {
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " + plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
obfuscateDataString(args[1]) + " (is it a valid option?)"); obfuscateDataString(args[1]) + " (is it a valid option?)");
} }
} else { } else {
plugin.log(Level.INFO, getHelpMenu()); plugin.log(Level.INFO, getHelpMenu());

View File

@@ -201,10 +201,10 @@ public class MpdbMigrator extends Migrator {
}) { }) {
plugin.log(Level.INFO, getHelpMenu()); plugin.log(Level.INFO, getHelpMenu());
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " + plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1])); obfuscateDataString(args[1]));
} else { } else {
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " + plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
obfuscateDataString(args[1]) + " (is it a valid option?)"); obfuscateDataString(args[1]) + " (is it a valid option?)");
} }
} else { } else {
plugin.log(Level.INFO, getHelpMenu()); plugin.log(Level.INFO, getHelpMenu());
@@ -255,7 +255,7 @@ public class MpdbMigrator extends Migrator {
If any of these are not correct, please correct them If any of these are not correct, please correct them
using the command: using the command:
"husksync migrate mpdb set <parameter> <value>" "husksync migrate mpdb set <parameter> <value>"
(e.g.: "husksync migrate mpdb set host 1.2.3.4") (e.g.: "husksync migrate set mpdb host 1.2.3.4")
STEP 3] HuskSync will migrate data into the database STEP 3] HuskSync will migrate data into the database
tables configures in the config.yml file of this tables configures in the config.yml file of this
@@ -263,7 +263,7 @@ public class MpdbMigrator extends Migrator {
before proceeding. before proceeding.
STEP 4] To start the migration, please run: STEP 4] To start the migration, please run:
"husksync migrate mpdb start" "husksync migrate start mpdb"
NOTE: This migrator currently WORKS WITH MPDB version NOTE: This migrator currently WORKS WITH MPDB version
v4.9.2 and below! v4.9.2 and below!

View File

@@ -23,14 +23,10 @@ import de.themoep.minedown.adventure.MineDown;
import dev.triumphteam.gui.builder.gui.StorageBuilder; import dev.triumphteam.gui.builder.gui.StorageBuilder;
import dev.triumphteam.gui.guis.Gui; import dev.triumphteam.gui.guis.Gui;
import dev.triumphteam.gui.guis.StorageGui; import dev.triumphteam.gui.guis.StorageGui;
import net.roxeez.advancement.display.FrameType;
import net.william278.andjam.Toast;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.BukkitData; import net.william278.husksync.data.BukkitData;
import net.william278.husksync.data.BukkitUserDataHolder; import net.william278.husksync.data.BukkitUserDataHolder;
import net.william278.husksync.data.Data; import net.william278.husksync.data.Data;
import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
@@ -40,8 +36,6 @@ import java.util.Arrays;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.Level; import java.util.logging.Level;
import static net.william278.husksync.util.BukkitKeyedAdapter.matchMaterial;
/** /**
* Bukkit platform implementation of an {@link OnlineUser} * Bukkit platform implementation of an {@link OnlineUser}
*/ */
@@ -68,20 +62,12 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
} }
@Override @Override
@Deprecated(since = "3.6.7")
public void sendToast(@NotNull MineDown title, @NotNull MineDown description, public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
@NotNull String iconMaterial, @NotNull String backgroundType) { @NotNull String iconMaterial, @NotNull String backgroundType) {
try { plugin.log(Level.WARNING, "Toast notifications are deprecated. " +
final Material material = matchMaterial(iconMaterial); "Please change your notification display slot to CHAT, ACTION_BAR or NONE.");
Toast.builder((BukkitHuskSync) plugin) this.sendActionBar(title);
.setTitle(title.toComponent())
.setDescription(description.toComponent())
.setIcon(material != null ? material : Material.BARRIER)
.setFrameType(FrameType.valueOf(backgroundType))
.build()
.show(player);
} catch (Throwable e) {
plugin.log(Level.WARNING, "Failed to send toast to player " + player.getName(), e);
}
} }
@Override @Override

View File

@@ -51,7 +51,7 @@ public final class BukkitKeyedAdapter {
@Nullable @Nullable
public static PotionEffectType matchEffectType(@NotNull String key) { public static PotionEffectType matchEffectType(@NotNull String key) {
return PotionEffectType.getByName(key); // No registry for this in 1.17 API return getRegistryValue(Registry.EFFECT, key);
} }
private static <T extends Keyed> T getRegistryValue(@NotNull Registry<T> registry, @NotNull String keyString) { private static <T extends Keyed> T getRegistryValue(@NotNull Registry<T> registry, @NotNull String keyString) {

View File

@@ -31,7 +31,7 @@ import net.william278.mapdataapi.MapData;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.ShulkerBox; import org.bukkit.block.Container;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BlockStateMeta; import org.bukkit.inventory.meta.BlockStateMeta;
@@ -96,7 +96,7 @@ public interface BukkitMapPersister {
} }
if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) { if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) {
items[i] = function.apply(item); items[i] = function.apply(item);
} else if (item.getItemMeta() instanceof BlockStateMeta b && b.getBlockState() instanceof ShulkerBox box) { } else if (item.getItemMeta() instanceof BlockStateMeta b && b.getBlockState() instanceof Container box) {
forEachMap(box.getInventory().getContents(), function); forEachMap(box.getInventory().getContents(), function);
b.setBlockState(box); b.setBlockState(box);
} }
@@ -155,8 +155,8 @@ public interface BukkitMapPersister {
Optional<String> world = Optional.empty(); Optional<String> world = Optional.empty();
for (String worldUid : mapIds.getKeys()) { for (String worldUid : mapIds.getKeys()) {
world = getPlugin().getServer().getWorlds().stream() world = getPlugin().getServer().getWorlds().stream()
.map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid)) .map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid))
.findFirst(); .findFirst();
if (world.isPresent()) { if (world.isPresent()) {
break; break;
} }
@@ -179,7 +179,7 @@ public interface BukkitMapPersister {
try { try {
getPlugin().debug("Deserializing map data from NBT and generating view..."); getPlugin().debug("Deserializing map data from NBT and generating view...");
canvasData = MapData.fromByteArray(Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY), canvasData = MapData.fromByteArray(Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY),
"Map pixel data is null")); "Map pixel data is null"));
} catch (Throwable e) { } catch (Throwable e) {
getPlugin().log(Level.WARNING, "Failed to deserialize map data from NBT", e); getPlugin().log(Level.WARNING, "Failed to deserialize map data from NBT", e);
return; return;
@@ -195,8 +195,8 @@ public interface BukkitMapPersister {
// Set the map view ID in NBT // Set the map view ID in NBT
NBT.modify(map, editable -> { NBT.modify(map, editable -> {
Objects.requireNonNull(editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY), Objects.requireNonNull(editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY),
"Map view ID mappings compound is null") "Map view ID mappings compound is null")
.setInteger(worldUid, view.getId()); .setInteger(worldUid, view.getId());
}); });
getPlugin().debug(String.format("Generated view (#%s) and updated map (UID: %s)", view.getId(), worldUid)); getPlugin().debug(String.format("Generated view (#%s) and updated map (UID: %s)", view.getId(), worldUid));
}); });
@@ -276,7 +276,7 @@ public interface BukkitMapPersister {
@NotNull @NotNull
private static World getDefaultMapWorld() { private static World getDefaultMapWorld() {
final World world = Bukkit.getWorlds().get(0); final World world = Bukkit.getWorlds().getFirst();
if (world == null) { if (world == null) {
throw new IllegalStateException("No worlds are loaded on the server!"); throw new IllegalStateException("No worlds are loaded on the server!");
} }
@@ -308,7 +308,7 @@ public interface BukkitMapPersister {
// We set the pixels in this order to avoid the map being rendered upside down // We set the pixels in this order to avoid the map being rendered upside down
for (int i = 0; i < 128; i++) { for (int i = 0; i < 128; i++) {
for (int j = 0; j < 128; j++) { for (int j = 0; j < 128; j++) {
canvas.setPixel(j, i, (byte) canvasData.getColorAt(i, j)); canvas.setPixelColor(j, i, canvasData.getMapColorAt(i, j));
} }
} }
@@ -326,29 +326,29 @@ public interface BukkitMapPersister {
@NotNull @NotNull
private static MapCursor createBannerCursor(@NotNull MapBanner banner) { private static MapCursor createBannerCursor(@NotNull MapBanner banner) {
return new MapCursor( return new MapCursor(
(byte) banner.getPosition().getX(), (byte) banner.getPosition().getX(),
(byte) banner.getPosition().getZ(), (byte) banner.getPosition().getZ(),
(byte) 8, // Always rotate banners upright (byte) 8, // Always rotate banners upright
switch (banner.getColor().toLowerCase(Locale.ENGLISH)) { switch (banner.getColor().toLowerCase(Locale.ENGLISH)) {
case "white" -> MapCursor.Type.BANNER_WHITE; case "white" -> MapCursor.Type.BANNER_WHITE;
case "orange" -> MapCursor.Type.BANNER_ORANGE; case "orange" -> MapCursor.Type.BANNER_ORANGE;
case "magenta" -> MapCursor.Type.BANNER_MAGENTA; case "magenta" -> MapCursor.Type.BANNER_MAGENTA;
case "light_blue" -> MapCursor.Type.BANNER_LIGHT_BLUE; case "light_blue" -> MapCursor.Type.BANNER_LIGHT_BLUE;
case "yellow" -> MapCursor.Type.BANNER_YELLOW; case "yellow" -> MapCursor.Type.BANNER_YELLOW;
case "lime" -> MapCursor.Type.BANNER_LIME; case "lime" -> MapCursor.Type.BANNER_LIME;
case "pink" -> MapCursor.Type.BANNER_PINK; case "pink" -> MapCursor.Type.BANNER_PINK;
case "gray" -> MapCursor.Type.BANNER_GRAY; case "gray" -> MapCursor.Type.BANNER_GRAY;
case "light_gray" -> MapCursor.Type.BANNER_LIGHT_GRAY; case "light_gray" -> MapCursor.Type.BANNER_LIGHT_GRAY;
case "cyan" -> MapCursor.Type.BANNER_CYAN; case "cyan" -> MapCursor.Type.BANNER_CYAN;
case "purple" -> MapCursor.Type.BANNER_PURPLE; case "purple" -> MapCursor.Type.BANNER_PURPLE;
case "blue" -> MapCursor.Type.BANNER_BLUE; case "blue" -> MapCursor.Type.BANNER_BLUE;
case "brown" -> MapCursor.Type.BANNER_BROWN; case "brown" -> MapCursor.Type.BANNER_BROWN;
case "green" -> MapCursor.Type.BANNER_GREEN; case "green" -> MapCursor.Type.BANNER_GREEN;
case "red" -> MapCursor.Type.BANNER_RED; case "red" -> MapCursor.Type.BANNER_RED;
default -> MapCursor.Type.BANNER_BLACK; default -> MapCursor.Type.BANNER_BLACK;
}, },
true, true,
banner.getText().isEmpty() ? null : banner.getText() banner.getText().isEmpty() ? null : banner.getText()
); );
} }
@@ -383,20 +383,40 @@ public interface BukkitMapPersister {
} }
@Override @Override
@Deprecated
public void setPixel(int x, int y, byte color) { public void setPixel(int x, int y, byte color) {
pixels[x][y] = color; pixels[x][y] = color;
} }
@Override @Override
@Deprecated
public byte getPixel(int x, int y) { public byte getPixel(int x, int y) {
return (byte) pixels[x][y]; return (byte) pixels[x][y];
} }
@Override @Override
@Deprecated
public byte getBasePixel(int x, int y) { public byte getBasePixel(int x, int y) {
return getPixel(x, y); return getPixel(x, y);
} }
@Override
public void setPixelColor(int i, int i1, @Nullable Color color) {
pixels[i][i1] = color == null ? 0 : color.getRGB();
}
@Nullable
@Override
public Color getPixelColor(int x, int y) {
return getBasePixelColor(x, y);
}
@NotNull
@Override
public Color getBasePixelColor(int x, int y) {
return new Color(pixels[x][y]);
}
@Override @Override
public void drawImage(int x, int y, @NotNull Image image) { public void drawImage(int x, int y, @NotNull Image image) {
// Not implemented // Not implemented
@@ -427,16 +447,17 @@ public interface BukkitMapPersister {
final String BANNER_PREFIX = "banner_"; final String BANNER_PREFIX = "banner_";
for (int i = 0; i < getCursors().size(); i++) { for (int i = 0; i < getCursors().size(); i++) {
final MapCursor cursor = getCursors().getCursor(i); final MapCursor cursor = getCursors().getCursor(i);
final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH); final String type = cursor.getType().getKey().getKey();
if (type.startsWith(BANNER_PREFIX)) { if (type.startsWith(BANNER_PREFIX)) {
banners.add(new MapBanner( banners.add(new MapBanner(
type.replaceAll(BANNER_PREFIX, ""), type.replaceAll(BANNER_PREFIX, ""),
cursor.getCaption() == null ? "" : cursor.getCaption(), cursor.getCaption() == null ? "" : cursor.getCaption(),
cursor.getX(), cursor.getX(),
mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128, mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128,
cursor.getY() cursor.getY()
)); ));
} }
} }
return MapData.fromPixels(pixels, getDimension(), (byte) 2, banners, List.of()); return MapData.fromPixels(pixels, getDimension(), (byte) 2, banners, List.of());
} }

View File

@@ -3,7 +3,7 @@ plugins {
} }
dependencies { dependencies {
api 'commons-io:commons-io:2.16.1' api 'commons-io:commons-io:2.17.0'
api 'org.apache.commons:commons-text:1.12.0' api 'org.apache.commons:commons-text:1.12.0'
api 'net.william278:minedown:1.8.2' api 'net.william278:minedown:1.8.2'
api 'org.json:json:20240303' api 'org.json:json:20240303'
@@ -12,17 +12,17 @@ dependencies {
api 'de.exlll:configlib-yaml:4.5.0' api 'de.exlll:configlib-yaml:4.5.0'
api 'net.william278:paginedown:1.1.2' api 'net.william278:paginedown:1.1.2'
api 'net.william278:DesertWell:2.0.4' api 'net.william278:DesertWell:2.0.4'
api('com.zaxxer:HikariCP:5.1.0') { api('com.zaxxer:HikariCP:6.0.0') {
exclude module: 'slf4j-api' exclude module: 'slf4j-api'
} }
compileOnly 'net.william278.uniform:uniform-common:1.1.4' compileOnly 'net.william278.uniform:uniform-common:1.2.1'
compileOnly 'com.mojang:brigadier:1.1.8' compileOnly 'com.mojang:brigadier:1.1.8'
compileOnly 'org.projectlombok:lombok:1.18.32' compileOnly 'org.projectlombok:lombok:1.18.34'
compileOnly 'org.jetbrains:annotations:24.1.0' compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'net.kyori:adventure-api:4.17.0' compileOnly 'net.kyori:adventure-api:4.17.0'
compileOnly 'net.kyori:adventure-platform-api:4.3.3' compileOnly 'net.kyori:adventure-platform-api:4.3.4'
compileOnly 'com.google.guava:guava:33.2.1-jre' compileOnly 'com.google.guava:guava:33.3.1-jre'
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272' compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
compileOnly "redis.clients:jedis:$jedis_version" compileOnly "redis.clients:jedis:$jedis_version"
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version" compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
@@ -33,10 +33,10 @@ dependencies {
testImplementation "redis.clients:jedis:$jedis_version" testImplementation "redis.clients:jedis:$jedis_version"
testImplementation "org.xerial.snappy:snappy-java:$snappy_version" testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
testImplementation 'com.google.guava:guava:33.2.1-jre' testImplementation 'com.google.guava:guava:33.3.1-jre'
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272' testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
testCompileOnly 'de.exlll:configlib-yaml:4.5.0' testCompileOnly 'de.exlll:configlib-yaml:4.5.0'
testCompileOnly 'org.jetbrains:annotations:24.1.0' testCompileOnly 'org.jetbrains:annotations:24.1.0'
annotationProcessor 'org.projectlombok:lombok:1.18.32' annotationProcessor 'org.projectlombok:lombok:1.18.34'
} }

View File

@@ -39,6 +39,7 @@ import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.sync.DataSyncer; import net.william278.husksync.sync.DataSyncer;
import net.william278.husksync.user.ConsoleUser; import net.william278.husksync.user.ConsoleUser;
import net.william278.husksync.user.OnlineUser; import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.util.CompatibilityChecker;
import net.william278.husksync.util.LegacyConverter; import net.william278.husksync.util.LegacyConverter;
import net.william278.husksync.util.Task; import net.william278.husksync.util.Task;
import net.william278.uniform.Uniform; import net.william278.uniform.Uniform;
@@ -52,7 +53,8 @@ import java.util.logging.Level;
/** /**
* Abstract implementation of the HuskSync plugin. * Abstract implementation of the HuskSync plugin.
*/ */
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider, SerializerRegistry { public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider, SerializerRegistry,
CompatibilityChecker {
int SPIGOT_RESOURCE_ID = 97144; int SPIGOT_RESOURCE_ID = 97144;
@@ -255,6 +257,14 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
@NotNull @NotNull
String getPlatformType(); String getPlatformType();
/**
* Returns the server software version
*
* @return the server software version string
*/
@NotNull
String getServerVersion();
/** /**
* Returns the legacy data converter if it exists * Returns the legacy data converter if it exists
* *
@@ -265,10 +275,10 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
@NotNull @NotNull
default UpdateChecker getUpdateChecker() { default UpdateChecker getUpdateChecker() {
return UpdateChecker.builder() return UpdateChecker.builder()
.currentVersion(getPluginVersion()) .currentVersion(getPluginVersion())
.endpoint(UpdateChecker.Endpoint.SPIGOT) .endpoint(UpdateChecker.Endpoint.SPIGOT)
.resource(Integer.toString(SPIGOT_RESOURCE_ID)) .resource(Integer.toString(SPIGOT_RESOURCE_ID))
.build(); .build();
} }
default void checkForUpdates() { default void checkForUpdates() {
@@ -276,8 +286,8 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
getUpdateChecker().check().thenAccept(checked -> { getUpdateChecker().check().thenAccept(checked -> {
if (!checked.isUpToDate()) { if (!checked.isUpToDate()) {
log(Level.WARNING, String.format( log(Level.WARNING, String.format(
"A new version of HuskSync is available: v%s (running v%s)", "A new version of HuskSync is available: v%s (running v%s)",
checked.getLatestVersion(), getPluginVersion()) checked.getLatestVersion(), getPluginVersion())
); );
} }
}); });
@@ -320,17 +330,21 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
final class FailedToLoadException extends IllegalStateException { final class FailedToLoadException extends IllegalStateException {
private static final String FORMAT = """ private static final String FORMAT = """
HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized. HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized.
Please make sure the plugin has been setup correctly (https://william278.net/docs/husksync/setup): Please make sure the plugin has been setup correctly (https://william278.net/docs/husksync/setup):
1) Make sure you've entered your MySQL, MariaDB or MongoDB database details correctly in config.yml 1) Make sure you've entered your MySQL, MariaDB or MongoDB database details correctly in config.yml
2) Make sure your Redis server details are also correct in config.yml 2) Make sure your Redis server details are also correct in config.yml
3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-file) 3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-file)
4) Check the error below for more details 4) Check the error below for more details
Caused by: %s"""; Caused by: %s""";
FailedToLoadException(@NotNull String message, @NotNull Throwable cause) { public FailedToLoadException(@NotNull String message) {
super(String.format(FORMAT, message));
}
public FailedToLoadException(@NotNull String message, @NotNull Throwable cause) {
super(String.format(FORMAT, message), cause); super(String.format(FORMAT, message), cause);
} }

View File

@@ -46,29 +46,29 @@ public class EnderChestCommand extends ItemsCommand {
final Optional<Data.Items.EnderChest> optionalEnderChest = snapshot.getEnderChest(); final Optional<Data.Items.EnderChest> optionalEnderChest = snapshot.getEnderChest();
if (optionalEnderChest.isEmpty()) { if (optionalEnderChest.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return; return;
} }
// Display opening message // Display opening message
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(), plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(),
snapshot.getTimestamp().format(DateTimeFormatter snapshot.getTimestamp().format(DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT))) .ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
// Show GUI // Show GUI
final Data.Items.EnderChest enderChest = optionalEnderChest.get(); final Data.Items.EnderChest enderChest = optionalEnderChest.get();
viewer.showGui( viewer.showGui(
enderChest, enderChest,
plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getUsername()) plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getUsername())
.orElse(new MineDown(String.format("%s's Ender Chest", user.getUsername()))), .orElse(new MineDown(String.format("%s's Ender Chest", user.getUsername()))),
allowEdit, allowEdit,
enderChest.getSlotCount(), enderChest.getSlotCount(),
(itemsOnClose) -> { (itemsOnClose) -> {
if (allowEdit && !enderChest.equals(itemsOnClose)) { if (allowEdit && !enderChest.equals(itemsOnClose)) {
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user)); plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
}
} }
}
); );
} }
@@ -78,7 +78,7 @@ public class EnderChestCommand extends ItemsCommand {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder); final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) { if (latestData.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return; return;
} }
@@ -88,7 +88,7 @@ public class EnderChestCommand extends ItemsCommand {
data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items)); data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items));
data.setSaveCause(DataSnapshot.SaveCause.ENDERCHEST_COMMAND); data.setSaveCause(DataSnapshot.SaveCause.ENDERCHEST_COMMAND);
data.setPinned( data.setPinned(
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND) plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND)
); );
}); });

View File

@@ -30,9 +30,11 @@ import net.kyori.adventure.text.format.TextColor;
import net.william278.desertwell.about.AboutMenu; import net.william278.desertwell.about.AboutMenu;
import net.william278.desertwell.util.UpdateChecker; import net.william278.desertwell.util.UpdateChecker;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.database.Database; import net.william278.husksync.database.Database;
import net.william278.husksync.migrator.Migrator; import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.user.CommandUser; import net.william278.husksync.user.CommandUser;
import net.william278.husksync.util.LegacyConverter;
import net.william278.uniform.BaseCommand; import net.william278.uniform.BaseCommand;
import net.william278.uniform.CommandProvider; import net.william278.uniform.CommandProvider;
import net.william278.uniform.Permission; import net.william278.uniform.Permission;
@@ -40,8 +42,10 @@ import net.william278.uniform.element.ArgumentElement;
import org.apache.commons.text.WordUtils; import org.apache.commons.text.WordUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.time.OffsetDateTime;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.UUID;
import java.util.function.Function; import java.util.function.Function;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -52,41 +56,41 @@ public class HuskSyncCommand extends PluginCommand {
private final AboutMenu aboutMenu; private final AboutMenu aboutMenu;
public HuskSyncCommand(@NotNull HuskSync plugin) { public HuskSyncCommand(@NotNull HuskSync plugin) {
super("husksync", List.of(), Permission.Default.TRUE, plugin); super("husksync", List.of(), Permission.Default.TRUE, ExecutionScope.ALL, plugin);
this.updateChecker = plugin.getUpdateChecker(); this.updateChecker = plugin.getUpdateChecker();
this.aboutMenu = AboutMenu.builder() this.aboutMenu = AboutMenu.builder()
.title(Component.text("HuskSync")) .title(Component.text("HuskSync"))
.description(Component.text("A modern, cross-server player data synchronization system")) .description(Component.text("A modern, cross-server player data synchronization system"))
.version(plugin.getPluginVersion()) .version(plugin.getPluginVersion())
.credits("Author", .credits("Author",
AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net")) AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net"))
.credits("Contributors", .credits("Contributors",
AboutMenu.Credit.of("HarvelsX").description("Code"), AboutMenu.Credit.of("HarvelsX").description("Code"),
AboutMenu.Credit.of("HookWoods").description("Code"), AboutMenu.Credit.of("HookWoods").description("Code"),
AboutMenu.Credit.of("Preva1l").description("Code"), AboutMenu.Credit.of("Preva1l").description("Code"),
AboutMenu.Credit.of("hanbings").description("Code (Fabric porting)"), AboutMenu.Credit.of("hanbings").description("Code (Fabric porting)"),
AboutMenu.Credit.of("Stampede2011").description("Code (Fabric mixins)")) AboutMenu.Credit.of("Stampede2011").description("Code (Fabric mixins)"))
.credits("Translators", .credits("Translators",
AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"), AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"),
AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"), AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"),
AboutMenu.Credit.of("Melonzio").description("Spanish (es-es)"), AboutMenu.Credit.of("Melonzio").description("Spanish (es-es)"),
AboutMenu.Credit.of("Ceddix").description("German (de-de)"), AboutMenu.Credit.of("Ceddix").description("German (de-de)"),
AboutMenu.Credit.of("Pukejoy_1").description("Bulgarian (bg-bg)"), AboutMenu.Credit.of("Pukejoy_1").description("Bulgarian (bg-bg)"),
AboutMenu.Credit.of("mateusneresrb").description("Brazilian Portuguese (pt-br)"), AboutMenu.Credit.of("mateusneresrb").description("Brazilian Portuguese (pt-br)"),
AboutMenu.Credit.of("小蔡").description("Traditional Chinese (zh-tw)"), AboutMenu.Credit.of("小蔡").description("Traditional Chinese (zh-tw)"),
AboutMenu.Credit.of("Ghost-chu").description("Simplified Chinese (zh-cn)"), AboutMenu.Credit.of("Ghost-chu").description("Simplified Chinese (zh-cn)"),
AboutMenu.Credit.of("DJelly4K").description("Simplified Chinese (zh-cn)"), AboutMenu.Credit.of("DJelly4K").description("Simplified Chinese (zh-cn)"),
AboutMenu.Credit.of("Thourgard").description("Ukrainian (uk-ua)"), AboutMenu.Credit.of("Thourgard").description("Ukrainian (uk-ua)"),
AboutMenu.Credit.of("xF3d3").description("Italian (it-it)"), AboutMenu.Credit.of("xF3d3").description("Italian (it-it)"),
AboutMenu.Credit.of("cada3141").description("Korean (ko-kr)"), AboutMenu.Credit.of("cada3141").description("Korean (ko-kr)"),
AboutMenu.Credit.of("Wirayuda5620").description("Indonesian (id-id)"), AboutMenu.Credit.of("Wirayuda5620").description("Indonesian (id-id)"),
AboutMenu.Credit.of("WinTone01").description("Turkish (tr-tr)"), AboutMenu.Credit.of("WinTone01").description("Turkish (tr-tr)"),
AboutMenu.Credit.of("IbanEtchep").description("French (fr-fr)")) AboutMenu.Credit.of("IbanEtchep").description("French (fr-fr)"))
.buttons( .buttons(
AboutMenu.Link.of("https://william278.net/docs/husksync").text("Documentation").icon(""), AboutMenu.Link.of("https://william278.net/docs/husksync").text("Documentation").icon(""),
AboutMenu.Link.of("https://github.com/WiIIiam278/HuskSync/issues").text("Issues").icon("").color(TextColor.color(0xff9f0f)), AboutMenu.Link.of("https://github.com/WiIIiam278/HuskSync/issues").text("Issues").icon("").color(TextColor.color(0xff9f0f)),
AboutMenu.Link.of("https://discord.gg/tVYhJfyDWG").text("Discord").icon("").color(TextColor.color(0x6773f5))) AboutMenu.Link.of("https://discord.gg/tVYhJfyDWG").text("Discord").icon("").color(TextColor.color(0x6773f5)))
.build(); .build();
} }
@Override @Override
@@ -96,6 +100,7 @@ public class HuskSyncCommand extends PluginCommand {
command.addSubCommand("status", needsOp("status"), status()); command.addSubCommand("status", needsOp("status"), status());
command.addSubCommand("reload", needsOp("reload"), reload()); command.addSubCommand("reload", needsOp("reload"), reload());
command.addSubCommand("update", needsOp("update"), update()); command.addSubCommand("update", needsOp("update"), update());
command.addSubCommand("forceupgrade", forceUpgrade());
command.addSubCommand("migrate", migrate()); command.addSubCommand("migrate", migrate());
} }
@@ -109,8 +114,8 @@ public class HuskSyncCommand extends PluginCommand {
final CommandUser user = user(sub, ctx); final CommandUser user = user(sub, ctx);
plugin.getLocales().getLocale("system_status_header").ifPresent(user::sendMessage); plugin.getLocales().getLocale("system_status_header").ifPresent(user::sendMessage);
user.sendMessage(Component.join( user.sendMessage(Component.join(
JoinConfiguration.newlines(), JoinConfiguration.newlines(),
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList() Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList()
)); ));
}); });
} }
@@ -126,7 +131,7 @@ public class HuskSyncCommand extends PluginCommand {
plugin.getLocales().getLocale("reload_complete").ifPresent(user::sendMessage); plugin.getLocales().getLocale("reload_complete").ifPresent(user::sendMessage);
} catch (Throwable e) { } catch (Throwable e) {
user.sendMessage(new MineDown( user.sendMessage(new MineDown(
"[Error:](#ff3300) [Failed to reload the plugin. Check console for errors.](#ff7e5e)" "[Error:](#ff3300) [Failed to reload the plugin. Check console for errors.](#ff7e5e)"
)); ));
plugin.log(Level.SEVERE, "Failed to reload the plugin", e); plugin.log(Level.SEVERE, "Failed to reload the plugin", e);
} }
@@ -139,11 +144,11 @@ public class HuskSyncCommand extends PluginCommand {
final CommandUser user = user(sub, ctx); final CommandUser user = user(sub, ctx);
if (checked.isUpToDate()) { if (checked.isUpToDate()) {
plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString()) plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString())
.ifPresent(user::sendMessage); .ifPresent(user::sendMessage);
return; return;
} }
plugin.getLocales().getLocale("update_available", checked.getLatestVersion().toString(), plugin.getLocales().getLocale("update_available", checked.getLatestVersion().toString(),
plugin.getPluginVersion().toString()).ifPresent(user::sendMessage); plugin.getPluginVersion().toString()).ifPresent(user::sendMessage);
})); }));
} }
@@ -152,14 +157,18 @@ public class HuskSyncCommand extends PluginCommand {
return (sub) -> { return (sub) -> {
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole()); sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
sub.setDefaultExecutor((ctx) -> { sub.setDefaultExecutor((ctx) -> {
plugin.log(Level.INFO, "Please choose a migrator, then run \"husksync migrate <migrator>\""); plugin.log(Level.INFO, "Please choose a migrator, then run \"husksync migrate start <migrator>\"");
plugin.log(Level.INFO, String.format( plugin.log(Level.INFO, String.format(
"List of available migrators:\nMigrator ID / Migrator Name:\n%s", "List of available migrators:\nMigrator ID / Migrator Name:\n%s",
plugin.getAvailableMigrators().stream() plugin.getAvailableMigrators().stream()
.map(migrator -> String.format("%s - %s", migrator.getIdentifier(), migrator.getName())) .map(migrator -> String.format("%s - %s", migrator.getIdentifier(), migrator.getName()))
.collect(Collectors.joining("\n")) .collect(Collectors.joining("\n"))
)); ));
}); });
sub.addSubCommand("help", (help) -> help.addSyntax((cmd) -> {
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
plugin.log(Level.INFO, migrator.getHelpMenu());
}, migrator()));
sub.addSubCommand("start", (start) -> start.addSyntax((cmd) -> { sub.addSubCommand("start", (start) -> start.addSyntax((cmd) -> {
final Migrator migrator = cmd.getArgument("migrator", Migrator.class); final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
migrator.start().thenAccept(succeeded -> { migrator.start().thenAccept(succeeded -> {
@@ -173,17 +182,46 @@ public class HuskSyncCommand extends PluginCommand {
sub.addSubCommand("set", (set) -> set.addSyntax((cmd) -> { sub.addSubCommand("set", (set) -> set.addSyntax((cmd) -> {
final Migrator migrator = cmd.getArgument("migrator", Migrator.class); final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
final String[] args = cmd.getArgument("args", String.class).split(" "); final String[] args = cmd.getArgument("args", String.class).split(" ");
migrator.handleConfigurationCommand(Arrays.copyOfRange(args, 3, args.length)); migrator.handleConfigurationCommand(args);
}, migrator(), BaseCommand.greedyString("args"))); }, migrator(), BaseCommand.greedyString("args")));
}; };
} }
@NotNull
private CommandProvider forceUpgrade() {
return (sub) -> {
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
sub.setDefaultExecutor((ctx) -> {
final LegacyConverter converter = plugin.getLegacyConverter().orElse(null);
if (converter == null) {
return;
}
plugin.runAsync(() -> {
final Database database = plugin.getDatabase();
plugin.log(Level.INFO, "Beginning forced legacy data upgrade for all users...");
database.getAllUsers().forEach(user -> database.getLatestSnapshot(user).ifPresent(snapshot -> {
final DataSnapshot.Packed upgraded = converter.convert(
snapshot.asBytes(plugin),
UUID.randomUUID(),
OffsetDateTime.now()
);
upgraded.setSaveCause(DataSnapshot.SaveCause.CONVERTED_FROM_V2);
plugin.getDatabase().addSnapshot(user, upgraded);
plugin.getRedisManager().clearUserData(user);
}));
plugin.log(Level.INFO, "Legacy data upgrade complete!");
});
});
};
}
@NotNull @NotNull
private <S> ArgumentElement<S, Migrator> migrator() { private <S> ArgumentElement<S, Migrator> migrator() {
return new ArgumentElement<>("migrator", reader -> { return new ArgumentElement<>("migrator", reader -> {
final String id = reader.readString(); final String id = reader.readString();
final Migrator migrator = plugin.getAvailableMigrators().stream() final Migrator migrator = plugin.getAvailableMigrators().stream()
.filter(m -> m.getIdentifier().equalsIgnoreCase(id)).findFirst().orElse(null); .filter(m -> m.getIdentifier().equalsIgnoreCase(id)).findFirst().orElse(null);
if (migrator == null) { if (migrator == null) {
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader); throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
} }
@@ -198,54 +236,54 @@ public class HuskSyncCommand extends PluginCommand {
private enum StatusLine { private enum StatusLine {
PLUGIN_VERSION(plugin -> Component.text("v" + plugin.getPluginVersion().toStringWithoutMetadata()) PLUGIN_VERSION(plugin -> Component.text("v" + plugin.getPluginVersion().toStringWithoutMetadata())
.appendSpace().append(plugin.getPluginVersion().getMetadata().isBlank() ? Component.empty() .appendSpace().append(plugin.getPluginVersion().getMetadata().isBlank() ? Component.empty()
: Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))), : Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))),
PLATFORM_TYPE(plugin -> Component.text(WordUtils.capitalizeFully(plugin.getPlatformType()))), SERVER_VERSION(plugin -> Component.text(plugin.getServerVersion())),
LANGUAGE(plugin -> Component.text(plugin.getSettings().getLanguage())), LANGUAGE(plugin -> Component.text(plugin.getSettings().getLanguage())),
MINECRAFT_VERSION(plugin -> Component.text(plugin.getMinecraftVersion().toString())), MINECRAFT_VERSION(plugin -> Component.text(plugin.getMinecraftVersion().toString())),
JAVA_VERSION(plugin -> Component.text(System.getProperty("java.version"))), JAVA_VERSION(plugin -> Component.text(System.getProperty("java.version"))),
JAVA_VENDOR(plugin -> Component.text(System.getProperty("java.vendor"))), JAVA_VENDOR(plugin -> Component.text(System.getProperty("java.vendor"))),
SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully(
plugin.getSettings().getSynchronization().getMode().toString()
))),
DELAY_LATENCY(plugin -> Component.text(
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms"
)),
SERVER_NAME(plugin -> Component.text(plugin.getServerName())), SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())), CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())),
SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully(
plugin.getSettings().getSynchronization().getMode().toString()
))),
DELAY_LATENCY(plugin -> Component.text(
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms"
)),
DATABASE_TYPE(plugin -> DATABASE_TYPE(plugin ->
Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() + Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() +
(plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ? (plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ?
(plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : "")) (plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : ""))
), ),
IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())), IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())),
USING_REDIS_SENTINEL(plugin -> getBoolean( USING_REDIS_SENTINEL(plugin -> getBoolean(
!plugin.getSettings().getRedis().getSentinel().getMaster().isBlank() !plugin.getSettings().getRedis().getSentinel().getMaster().isBlank()
)), )),
USING_REDIS_PASSWORD(plugin -> getBoolean( USING_REDIS_PASSWORD(plugin -> getBoolean(
!plugin.getSettings().getRedis().getCredentials().getPassword().isBlank() !plugin.getSettings().getRedis().getCredentials().getPassword().isBlank()
)), )),
REDIS_USING_SSL(plugin -> getBoolean( REDIS_USING_SSL(plugin -> getBoolean(
plugin.getSettings().getRedis().getCredentials().isUseSsl() plugin.getSettings().getRedis().getCredentials().isUseSsl()
)), )),
IS_REDIS_LOCAL(plugin -> getLocalhostBoolean( IS_REDIS_LOCAL(plugin -> getLocalhostBoolean(
plugin.getSettings().getRedis().getCredentials().getHost() plugin.getSettings().getRedis().getCredentials().getHost()
)), )),
DATA_TYPES(plugin -> Component.join( DATA_TYPES(plugin -> Component.join(
JoinConfiguration.commas(true), JoinConfiguration.commas(true),
plugin.getRegisteredDataTypes().stream().map(i -> Component.textOfChildren(Component.text(i.toString()) plugin.getRegisteredDataTypes().stream().map(i -> Component.textOfChildren(Component.text(i.toString())
.appendSpace().append(Component.text(i.isEnabled() ? '✔' : '❌'))) .appendSpace().append(Component.text(i.isEnabled() ? '✔' : '❌')))
.color(i.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED) .color(i.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED)
.hoverEvent(HoverEvent.showText( .hoverEvent(HoverEvent.showText(
Component.text(i.isEnabled() ? "Enabled" : "Disabled") Component.text(i.isEnabled() ? "Enabled" : "Disabled")
.append(Component.newline()) .append(Component.newline())
.append(Component.text("Dependencies: %s".formatted(i.getDependencies() .append(Component.text("Dependencies: %s".formatted(i.getDependencies()
.isEmpty() ? "(None)" : i.getDependencies().stream() .isEmpty() ? "(None)" : i.getDependencies().stream()
.map(d -> "%s (%s)".formatted( .map(d -> "%s (%s)".formatted(
d.getKey().value(), d.isRequired() ? "Required" : "Optional" d.getKey().value(), d.isRequired() ? "Required" : "Optional"
)).collect(Collectors.joining(", "))) )).collect(Collectors.joining(", ")))
).color(NamedTextColor.GRAY)) ).color(NamedTextColor.GRAY))
))).toList() ))).toList()
)); ));
private final Function<HuskSync, Component> supplier; private final Function<HuskSync, Component> supplier;
@@ -257,13 +295,13 @@ public class HuskSyncCommand extends PluginCommand {
@NotNull @NotNull
private Component get(@NotNull HuskSync plugin) { private Component get(@NotNull HuskSync plugin) {
return Component return Component
.text("").appendSpace() .text("").appendSpace()
.append(Component.text( .append(Component.text(
WordUtils.capitalizeFully(name().replaceAll("_", " ")), WordUtils.capitalizeFully(name().replaceAll("_", " ")),
TextColor.color(0x848484) TextColor.color(0x848484)
)) ))
.append(Component.text(':')).append(Component.space().color(NamedTextColor.WHITE)) .append(Component.text(':')).append(Component.space().color(NamedTextColor.WHITE))
.append(supplier.apply(plugin)); .append(supplier.apply(plugin));
} }
@NotNull @NotNull
@@ -274,7 +312,7 @@ public class HuskSyncCommand extends PluginCommand {
@NotNull @NotNull
private static Component getLocalhostBoolean(@NotNull String value) { private static Component getLocalhostBoolean(@NotNull String value) {
return getBoolean(value.equals("127.0.0.1") || value.equals("0.0.0.0") return getBoolean(value.equals("127.0.0.1") || value.equals("0.0.0.0")
|| value.equals("localhost") || value.equals("::1")); || value.equals("localhost") || value.equals("::1"));
} }
} }

View File

@@ -47,29 +47,29 @@ public class InventoryCommand extends ItemsCommand {
if (optionalInventory.isEmpty()) { if (optionalInventory.isEmpty()) {
viewer.sendMessage(new MineDown("what the FUCK is happening")); viewer.sendMessage(new MineDown("what the FUCK is happening"));
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return; return;
} }
// Display opening message // Display opening message
plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(), plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(),
snapshot.getTimestamp().format(DateTimeFormatter snapshot.getTimestamp().format(DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT))) .ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
// Show GUI // Show GUI
final Data.Items.Inventory inventory = optionalInventory.get(); final Data.Items.Inventory inventory = optionalInventory.get();
viewer.showGui( viewer.showGui(
inventory, inventory,
plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getUsername()) plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getUsername())
.orElse(new MineDown(String.format("%s's Inventory", user.getUsername()))), .orElse(new MineDown(String.format("%s's Inventory", user.getUsername()))),
allowEdit, allowEdit,
inventory.getSlotCount(), inventory.getSlotCount(),
(itemsOnClose) -> { (itemsOnClose) -> {
if (allowEdit && !inventory.equals(itemsOnClose)) { if (allowEdit && !inventory.equals(itemsOnClose)) {
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user)); plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
}
} }
}
); );
} }
@@ -79,7 +79,7 @@ public class InventoryCommand extends ItemsCommand {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder); final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) { if (latestData.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return; return;
} }
@@ -89,7 +89,7 @@ public class InventoryCommand extends ItemsCommand {
data.getInventory().ifPresent(inventory -> inventory.setContents(items)); data.getInventory().ifPresent(inventory -> inventory.setContents(items));
data.setSaveCause(DataSnapshot.SaveCause.INVENTORY_COMMAND); data.setSaveCause(DataSnapshot.SaveCause.INVENTORY_COMMAND);
data.setPinned( data.setPinned(
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND) plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND)
); );
}); });

View File

@@ -35,7 +35,7 @@ import java.util.UUID;
public abstract class ItemsCommand extends PluginCommand { public abstract class ItemsCommand extends PluginCommand {
protected ItemsCommand(@NotNull String name, @NotNull List<String> aliases, @NotNull HuskSync plugin) { protected ItemsCommand(@NotNull String name, @NotNull List<String> aliases, @NotNull HuskSync plugin) {
super(name, aliases, Permission.Default.IF_OP, plugin); super(name, aliases, Permission.Default.IF_OP, ExecutionScope.IN_GAME, plugin);
} }
@Override @Override
@@ -46,7 +46,7 @@ public abstract class ItemsCommand extends PluginCommand {
final CommandUser executor = user(command, ctx); final CommandUser executor = user(command, ctx);
if (!(executor instanceof OnlineUser online)) { if (!(executor instanceof OnlineUser online)) {
plugin.getLocales().getLocale("error_in_game_command_only") plugin.getLocales().getLocale("error_in_game_command_only")
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
return; return;
} }
this.showSnapshotItems(online, user, version); this.showSnapshotItems(online, user, version);
@@ -56,7 +56,7 @@ public abstract class ItemsCommand extends PluginCommand {
final CommandUser executor = user(command, ctx); final CommandUser executor = user(command, ctx);
if (!(executor instanceof OnlineUser online)) { if (!(executor instanceof OnlineUser online)) {
plugin.getLocales().getLocale("error_in_game_command_only") plugin.getLocales().getLocale("error_in_game_command_only")
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
return; return;
} }
this.showLatestItems(online, user); this.showLatestItems(online, user);
@@ -66,44 +66,44 @@ public abstract class ItemsCommand extends PluginCommand {
// View (and edit) the latest user data // View (and edit) the latest user data
private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) { private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) {
plugin.getRedisManager().getUserData(user.getUuid(), user).thenAccept(data -> data plugin.getRedisManager().getUserData(user.getUuid(), user).thenAccept(data -> data
.or(() -> plugin.getDatabase().getLatestSnapshot(user)) .or(() -> plugin.getDatabase().getLatestSnapshot(user))
.or(() -> { .or(() -> {
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return Optional.empty();
})
.flatMap(packed -> {
if (packed.isInvalid()) {
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
.ifPresent(viewer::sendMessage);
return Optional.empty(); return Optional.empty();
} })
return Optional.of(packed.unpack(plugin)); .flatMap(packed -> {
}) if (packed.isInvalid()) {
.ifPresent(snapshot -> this.showItems( plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
viewer, snapshot, user, viewer.hasPermission(getPermission("edit")) .ifPresent(viewer::sendMessage);
))); return Optional.empty();
}
return Optional.of(packed.unpack(plugin));
})
.ifPresent(snapshot -> this.showItems(
viewer, snapshot, user, viewer.hasPermission(getPermission("edit"))
)));
} }
// View a specific version of the user data // View a specific version of the user data
private void showSnapshotItems(@NotNull OnlineUser viewer, @NotNull User user, @NotNull UUID version) { private void showSnapshotItems(@NotNull OnlineUser viewer, @NotNull User user, @NotNull UUID version) {
plugin.getDatabase().getSnapshot(user, version) plugin.getDatabase().getSnapshot(user, version)
.or(() -> { .or(() -> {
plugin.getLocales().getLocale("error_invalid_version_uuid") plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return Optional.empty();
})
.flatMap(packed -> {
if (packed.isInvalid()) {
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
.ifPresent(viewer::sendMessage);
return Optional.empty(); return Optional.empty();
} })
return Optional.of(packed.unpack(plugin)); .flatMap(packed -> {
}) if (packed.isInvalid()) {
.ifPresent(snapshot -> this.showItems( plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
viewer, snapshot, user, false .ifPresent(viewer::sendMessage);
)); return Optional.empty();
}
return Optional.of(packed.unpack(plugin));
})
.ifPresent(snapshot -> this.showItems(
viewer, snapshot, user, false
));
} }
// Show a GUI menu with the correct item data from the snapshot // Show a GUI menu with the correct item data from the snapshot

View File

@@ -39,9 +39,9 @@ public abstract class PluginCommand extends Command {
protected final HuskSync plugin; protected final HuskSync plugin;
protected PluginCommand(@NotNull String name, @NotNull List<String> aliases, protected PluginCommand(@NotNull String name, @NotNull List<String> aliases, @NotNull Permission.Default defPerm,
@NotNull Permission.Default permissionDefault, @NotNull HuskSync plugin) { @NotNull ExecutionScope scope, @NotNull HuskSync plugin) {
super(name, aliases, getDescription(plugin, name), new Permission(createPermission(name), permissionDefault)); super(name, aliases, getDescription(plugin, name), new Permission(createPermission(name), defPerm), scope);
this.plugin = plugin; this.plugin = plugin;
} }

View File

@@ -44,7 +44,7 @@ import java.util.logging.Level;
public class UserDataCommand extends PluginCommand { public class UserDataCommand extends PluginCommand {
public UserDataCommand(@NotNull HuskSync plugin) { public UserDataCommand(@NotNull HuskSync plugin) {
super("userdata", List.of("playerdata"), Permission.Default.IF_OP, plugin); super("userdata", List.of("playerdata"), Permission.Default.IF_OP, ExecutionScope.ALL, plugin);
} }
@Override @Override

View File

@@ -193,14 +193,20 @@ public class Locales {
* Displays the notification in the action bar * Displays the notification in the action bar
*/ */
ACTION_BAR, ACTION_BAR,
/** /**
* Displays the notification in the chat * Displays the notification in the chat
*/ */
CHAT, CHAT,
/** /**
* Displays the notification in an Advancement Toast * Displays the notification in an Advancement Toast
*
* @deprecated No longer supported
*/ */
@Deprecated(since = "3.6.7")
TOAST, TOAST,
/** /**
* Does not display the notification * Does not display the notification
*/ */

View File

@@ -64,7 +64,7 @@ public class Settings {
private boolean checkForUpdates = true; private boolean checkForUpdates = true;
@Comment("Specify a common ID for grouping servers running HuskSync. " @Comment("Specify a common ID for grouping servers running HuskSync. "
+ "Don't modify this unless you know what you're doing!") + "Don't modify this unless you know what you're doing!")
private String clusterId = ""; private String clusterId = "";
@Comment("Enable development debug logging") @Comment("Enable development debug logging")
@@ -141,6 +141,9 @@ public class Settings {
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
private Map<String, String> tableNames = Database.TableName.getDefaults(); private Map<String, String> tableNames = Database.TableName.getDefaults();
@Comment("Whether to run the creation SQL on the database when the server starts. Don't modify this unless you know what you're doing!")
private boolean createTables = true;
@NotNull @NotNull
public String getTableName(@NotNull Database.TableName tableName) { public String getTableName(@NotNull Database.TableName tableName) {
return tableNames.getOrDefault(tableName.name().toLowerCase(Locale.ENGLISH), tableName.getDefaultName()); return tableNames.getOrDefault(tableName.name().toLowerCase(Locale.ENGLISH), tableName.getDefaultName());
@@ -229,7 +232,7 @@ public class Settings {
private boolean enabled = false; private boolean enabled = false;
@Comment("What items to save in death snapshots? (DROPS or ITEMS_TO_KEEP). " @Comment("What items to save in death snapshots? (DROPS or ITEMS_TO_KEEP). "
+ "Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server.") + "Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server.")
private DeathItemsMode itemsToSave = DeathItemsMode.DROPS; private DeathItemsMode itemsToSave = DeathItemsMode.DROPS;
@Comment("Should a death snapshot still be created even if the items to save on the player's death are empty?") @Comment("Should a death snapshot still be created even if the items to save on the player's death are empty?")
@@ -250,14 +253,14 @@ public class Settings {
@Comment("Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing") @Comment("Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing")
private boolean compressData = true; private boolean compressData = true;
@Comment("Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)") @Comment("Where to display sync notifications (ACTION_BAR, CHAT or NONE)")
private Locales.NotificationSlot notificationDisplaySlot = Locales.NotificationSlot.ACTION_BAR; private Locales.NotificationSlot notificationDisplaySlot = Locales.NotificationSlot.ACTION_BAR;
@Comment("Persist maps locked in a Cartography Table to let them be viewed on any server") @Comment("Persist maps locked in a Cartography Table to let them be viewed on any server")
private boolean persistLockedMaps = true; private boolean persistLockedMaps = true;
@Comment("If using the DELAY sync method, how long should this server listen for Redis key data updates before " @Comment("If using the DELAY sync method, how long should this server listen for Redis key data updates before "
+ "pulling data from the database instead (i.e., if the user did not change servers).") + "pulling data from the database instead (i.e., if the user did not change servers).")
private int networkLatencyMilliseconds = 500; private int networkLatencyMilliseconds = 500;
@Comment({"Which data types to synchronize.", "Docs: https://william278.net/docs/husksync/sync-features"}) @Comment({"Which data types to synchronize.", "Docs: https://william278.net/docs/husksync/sync-features"})
@@ -267,10 +270,52 @@ public class Settings {
@Comment("Commands which should be blocked before a player has finished syncing (Use * to block all commands)") @Comment("Commands which should be blocked before a player has finished syncing (Use * to block all commands)")
private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*")); private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*"));
@Comment({"For attribute syncing, which attributes should be ignored/skipped when syncing", @Comment("Configuration for how to sync attributes")
"(e.g. ['minecraft:generic.max_health', 'minecraft:generic.attack_damage'])"}) private AttributeSettings attributes = new AttributeSettings();
@Getter(AccessLevel.NONE)
private List<String> ignoredAttributes = new ArrayList<>(List.of("")); @Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class AttributeSettings {
@Comment({"Which attribute types should be saved as part of attribute syncing. Supports wildcard matching.",
"(e.g. ['minecraft:generic.max_health', 'minecraft:generic.*'])"})
@Getter(AccessLevel.NONE)
private List<String> syncedAttributes = new ArrayList<>(List.of(
"minecraft:generic.max_health", "minecraft:max_health",
"minecraft:generic.max_absorption", "minecraft:max_absorption",
"minecraft:generic.luck", "minecraft:luck",
"minecraft:generic.scale", "minecraft:scale",
"minecraft:generic.step_height", "minecraft:step_height",
"minecraft:generic.gravity", "minecraft:gravity"
));
@Comment({"Which attribute modifiers should be saved. Supports wildcard matching.",
"(e.g. ['minecraft:effect.speed', 'minecraft:effect.*'])"})
@Getter(AccessLevel.NONE)
private List<String> ignoredModifiers = new ArrayList<>(List.of(
"minecraft:effect.*", "minecraft:creative_mode_*"
));
private boolean matchesWildcard(@NotNull String pat, @NotNull String value) {
if (!pat.contains(":")) {
pat = "minecraft:%s".formatted(pat);
}
if (!value.contains(":")) {
value = "minecraft:%s".formatted(value);
}
return pat.contains("*") ? value.matches(pat.replace("*", ".*")) : pat.equals(value);
}
public boolean isIgnoredAttribute(@NotNull String attribute) {
return syncedAttributes.stream().noneMatch(wildcard -> matchesWildcard(wildcard, attribute));
}
public boolean isIgnoredModifier(@NotNull String modifier) {
return ignoredModifiers.stream().anyMatch(wildcard -> matchesWildcard(wildcard, modifier));
}
}
@Comment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts") @Comment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts")
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
@@ -284,10 +329,6 @@ public class Settings {
return id.isCustom() || features.getOrDefault(id.getKeyValue(), id.isEnabledByDefault()); return id.isCustom() || features.getOrDefault(id.getKeyValue(), id.isEnabledByDefault());
} }
public boolean isIgnoredAttribute(@NotNull String attribute) {
return ignoredAttributes.contains(attribute);
}
@NotNull @NotNull
public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) { public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) {
try { try {

View File

@@ -22,9 +22,8 @@ package net.william278.husksync.data;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
@@ -132,7 +131,7 @@ public interface Data {
/** /**
* Represents a potion effect * Represents a potion effect
* *
* @param type the type of potion effect * @param type the key of potion effect
* @param amplifier the amplifier of the potion effect * @param amplifier the amplifier of the potion effect
* @param duration the duration of the potion effect * @param duration the duration of the potion effect
* @param isAmbient whether the potion effect is ambient * @param isAmbient whether the potion effect is ambient
@@ -155,14 +154,14 @@ public interface Data {
*/ */
interface Advancements extends Data { interface Advancements extends Data {
String RECIPE_ADVANCEMENT = "minecraft:recipe";
@NotNull @NotNull
List<Advancement> getCompleted(); List<Advancement> getCompleted();
@NotNull @NotNull
default List<Advancement> getCompletedExcludingRecipes() { default List<Advancement> getCompletedExcludingRecipes() {
return getCompleted().stream() return getCompleted().stream().filter(adv -> !adv.getKey().startsWith(RECIPE_ADVANCEMENT)).toList();
.filter(advancement -> !advancement.getKey().startsWith("minecraft:recipe"))
.collect(Collectors.toList());
} }
void setCompleted(@NotNull List<Advancement> completed); void setCompleted(@NotNull List<Advancement> completed);
@@ -191,13 +190,13 @@ public interface Data {
@NotNull @NotNull
private static Map<String, Long> adaptDateMap(@NotNull Map<String, Date> dateMap) { private static Map<String, Long> adaptDateMap(@NotNull Map<String, Date> dateMap) {
return dateMap.entrySet().stream() return dateMap.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getTime())); .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getTime()));
} }
@NotNull @NotNull
private static Map<String, Date> adaptLongMap(@NotNull Map<String, Long> dateMap) { private static Map<String, Date> adaptLongMap(@NotNull Map<String, Long> dateMap) {
return dateMap.entrySet().stream() return dateMap.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> new Date(e.getValue()))); .collect(Collectors.toMap(Map.Entry::getKey, e -> new Date(e.getValue())));
} }
@NotNull @NotNull
@@ -250,9 +249,9 @@ public interface Data {
void setWorld(@NotNull World world); void setWorld(@NotNull World world);
record World( record World(
@SerializedName("name") @NotNull String name, @SerializedName("name") @NotNull String name,
@SerializedName("uuid") @NotNull UUID uuid, @SerializedName("uuid") @NotNull UUID uuid,
@SerializedName("environment") @NotNull String environment @SerializedName("environment") @NotNull String environment
) { ) {
} }
} }
@@ -324,9 +323,9 @@ public interface Data {
List<Attribute> getAttributes(); List<Attribute> getAttributes();
record Attribute( record Attribute(
@NotNull String name, @NotNull String name,
double baseValue, double baseValue,
@NotNull Set<Modifier> modifiers @NotNull Set<Modifier> modifiers
) { ) {
public double getValue() { public double getValue() {
@@ -341,36 +340,60 @@ public interface Data {
@Getter @Getter
@Accessors(fluent = true) @Accessors(fluent = true)
@AllArgsConstructor @RequiredArgsConstructor
@NoArgsConstructor
final class Modifier { final class Modifier {
final static String ANY_EQUIPMENT_SLOT_GROUP = "any";
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
@Nullable @Nullable
@SerializedName("uuid") @SerializedName("uuid")
private UUID uuid; private UUID uuid = null;
// Since 1.21.1: Name, amount, operation, slotGroup
@SerializedName("name") @SerializedName("name")
private String name; private String name;
@SerializedName("amount") @SerializedName("amount")
private double amount; private double amount;
@SerializedName("operation") @SerializedName("operation")
private int operationType; private int operation;
@SerializedName("equipment_slot") @SerializedName("equipment_slot")
@Deprecated(since = "3.7")
private int equipmentSlot; private int equipmentSlot;
public Modifier(@NotNull String name, double amount, int operationType, int equipmentSlot) { @SerializedName("equipment_slot_group")
private String slotGroup = ANY_EQUIPMENT_SLOT_GROUP;
public Modifier(@NotNull String name, double amount, int operation, @NotNull String slotGroup) {
this.name = name; this.name = name;
this.amount = amount; this.amount = amount;
this.operationType = operationType; this.operation = operation;
this.slotGroup = slotGroup;
}
@Deprecated(since = "3.7")
public Modifier(@NotNull UUID uuid, @NotNull String name, double amount, int operation, int equipmentSlot) {
this.name = name;
this.amount = amount;
this.operation = operation;
this.equipmentSlot = equipmentSlot; this.equipmentSlot = equipmentSlot;
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
return obj instanceof Modifier modifier && modifier.uuid().equals(uuid()); if (obj instanceof Modifier other) {
if (uuid != null && other.uuid != null) {
return uuid.equals(other.uuid);
}
return name.equals(other.name);
}
return super.equals(obj);
} }
public double modify(double value) { public double modify(double value) {
return switch (operationType) { return switch (operation) {
case 0 -> value + amount; case 0 -> value + amount;
case 1 -> value * amount; case 1 -> value * amount;
case 2 -> value * (1 + amount); case 2 -> value * (1 + amount);
@@ -378,6 +401,10 @@ public interface Data {
}; };
} }
public boolean hasUuid() {
return uuid != null;
}
@NotNull @NotNull
public UUID uuid() { public UUID uuid() {
return uuid != null ? uuid : UUID.nameUUIDFromBytes(name.getBytes()); return uuid != null ? uuid : UUID.nameUUIDFromBytes(name.getBytes());
@@ -387,8 +414,8 @@ public interface Data {
default Optional<Attribute> getAttribute(@NotNull Key key) { default Optional<Attribute> getAttribute(@NotNull Key key) {
return getAttributes().stream() return getAttributes().stream()
.filter(attribute -> attribute.name().equals(key.asString())) .filter(attribute -> attribute.name().equals(key.asString()))
.findFirst(); .findFirst();
} }
default void removeAttribute(@NotNull Key key) { default void removeAttribute(@NotNull Key key) {
@@ -397,8 +424,8 @@ public interface Data {
default double getMaxHealth() { default double getMaxHealth() {
return getAttribute(MAX_HEALTH_KEY) return getAttribute(MAX_HEALTH_KEY)
.map(Attribute::getValue) .map(Attribute::getValue)
.orElse(20.0); .orElse(20.0);
} }
default void setMaxHealth(double maxHealth) { default void setMaxHealth(double maxHealth) {

View File

@@ -45,13 +45,13 @@ public class DataException extends IllegalStateException {
@AllArgsConstructor @AllArgsConstructor
public enum Reason { public enum Reason {
INVALID_MINECRAFT_VERSION((plugin, snapshot) -> String.format("The Minecraft version of the snapshot (%s) is " + INVALID_MINECRAFT_VERSION((plugin, snapshot) -> String.format("The Minecraft version of the snapshot (%s) is " +
"newer than the server's version (%s). Ensure each server is on the same version of Minecraft.", "newer than the server's version (%s). Ensure each server is on the same version of Minecraft.",
snapshot.getMinecraftVersion(), plugin.getMinecraftVersion())), snapshot.getMinecraftVersion(), plugin.getMinecraftVersion())),
INVALID_FORMAT_VERSION((plugin, snapshot) -> String.format("The format version of the snapshot (%s) is newer " + INVALID_FORMAT_VERSION((plugin, snapshot) -> String.format("The format version of the snapshot (%s) is newer " +
"than the server's version (%s). Ensure each server is running the same version of HuskSync.", "than the server's version (%s). Ensure each server is running the same version of HuskSync.",
snapshot.getFormatVersion(), DataSnapshot.CURRENT_FORMAT_VERSION)), snapshot.getFormatVersion(), DataSnapshot.CURRENT_FORMAT_VERSION)),
INVALID_PLATFORM_TYPE((plugin, snapshot) -> String.format("The platform type of the snapshot (%s) does " + INVALID_PLATFORM_TYPE((plugin, snapshot) -> String.format("The platform type of the snapshot (%s) does " +
"not match the server's platform type (%s). Ensure each server has the same platform type.", "not match the server's platform type (%s). Ensure each server has the same platform type.",
snapshot.getPlatformType(), plugin.getPlatformType())), snapshot.getPlatformType(), plugin.getPlatformType())),
NO_LEGACY_CONVERTER((plugin, snapshot) -> String.format("No legacy converter to convert format version: %s", NO_LEGACY_CONVERTER((plugin, snapshot) -> String.format("No legacy converter to convert format version: %s",
snapshot.getFormatVersion())); snapshot.getFormatVersion()));

View File

@@ -395,7 +395,7 @@ public class DataSnapshot {
.map(entry -> Map.entry(plugin.getIdentifier(entry.getKey()).orElseThrow(), entry.getValue())) .map(entry -> Map.entry(plugin.getIdentifier(entry.getKey()).orElseThrow(), entry.getValue()))
.collect(Collectors.toMap( .collect(Collectors.toMap(
Map.Entry::getKey, Map.Entry::getKey,
entry -> plugin.deserializeData(entry.getKey(), entry.getValue()), entry -> plugin.deserializeData(entry.getKey(), entry.getValue(), getMinecraftVersion()),
(a, b) -> b, () -> Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR) (a, b) -> b, () -> Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR)
)); ));
} }
@@ -535,9 +535,9 @@ public class DataSnapshot {
public Builder timestamp(@NotNull OffsetDateTime timestamp) { public Builder timestamp(@NotNull OffsetDateTime timestamp) {
if (timestamp.isAfter(OffsetDateTime.now())) { if (timestamp.isAfter(OffsetDateTime.now())) {
throw new IllegalArgumentException("Data snapshots cannot have a timestamp set in the future! " throw new IllegalArgumentException("Data snapshots cannot have a timestamp set in the future! "
+ "Make sure your database server time matches the server time.\n" + "Make sure your database server time matches the server time.\n"
+ "Current game server timestamp: " + OffsetDateTime.now() + " / " + "Current game server timestamp: " + OffsetDateTime.now() + " / "
+ "Snapshot timestamp: " + timestamp); + "Snapshot timestamp: " + timestamp);
} }
this.timestamp = timestamp; this.timestamp = timestamp;
return this; return this;

View File

@@ -50,7 +50,8 @@ public class Identifier {
Dependency.optional("game_mode") Dependency.optional("game_mode")
); );
public static final Identifier ATTRIBUTES = huskSync("attributes", true, public static final Identifier ATTRIBUTES = huskSync("attributes", true,
Dependency.required("potion_effects") Dependency.optional("inventory"),
Dependency.optional("potion_effects")
); );
public static final Identifier HEALTH = huskSync("health", true, public static final Identifier HEALTH = huskSync("health", true,
Dependency.optional("attributes") Dependency.optional("attributes")

View File

@@ -19,6 +19,7 @@
package net.william278.husksync.data; package net.william278.husksync.data;
import net.william278.desertwell.util.Version;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -119,19 +120,36 @@ public interface SerializerRegistry {
} }
/** /**
* Deserialize data for the given {@link Identifier} * Deserialize data of a given {@link Version Minecraft version} for the given {@link Identifier data identifier}
*
* @param identifier the {@link Identifier} to deserialize data for
* @param data the data to deserialize
* @param dataMcVersion the Minecraft version of the data
* @return the deserialized data
* @throws IllegalStateException if no serializer is found for the given {@link Identifier}
* @since 3.6.4
*/
@NotNull
default Data deserializeData(@NotNull Identifier identifier, @NotNull String data,
@NotNull Version dataMcVersion) throws IllegalStateException {
return getSerializer(identifier).map(serializer -> serializer.deserialize(data, dataMcVersion)).orElseThrow(
() -> new IllegalStateException("No serializer found for %s".formatted(identifier))
);
}
/**
* Deserialize data for the given {@link Identifier data identifier}
* *
* @param identifier the {@link Identifier} to deserialize data for * @param identifier the {@link Identifier} to deserialize data for
* @param data the data to deserialize * @param data the data to deserialize
* @return the deserialized data * @return the deserialized data
* @throws IllegalStateException if no serializer is found for the given {@link Identifier}
* @since 3.5.4 * @since 3.5.4
* @deprecated Use {@link #deserializeData(Identifier, String, Version)} instead
*/ */
@NotNull @NotNull
default Data deserializeData(@NotNull Identifier identifier, @NotNull String data) throws IllegalStateException { @Deprecated(since = "3.6.5")
return getSerializer(identifier).map(serializer -> serializer.deserialize(data)).orElseThrow( default Data deserializeData(@NotNull Identifier identifier, @NotNull String data) {
() -> new IllegalStateException("No serializer found for %s".formatted(identifier)) return deserializeData(identifier, data, getPlugin().getMinecraftVersion());
);
} }
/** /**

View File

@@ -107,6 +107,14 @@ public abstract class Database {
@Blocking @Blocking
public abstract Optional<User> getUserByName(@NotNull String username); public abstract Optional<User> getUserByName(@NotNull String username);
/**
* Get all users
*
* @return A list of all users
*/
@NotNull
@Blocking
public abstract List<User> getAllUsers();
/** /**
* Get the latest data snapshot for a user. * Get the latest data snapshot for a user.

View File

@@ -50,17 +50,13 @@ public class MongoDbDatabase extends Database {
private final String usersTable; private final String usersTable;
private final String userDataTable; private final String userDataTable;
public MongoDbDatabase(@NotNull HuskSync plugin) { public MongoDbDatabase(@NotNull HuskSync plugin) {
super(plugin); super(plugin);
this.usersTable = plugin.getSettings().getDatabase().getTableName(TableName.USERS); this.usersTable = plugin.getSettings().getDatabase().getTableName(TableName.USERS);
this.userDataTable = plugin.getSettings().getDatabase().getTableName(TableName.USER_DATA); this.userDataTable = plugin.getSettings().getDatabase().getTableName(TableName.USER_DATA);
} }
/**
* Initialize the database and ensure tables are present; create tables if they do not exist.
*
* @throws IllegalStateException if the database could not be initialized
*/
@Override @Override
public void initialize() throws IllegalStateException { public void initialize() throws IllegalStateException {
final Settings.DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials(); final Settings.DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
@@ -68,6 +64,10 @@ public class MongoDbDatabase extends Database {
ConnectionString URI = createConnectionURI(credentials); ConnectionString URI = createConnectionURI(credentials);
mongoConnectionHandler = new MongoConnectionHandler(URI, credentials.getDatabase()); mongoConnectionHandler = new MongoConnectionHandler(URI, credentials.getDatabase());
mongoCollectionHelper = new MongoCollectionHelper(mongoConnectionHandler); mongoCollectionHelper = new MongoCollectionHelper(mongoConnectionHandler);
// Check config for if tables should be created
if (!plugin.getSettings().getDatabase().isCreateTables()) return;
if (mongoCollectionHelper.getCollection(usersTable) == null) { if (mongoCollectionHelper.getCollection(usersTable) == null) {
mongoCollectionHelper.createCollection(usersTable); mongoCollectionHelper.createCollection(usersTable);
} }
@@ -76,7 +76,7 @@ public class MongoDbDatabase extends Database {
} }
} catch (Exception e) { } catch (Exception e) {
throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " + throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " +
"Please check the supplied database credentials in the config file", e); "Please check the supplied database credentials in the config file", e);
} }
} }
@@ -93,11 +93,6 @@ public class MongoDbDatabase extends Database {
return new ConnectionString(baseURI); return new ConnectionString(baseURI);
} }
/**
* Ensure a {@link User} has an entry in the database and that their username is up-to-date
*
* @param user The {@link User} to ensure
*/
@Blocking @Blocking
@Override @Override
public void ensureUser(@NotNull User user) { public void ensureUser(@NotNull User user) {
@@ -135,12 +130,6 @@ public class MongoDbDatabase extends Database {
} }
} }
/**
* Get a player by their Minecraft account {@link UUID}
*
* @param uuid Minecraft account {@link UUID} of the {@link User} to get
* @return An optional with the {@link User} present if they exist
*/
@Blocking @Blocking
@Override @Override
public Optional<User> getUser(@NotNull UUID uuid) { public Optional<User> getUser(@NotNull UUID uuid) {
@@ -157,12 +146,6 @@ public class MongoDbDatabase extends Database {
} }
} }
/**
* Get a user by their username (<i>case-insensitive</i>)
*
* @param username Username of the {@link User} to get (<i>case-insensitive</i>)
* @return An optional with the {@link User} present if they exist
*/
@Blocking @Blocking
@Override @Override
public Optional<User> getUserByName(@NotNull String username) { public Optional<User> getUserByName(@NotNull String username) {
@@ -180,12 +163,24 @@ public class MongoDbDatabase extends Database {
} }
} }
/** @Override
* Get the latest data snapshot for a user. @NotNull
* public List<User> getAllUsers() {
* @param user The user to get data for final List<User> users = Lists.newArrayList();
* @return an optional containing the {@link DataSnapshot}, if it exists, or an empty optional if it does not try {
*/ final FindIterable<Document> doc = mongoCollectionHelper.getCollection(usersTable).find();
for (Document document : doc) {
users.add(new User(
UUID.fromString(document.getString("uuid")),
document.getString("username")
));
}
} catch (MongoException e) {
plugin.log(Level.SEVERE, "Failed to get all users from the database", e);
}
return users;
}
@Blocking @Blocking
@Override @Override
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) { public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
@@ -208,12 +203,6 @@ public class MongoDbDatabase extends Database {
} }
} }
/**
* Get all {@link DataSnapshot} entries for a user from the database.
*
* @param user The user to get data for
* @return The list of a user's {@link DataSnapshot} entries
*/
@Blocking @Blocking
@Override @Override
@NotNull @NotNull
@@ -237,13 +226,6 @@ public class MongoDbDatabase extends Database {
} }
} }
/**
* Gets a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
*
* @param user The user to get data for
* @param versionUuid The UUID of the {@link DataSnapshot} entry to get
* @return An optional containing the {@link DataSnapshot}, if it exists
*/
@Blocking @Blocking
@Override @Override
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) { public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
@@ -265,12 +247,6 @@ public class MongoDbDatabase extends Database {
} }
} }
/**
* <b>(Internal)</b> Prune user data for a given user to the maximum value as configured.
*
* @param user The user to prune data for
* @implNote Data snapshots marked as {@code pinned} are exempt from rotation
*/
@Blocking @Blocking
@Override @Override
protected void rotateSnapshots(@NotNull User user) { protected void rotateSnapshots(@NotNull User user) {
@@ -296,12 +272,6 @@ public class MongoDbDatabase extends Database {
} }
} }
/**
* Deletes a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
*
* @param user The user to get data for
* @param versionUuid The UUID of the {@link DataSnapshot} entry to delete
*/
@Blocking @Blocking
@Override @Override
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) { public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
@@ -319,14 +289,6 @@ public class MongoDbDatabase extends Database {
return false; return false;
} }
/**
* Deletes the most recent data snapshot by the given {@link User user}
* The snapshot must have been created after {@link OffsetDateTime time} and NOT be pinned
* Facilities the backup frequency feature, reducing redundant snapshots from being saved longer than needed
*
* @param user The user to delete a snapshot for
* @param within The time to delete a snapshot after
*/
@Blocking @Blocking
@Override @Override
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) { protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
@@ -351,12 +313,6 @@ public class MongoDbDatabase extends Database {
} }
} }
/**
* <b>Internal</b> - Create user data in the database
*
* @param user The user to add data for
* @param data The {@link DataSnapshot} to set.
*/
@Blocking @Blocking
@Override @Override
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) { protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
@@ -373,12 +329,6 @@ public class MongoDbDatabase extends Database {
} }
} }
/**
* Update a saved {@link DataSnapshot} by given version UUID
*
* @param user The user whose data snapshot
* @param data The {@link DataSnapshot} to update
*/
@Blocking @Blocking
@Override @Override
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) { public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
@@ -395,10 +345,6 @@ public class MongoDbDatabase extends Database {
} }
} }
/**
* Wipes <b>all</b> {@link User} entries from the database.
* <b>This should only be used when preparing tables for a data migration.</b>
*/
@Blocking @Blocking
@Override @Override
public void wipeDatabase() { public void wipeDatabase() {
@@ -409,9 +355,6 @@ public class MongoDbDatabase extends Database {
} }
} }
/**
* Close the database connection
*/
@Override @Override
public void terminate() { public void terminate() {
if (mongoConnectionHandler != null) { if (mongoConnectionHandler != null) {

View File

@@ -115,6 +115,9 @@ public class MySqlDatabase extends Database {
); );
dataSource.setDataSourceProperties(properties); dataSource.setDataSourceProperties(properties);
// Check config for if tables should be created
if (!plugin.getSettings().getDatabase().isCreateTables()) return;
// Prepare database schema; make tables if they don't exist // Prepare database schema; make tables if they don't exist
try (Connection connection = dataSource.getConnection()) { try (Connection connection = dataSource.getConnection()) {
final String[] databaseSchema = getSchemaStatements(String.format("database/%s_schema.sql", flavor)); final String[] databaseSchema = getSchemaStatements(String.format("database/%s_schema.sql", flavor));
@@ -124,11 +127,11 @@ public class MySqlDatabase extends Database {
} }
} catch (SQLException e) { } catch (SQLException e) {
throw new IllegalStateException("Failed to create database tables. Please ensure you are running MySQL v8.0+ " + throw new IllegalStateException("Failed to create database tables. Please ensure you are running MySQL v8.0+ " +
"and that your connecting user account has privileges to create tables.", e); "and that your connecting user account has privileges to create tables.", e);
} }
} catch (SQLException | IOException e) { } catch (SQLException | IOException e) {
throw new IllegalStateException("Failed to establish a connection to the MySQL database. " + throw new IllegalStateException("Failed to establish a connection to the MySQL database. " +
"Please check the supplied database credentials in the config file", e); "Please check the supplied database credentials in the config file", e);
} }
} }
@@ -218,6 +221,27 @@ public class MySqlDatabase extends Database {
return Optional.empty(); return Optional.empty();
} }
@Override
@NotNull
public List<User> getAllUsers() {
final List<User> users = Lists.newArrayList();
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `uuid`, `username`
FROM `%users_table%`;
"""))) {
final ResultSet resultSet = statement.executeQuery();
while (resultSet.next()) {
users.add(new User(UUID.fromString(resultSet.getString("uuid")),
resultSet.getString("username")));
}
}
} catch (SQLException e) {
plugin.log(Level.SEVERE, "Failed to fetch a user by name from the database", e);
}
return users;
}
@Blocking @Blocking
@Override @Override
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) { public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {

View File

@@ -51,12 +51,6 @@ public class PostgresDatabase extends Database {
this.driverClass = "org.postgresql.Driver"; this.driverClass = "org.postgresql.Driver";
} }
/**
* Fetch the auto-closeable connection from the hikariDataSource
*
* @return The {@link Connection} to the MySQL database
* @throws SQLException if the connection fails for some reason
*/
@Blocking @Blocking
@NotNull @NotNull
private Connection getConnection() throws SQLException { private Connection getConnection() throws SQLException {
@@ -114,6 +108,9 @@ public class PostgresDatabase extends Database {
); );
dataSource.setDataSourceProperties(properties); dataSource.setDataSourceProperties(properties);
// Check config for if tables should be created
if (!plugin.getSettings().getDatabase().isCreateTables()) return;
// Prepare database schema; make tables if they don't exist // Prepare database schema; make tables if they don't exist
try (Connection connection = dataSource.getConnection()) { try (Connection connection = dataSource.getConnection()) {
final String[] databaseSchema = getSchemaStatements(String.format("database/%s_schema.sql", flavor)); final String[] databaseSchema = getSchemaStatements(String.format("database/%s_schema.sql", flavor));
@@ -123,11 +120,11 @@ public class PostgresDatabase extends Database {
} }
} catch (SQLException e) { } catch (SQLException e) {
throw new IllegalStateException("Failed to create database tables. Please ensure you are running PostgreSQL " + throw new IllegalStateException("Failed to create database tables. Please ensure you are running PostgreSQL " +
"and that your connecting user account has privileges to create tables.", e); "and that your connecting user account has privileges to create tables.", e);
} }
} catch (SQLException | IOException e) { } catch (SQLException | IOException e) {
throw new IllegalStateException("Failed to establish a connection to the PostgreSQL database. " + throw new IllegalStateException("Failed to establish a connection to the PostgreSQL database. " +
"Please check the supplied database credentials in the config file", e); "Please check the supplied database credentials in the config file", e);
} }
} }
@@ -140,9 +137,9 @@ public class PostgresDatabase extends Database {
// Update a user's name if it has changed in the database // Update a user's name if it has changed in the database
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
UPDATE "%users_table%" UPDATE %users_table%
SET "username"=? SET username=?
WHERE "uuid"=?"""))) { WHERE uuid=?;"""))) {
statement.setString(1, user.getUsername()); statement.setString(1, user.getUsername());
statement.setObject(2, existingUser.getUuid()); statement.setObject(2, existingUser.getUuid());
@@ -158,7 +155,7 @@ public class PostgresDatabase extends Database {
// Insert new player data into the database // Insert new player data into the database
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
INSERT INTO "%users_table%" ("uuid","username") INSERT INTO %users_table% (uuid,username)
VALUES (?,?);"""))) { VALUES (?,?);"""))) {
statement.setObject(1, user.getUuid()); statement.setObject(1, user.getUuid());
@@ -177,9 +174,9 @@ public class PostgresDatabase extends Database {
public Optional<User> getUser(@NotNull UUID uuid) { public Optional<User> getUser(@NotNull UUID uuid) {
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT "uuid", "username" SELECT uuid, username
FROM "%users_table%" FROM %users_table%
WHERE "uuid"=?"""))) { WHERE uuid=?;"""))) {
statement.setObject(1, uuid); statement.setObject(1, uuid);
@@ -200,9 +197,9 @@ public class PostgresDatabase extends Database {
public Optional<User> getUserByName(@NotNull String username) { public Optional<User> getUserByName(@NotNull String username) {
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT "uuid", "username" SELECT uuid, username
FROM "%users_table%" FROM %users_table%
WHERE "username"=?"""))) { WHERE username=?;"""))) {
statement.setString(1, username); statement.setString(1, username);
final ResultSet resultSet = statement.executeQuery(); final ResultSet resultSet = statement.executeQuery();
@@ -217,15 +214,37 @@ public class PostgresDatabase extends Database {
return Optional.empty(); return Optional.empty();
} }
@Override
@NotNull
public List<User> getAllUsers() {
final List<User> users = Lists.newArrayList();
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT uuid, username
FROM %users_table%;
"""))) {
final ResultSet resultSet = statement.executeQuery();
while (resultSet.next()) {
users.add(new User(UUID.fromString(resultSet.getString("uuid")),
resultSet.getString("username")));
}
}
} catch (SQLException e) {
plugin.log(Level.SEVERE, "Failed to fetch a user by name from the database", e);
}
return users;
}
@Blocking @Blocking
@Override @Override
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) { public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT "version_uuid", "timestamp", "data" SELECT version_uuid, timestamp, data
FROM "%user_data_table%" FROM %user_data_table%
WHERE "player_uuid"=? WHERE player_uuid=?
ORDER BY "timestamp" DESC ORDER BY timestamp DESC
LIMIT 1;"""))) { LIMIT 1;"""))) {
statement.setObject(1, user.getUuid()); statement.setObject(1, user.getUuid());
final ResultSet resultSet = statement.executeQuery(); final ResultSet resultSet = statement.executeQuery();
@@ -251,10 +270,10 @@ public class PostgresDatabase extends Database {
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList(); final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT "version_uuid", "timestamp", "data" SELECT version_uuid, timestamp, data
FROM "%user_data_table%" FROM %user_data_table%
WHERE "player_uuid"=? WHERE player_uuid=?
ORDER BY "timestamp" DESC;"""))) { ORDER BY timestamp DESC;"""))) {
statement.setObject(1, user.getUuid()); statement.setObject(1, user.getUuid());
final ResultSet resultSet = statement.executeQuery(); final ResultSet resultSet = statement.executeQuery();
while (resultSet.next()) { while (resultSet.next()) {
@@ -278,10 +297,10 @@ public class PostgresDatabase extends Database {
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) { public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT "version_uuid", "timestamp", "data" SELECT version_uuid, timestamp, data
FROM "%user_data_table%" FROM %user_data_table%
WHERE "player_uuid"=? AND "version_uuid"=? WHERE player_uuid=? AND version_uuid=?
ORDER BY "timestamp" DESC ORDER BY timestamp DESC
LIMIT 1;"""))) { LIMIT 1;"""))) {
statement.setObject(1, user.getUuid()); statement.setObject(1, user.getUuid());
statement.setObject(2, versionUuid); statement.setObject(2, versionUuid);
@@ -309,11 +328,16 @@ public class PostgresDatabase extends Database {
if (unpinnedUserData.size() > maxSnapshots) { if (unpinnedUserData.size() > maxSnapshots) {
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
DELETE FROM "%user_data_table%" WITH cte AS (
WHERE "player_uuid"=? SELECT version_uuid
AND "pinned" = FALSE FROM %user_data_table%
ORDER BY "timestamp" ASC WHERE player_uuid=?
LIMIT %entry_count%;""".replace("%entry_count%", AND pinned=FALSE
ORDER BY timestamp ASC
LIMIT %entry_count%
)
DELETE FROM %user_data_table%
WHERE version_uuid IN (SELECT version_uuid FROM cte);""".replace("%entry_count%",
Integer.toString(unpinnedUserData.size() - maxSnapshots))))) { Integer.toString(unpinnedUserData.size() - maxSnapshots))))) {
statement.setObject(1, user.getUuid()); statement.setObject(1, user.getUuid());
statement.executeUpdate(); statement.executeUpdate();
@@ -329,11 +353,10 @@ public class PostgresDatabase extends Database {
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) { public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
DELETE FROM "%user_data_table%" DELETE FROM %user_data_table%
WHERE "player_uuid"=? AND "version_uuid"=? WHERE player_uuid=? AND version_uuid=?;"""))) {
LIMIT 1;"""))) {
statement.setObject(1, user.getUuid()); statement.setObject(1, user.getUuid());
statement.setString(2, versionUuid.toString()); statement.setObject(2, versionUuid);
return statement.executeUpdate() > 0; return statement.executeUpdate() > 0;
} }
} catch (SQLException e) { } catch (SQLException e) {
@@ -347,12 +370,12 @@ public class PostgresDatabase extends Database {
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) { protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
DELETE FROM "%user_data_table%" DELETE FROM %user_data_table%
WHERE "player_uuid"=? AND "timestamp" = ( WHERE player_uuid=? AND timestamp = (
SELECT "timestamp" SELECT timestamp
FROM "%user_data_table%" FROM %user_data_table%
WHERE "player_uuid"=? AND "timestamp" > ? AND "pinned" = FALSE WHERE player_uuid=? AND timestamp > ? AND pinned=FALSE
ORDER BY "timestamp" ASC ORDER BY timestamp ASC
LIMIT 1 LIMIT 1
);"""))) { );"""))) {
statement.setObject(1, user.getUuid()); statement.setObject(1, user.getUuid());
@@ -370,8 +393,8 @@ public class PostgresDatabase extends Database {
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) { protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
INSERT INTO "%user_data_table%" INSERT INTO %user_data_table%
("player_uuid","version_uuid","timestamp","save_cause","pinned","data") (player_uuid,version_uuid,timestamp,save_cause,pinned,data)
VALUES (?,?,?,?,?,?);"""))) { VALUES (?,?,?,?,?,?);"""))) {
statement.setObject(1, user.getUuid()); statement.setObject(1, user.getUuid());
statement.setObject(2, data.getId()); statement.setObject(2, data.getId());
@@ -391,9 +414,9 @@ public class PostgresDatabase extends Database {
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) { public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
UPDATE "%user_data_table%" UPDATE %user_data_table%
SET "save_cause"=?,"pinned"=?,"data"=? SET save_cause=?,pinned=?,data=?
WHERE "player_uuid"=? AND "version_uuid"=? WHERE player_uuid=? AND version_uuid=?
LIMIT 1;"""))) { LIMIT 1;"""))) {
statement.setString(1, data.getSaveCause().name()); statement.setString(1, data.getSaveCause().name());
statement.setBoolean(2, data.isPinned()); statement.setBoolean(2, data.isPinned());
@@ -411,7 +434,7 @@ public class PostgresDatabase extends Database {
public void wipeDatabase() { public void wipeDatabase() {
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (Statement statement = connection.createStatement()) { try (Statement statement = connection.createStatement()) {
statement.executeUpdate(formatStatementTables("DELETE FROM \"%user_data_table%\";")); statement.executeUpdate(formatStatementTables("DELETE FROM %user_data_table%;"));
} }
} catch (SQLException e) { } catch (SQLException e) {
plugin.log(Level.SEVERE, "Failed to wipe the database", e); plugin.log(Level.SEVERE, "Failed to wipe the database", e);

View File

@@ -29,6 +29,7 @@ public class MongoCollectionHelper {
/** /**
* Initialize the collection helper * Initialize the collection helper
*
* @param database Instance of {@link MongoConnectionHandler} * @param database Instance of {@link MongoConnectionHandler}
*/ */
public MongoCollectionHelper(@NotNull MongoConnectionHandler database) { public MongoCollectionHelper(@NotNull MongoConnectionHandler database) {
@@ -37,6 +38,7 @@ public class MongoCollectionHelper {
/** /**
* Create a collection * Create a collection
*
* @param collectionName the collection name * @param collectionName the collection name
*/ */
public void createCollection(@NotNull String collectionName) { public void createCollection(@NotNull String collectionName) {
@@ -45,6 +47,7 @@ public class MongoCollectionHelper {
/** /**
* Delete a collection * Delete a collection
*
* @param collectionName the collection name * @param collectionName the collection name
*/ */
public void deleteCollection(@NotNull String collectionName) { public void deleteCollection(@NotNull String collectionName) {
@@ -53,6 +56,7 @@ public class MongoCollectionHelper {
/** /**
* Get a collection * Get a collection
*
* @param collectionName the collection name * @param collectionName the collection name
* @return MongoCollection<Document> * @return MongoCollection<Document>
*/ */
@@ -62,8 +66,9 @@ public class MongoCollectionHelper {
/** /**
* Add a document to a collection * Add a document to a collection
*
* @param collectionName collection to add to * @param collectionName collection to add to
* @param document Document to add * @param document Document to add
*/ */
public void insertDocument(@NotNull String collectionName, @NotNull Document document) { public void insertDocument(@NotNull String collectionName, @NotNull Document document) {
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName); MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
@@ -72,9 +77,10 @@ public class MongoCollectionHelper {
/** /**
* Update a document * Update a document
*
* @param collectionName collection the document is in * @param collectionName collection the document is in
* @param document filter of document * @param document filter of document
* @param updates Bson of updates * @param updates Bson of updates
*/ */
public void updateDocument(@NotNull String collectionName, @NotNull Document document, @NotNull Bson updates) { public void updateDocument(@NotNull String collectionName, @NotNull Document document, @NotNull Bson updates) {
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName); MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
@@ -83,8 +89,9 @@ public class MongoCollectionHelper {
/** /**
* Delete a document * Delete a document
*
* @param collectionName collection the document is in * @param collectionName collection the document is in
* @param document filter to remove * @param document filter to remove
*/ */
public void deleteDocument(@NotNull String collectionName, @NotNull Document document) { public void deleteDocument(@NotNull String collectionName, @NotNull Document document) {
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName); MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);

View File

@@ -35,9 +35,10 @@ public class MongoConnectionHandler {
/** /**
* Initiate a connection to a Mongo Server * Initiate a connection to a Mongo Server
*
* @param uri The connection string * @param uri The connection string
*/ */
public MongoConnectionHandler(@NotNull ConnectionString uri, @NotNull String databaseName) { public MongoConnectionHandler(@NotNull ConnectionString uri, @NotNull String databaseName) {
try { try {
final MongoClientSettings settings = MongoClientSettings.builder() final MongoClientSettings settings = MongoClientSettings.builder()
.applyConnectionString(uri) .applyConnectionString(uri)
@@ -48,7 +49,7 @@ public class MongoConnectionHandler {
this.database = mongoClient.getDatabase(databaseName); this.database = mongoClient.getDatabase(databaseName);
} catch (Exception e) { } catch (Exception e) {
throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " + throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " +
"Please check the supplied database credentials in the config file", e); "Please check the supplied database credentials in the config file", e);
} }
} }

View File

@@ -53,7 +53,7 @@ public abstract class EventListener {
return; return;
} }
plugin.lockPlayer(user.getUuid()); plugin.lockPlayer(user.getUuid());
plugin.getDataSyncer().setUserData(user); plugin.getDataSyncer().syncApplyUserData(user);
} }
/** /**
@@ -66,7 +66,7 @@ public abstract class EventListener {
return; return;
} }
plugin.lockPlayer(user.getUuid()); plugin.lockPlayer(user.getUuid());
plugin.getDataSyncer().saveUserData(user); plugin.getDataSyncer().syncSaveUserData(user);
} }
/** /**
@@ -94,7 +94,7 @@ public abstract class EventListener {
protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull Data.Items items) { protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull Data.Items items) {
final SaveOnDeathSettings settings = plugin.getSettings().getSynchronization().getSaveOnDeath(); final SaveOnDeathSettings settings = plugin.getSettings().getSynchronization().getSaveOnDeath();
if (plugin.isDisabling() || !settings.isEnabled() || plugin.isLocked(user.getUuid()) if (plugin.isDisabling() || !settings.isEnabled() || plugin.isLocked(user.getUuid())
|| user.isNpc() || (!settings.isSaveEmptyItems() && items.isEmpty())) { || user.isNpc() || (!settings.isSaveEmptyItems() && items.isEmpty())) {
return; return;
} }

View File

@@ -92,7 +92,7 @@ public class RedisManager extends JedisPubSub {
jedisPool.getResource().ping(); jedisPool.getResource().ping();
} catch (JedisException e) { } catch (JedisException e) {
throw new IllegalStateException("Failed to establish connection with Redis. " throw new IllegalStateException("Failed to establish connection with Redis. "
+ "Please check the supplied credentials in the config file", e); + "Please check the supplied credentials in the config file", e);
} }
// Subscribe using a thread (rather than a task) // Subscribe using a thread (rather than a task)
@@ -159,6 +159,7 @@ public class RedisManager extends JedisPubSub {
switch (messageType) { switch (messageType) {
case UPDATE_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent( case UPDATE_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
user -> { user -> {
plugin.lockPlayer(user.getUuid());
try { try {
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload()); final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload());
user.applySnapshot(data, DataSnapshot.UpdateCause.UPDATED); user.applySnapshot(data, DataSnapshot.UpdateCause.UPDATED);
@@ -281,16 +282,21 @@ public class RedisManager extends JedisPubSub {
@Blocking @Blocking
public void setUserCheckedOut(@NotNull User user, boolean checkedOut) { public void setUserCheckedOut(@NotNull User user, boolean checkedOut) {
try (Jedis jedis = jedisPool.getResource()) { try (Jedis jedis = jedisPool.getResource()) {
final String key = getKeyString(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId);
if (checkedOut) { if (checkedOut) {
jedis.set( jedis.set(
getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId), key.getBytes(StandardCharsets.UTF_8),
plugin.getServerName().getBytes(StandardCharsets.UTF_8) plugin.getServerName().getBytes(StandardCharsets.UTF_8)
); );
} else { } else {
jedis.del(getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId)); if (jedis.del(key.getBytes(StandardCharsets.UTF_8)) == 0) {
plugin.debug(String.format("[%s] %s key not set on Redis when attempting removal (%s)",
user.getUsername(), RedisKeyType.DATA_CHECKOUT, key));
return;
}
} }
plugin.debug(String.format("[%s] %s %s key to/from Redis", user.getUsername(), plugin.debug(String.format("[%s] %s %s key %s Redis (%s)", user.getUsername(),
checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT)); checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT, checkedOut ? "to" : "from", key));
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred setting checkout to", e); plugin.log(Level.SEVERE, "An exception occurred setting checkout to", e);
} }
@@ -418,7 +424,12 @@ public class RedisManager extends JedisPubSub {
} }
private static byte[] getKey(@NotNull RedisKeyType keyType, @NotNull UUID uuid, @NotNull String clusterId) { private static byte[] getKey(@NotNull RedisKeyType keyType, @NotNull UUID uuid, @NotNull String clusterId) {
return String.format("%s:%s", keyType.getKeyPrefix(clusterId), uuid).getBytes(StandardCharsets.UTF_8); return getKeyString(keyType, uuid, clusterId).getBytes(StandardCharsets.UTF_8);
}
@NotNull
private static String getKeyString(@NotNull RedisKeyType keyType, @NotNull UUID uuid, @NotNull String clusterId) {
return String.format("%s:%s", keyType.getKeyPrefix(clusterId), uuid);
} }
} }

View File

@@ -21,6 +21,8 @@ package net.william278.husksync.redis;
import com.google.gson.JsonSyntaxException; import com.google.gson.JsonSyntaxException;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import lombok.Getter;
import lombok.Setter;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.Adaptable; import net.william278.husksync.adapter.Adaptable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -34,6 +36,8 @@ public class RedisMessage implements Adaptable {
@SerializedName("target_uuid") @SerializedName("target_uuid")
private UUID targetUuid; private UUID targetUuid;
@Getter
@Setter
@SerializedName("payload") @SerializedName("payload")
private byte[] payload; private byte[] payload;
@@ -72,14 +76,6 @@ public class RedisMessage implements Adaptable {
this.targetUuid = targetUuid; this.targetUuid = targetUuid;
} }
public byte[] getPayload() {
return payload;
}
public void setPayload(byte[] payload) {
this.payload = payload;
}
public enum Type { public enum Type {
UPDATE_USER_DATA, UPDATE_USER_DATA,

View File

@@ -81,18 +81,18 @@ public abstract class DataSyncer {
} }
/** /**
* Called when a user's data should be fetched and applied to them * Called when a user's data should be fetched and applied to them as part of a synchronization process
* *
* @param user the user to fetch data for * @param user the user to fetch data for
*/ */
public abstract void setUserData(@NotNull OnlineUser user); public abstract void syncApplyUserData(@NotNull OnlineUser user);
/** /**
* Called when a user's data should be serialized and saved * Called when a user's data should be serialized and saved as part of a synchronization process
* *
* @param user the user to save * @param user the user to save
*/ */
public abstract void saveUserData(@NotNull OnlineUser user); public abstract void syncSaveUserData(@NotNull OnlineUser user);
/** /**
* Save a {@link DataSnapshot.Packed user's data snapshot} to the database, * Save a {@link DataSnapshot.Packed user's data snapshot} to the database,
@@ -150,7 +150,7 @@ public abstract class DataSyncer {
private long getMaxListenAttempts() { private long getMaxListenAttempts() {
return BASE_LISTEN_ATTEMPTS + ( return BASE_LISTEN_ATTEMPTS + (
(Math.max(100, plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds()) / 1000) (Math.max(100, plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds()) / 1000)
* 20 / LISTEN_DELAY * 20 / LISTEN_DELAY
); );
} }

View File

@@ -35,7 +35,7 @@ public class DelayDataSyncer extends DataSyncer {
} }
@Override @Override
public void setUserData(@NotNull OnlineUser user) { public void syncApplyUserData(@NotNull OnlineUser user) {
plugin.runAsyncDelayed( plugin.runAsyncDelayed(
() -> { () -> {
// Fetch from the database if the user isn't changing servers // Fetch from the database if the user isn't changing servers
@@ -58,7 +58,7 @@ public class DelayDataSyncer extends DataSyncer {
} }
@Override @Override
public void saveUserData(@NotNull OnlineUser onlineUser) { public void syncSaveUserData(@NotNull OnlineUser onlineUser) {
plugin.runAsync(() -> { plugin.runAsync(() -> {
getRedis().setUserServerSwitch(onlineUser); getRedis().setUserServerSwitch(onlineUser);
saveData( saveData(

View File

@@ -43,7 +43,7 @@ public class LockstepDataSyncer extends DataSyncer {
// Consume their data when they are checked in // Consume their data when they are checked in
@Override @Override
public void setUserData(@NotNull OnlineUser user) { public void syncApplyUserData(@NotNull OnlineUser user) {
this.listenForRedisData(user, () -> { this.listenForRedisData(user, () -> {
if (getRedis().getUserCheckedOut(user).isPresent()) { if (getRedis().getUserCheckedOut(user).isPresent()) {
return false; return false;
@@ -58,7 +58,7 @@ public class LockstepDataSyncer extends DataSyncer {
} }
@Override @Override
public void saveUserData(@NotNull OnlineUser onlineUser) { public void syncSaveUserData(@NotNull OnlineUser onlineUser) {
plugin.runAsync(() -> saveData( plugin.runAsync(() -> saveData(
onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT), onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
(user, data) -> { (user, data) -> {

View File

@@ -24,7 +24,7 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public interface CommandUser { public interface CommandUser {
@NotNull @NotNull
Audience getAudience(); Audience getAudience();

View File

@@ -89,7 +89,9 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
* @param description the description of the toast * @param description the description of the toast
* @param iconMaterial the namespace-keyed material to use as an hasIcon of the toast * @param iconMaterial the namespace-keyed material to use as an hasIcon of the toast
* @param backgroundType the background ("ToastType") of the toast * @param backgroundType the background ("ToastType") of the toast
* @deprecated No longer supported
*/ */
@Deprecated(since = "3.6.7")
public abstract void sendToast(@NotNull MineDown title, @NotNull MineDown description, public abstract void sendToast(@NotNull MineDown title, @NotNull MineDown description,
@NotNull String iconMaterial, @NotNull String backgroundType); @NotNull String iconMaterial, @NotNull String backgroundType);
@@ -145,12 +147,6 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
switch (plugin.getSettings().getSynchronization().getNotificationDisplaySlot()) { switch (plugin.getSettings().getSynchronization().getNotificationDisplaySlot()) {
case CHAT -> cause.getCompletedLocale(plugin).ifPresent(this::sendMessage); case CHAT -> cause.getCompletedLocale(plugin).ifPresent(this::sendMessage);
case ACTION_BAR -> cause.getCompletedLocale(plugin).ifPresent(this::sendActionBar); case ACTION_BAR -> cause.getCompletedLocale(plugin).ifPresent(this::sendActionBar);
case TOAST -> cause.getCompletedLocale(plugin)
.ifPresent(locale -> this.sendToast(
locale, new MineDown(""),
"minecraft:bell",
"TASK"
));
} }
plugin.fireEvent( plugin.fireEvent(
plugin.getSyncCompleteEvent(this), plugin.getSyncCompleteEvent(this),

View File

@@ -0,0 +1,76 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.util;
import de.exlll.configlib.Configuration;
import de.exlll.configlib.YamlConfigurationProperties;
import de.exlll.configlib.YamlConfigurationStore;
import net.william278.desertwell.util.Version;
import net.william278.husksync.HuskSync;
import org.jetbrains.annotations.NotNull;
import java.io.InputStream;
import java.util.Objects;
import java.util.logging.Level;
import static net.william278.husksync.config.ConfigProvider.YAML_CONFIGURATION_PROPERTIES;
public interface CompatibilityChecker {
String COMPATIBILITY_FILE = "compatibility.yml";
default void checkCompatibility() throws HuskSync.FailedToLoadException {
final YamlConfigurationProperties p = YAML_CONFIGURATION_PROPERTIES.build();
final Version compatible;
// Load compatibility file
try (InputStream input = getResource(COMPATIBILITY_FILE)) {
final CompatibilityConfig compat = new YamlConfigurationStore<>(CompatibilityConfig.class, p).read(input);
compatible = Objects.requireNonNull(compat.getCompatibleWith());
} catch (Throwable e) {
getPlugin().log(Level.WARNING, "Failed to load compatibility config, skipping check.", e);
return;
}
// Check compatibility
if (compatible.compareTo(getPlugin().getMinecraftVersion()) != 0) {
throw new HuskSync.FailedToLoadException("""
Incompatible Minecraft version. This version of HuskSync is designed for Minecraft %s.
Please download the correct version of HuskSync for your server's Minecraft version (%s)."""
.formatted(compatible.toString(), getPlugin().getMinecraftVersion().toString()));
}
}
InputStream getResource(@NotNull String name);
@NotNull
HuskSync getPlugin();
@Configuration
record CompatibilityConfig(@NotNull String minecraftVersion) {
@NotNull
public Version getCompatibleWith() {
return Version.fromString(minecraftVersion);
}
}
}

View File

@@ -28,6 +28,7 @@ import org.jetbrains.annotations.NotNull;
import java.io.*; import java.io.*;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL; import java.net.URL;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -82,7 +83,7 @@ public class DataDumper {
@NotNull @NotNull
public String toWeb() { public String toWeb() {
try { try {
final URL url = new URL(LOGS_SITE_ENDPOINT); final URL url = URI.create(LOGS_SITE_ENDPOINT).toURL();
final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST"); connection.setRequestMethod("POST");
connection.setDoOutput(true); connection.setDoOutput(true);
@@ -178,11 +179,11 @@ public class DataDumper {
@NotNull @NotNull
private String getFileName() { private String getFileName() {
return new StringJoiner("_") return new StringJoiner("_")
.add(user.getUsername()) .add(user.getUsername())
.add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"))) .add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")))
.add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH)) .add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH))
.add(snapshot.getShortId()) .add(snapshot.getShortId())
+ ".json"; + ".json";
} }
} }

View File

@@ -106,7 +106,7 @@ public class DataSnapshotOverview {
.ifPresent(user::sendMessage); .ifPresent(user::sendMessage);
if (user.hasPermission("husksync.command.inventory.edit") if (user.hasPermission("husksync.command.inventory.edit")
&& user.hasPermission("husksync.command.enderchest.edit")) { && user.hasPermission("husksync.command.enderchest.edit")) {
locales.getLocale("data_manager_item_buttons", dataOwner.getUsername(), snapshot.getId().toString()) locales.getLocale("data_manager_item_buttons", dataOwner.getUsername(), snapshot.getId().toString())
.ifPresent(user::sendMessage); .ifPresent(user::sendMessage);
} }

View File

@@ -0,0 +1,2 @@
# File used for checking Minecraft server compatibility with this version of HuskSync
minecraft_version: '${minecraft_version}'

View File

@@ -53,12 +53,12 @@ Add the repository to your `pom.xml` as per below. You can alternatively specify
</repository> </repository>
</repositories> </repositories>
``` ```
Add the dependency to your `pom.xml` as per below. Replace `VERSION` with the latest version of HuskSync (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/HuskSync?color=%23282828&label=%20&style=flat-square). Note for Fabric you must append the target Minecraft version to the version number (e.g. `3.6.1+1.20.1`). Add the dependency to your `pom.xml` as per below. Replace `HUSKSYNC_VERSION` with the latest version of HuskSync (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/HuskSync?color=%23282828&label=%20&style=flat-square). and `MINECRAFT_VERSION` with the version of Minecraft you want to target (e.g. `1.20.1`). A correctly formed version target should look like: `3.7+1.20.1`. Omit the plus symbol and Minecraft version if you are targeting the `common` platform.
```xml ```xml
<dependency> <dependency>
<groupId>net.william278.husksync</groupId> <groupId>net.william278.husksync</groupId>
<artifactId>husksync-PLATFORM</artifactId> <artifactId>husksync-PLATFORM</artifactId>
<version>VERSION</version> <version>HUSKSYNC_VERSION+MINECRAFT_VERSION</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
``` ```
@@ -76,11 +76,11 @@ allprojects {
} }
} }
``` ```
Add the dependency as per below. Replace `VERSION` with the latest version of HuskSync (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/HuskSync?color=%23282828&label=%20&style=flat-square). Note for Fabric you must append the target Minecraft version to the version number (e.g. `3.6.1+1.20.1`). Add the dependency as per below. Replace `HUSKSYNC_VERSION` with the latest version of HuskSync (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/HuskSync?color=%23282828&label=%20&style=flat-square). and `MINECRAFT_VERSION` with the version of Minecraft you want to target (e.g. `1.20.1`). A correctly formed version target should look like: `3.7+1.20.1`. Omit the plus symbol and Minecraft version if you are targeting the `common` platform.
```groovy ```groovy
dependencies { dependencies {
compileOnly 'net.william278.husksync:husksync-PLATFORM:VERSION' compileOnly 'net.william278.husksync:husksync-PLATFORM:HUSKSYNC_VERSION+MINECRAFT_VERSION'
} }
``` ```
</details> </details>

27
docs/Compatibility.md Normal file
View File

@@ -0,0 +1,27 @@
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 Ends |
|:---------------:|:---------------:|:------------:|:--------------|:--------------------------|
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **Active Release** |
| 1.20.6 | 3.6.8 | 17 | Paper | ❌ _October 2024_ |
| 1.20.4 | 3.6.8 | 17 | Paper | ❌ _July 2024_ |
| 1.20.1 | _latest_ | 17 | Paper, Fabric | ✅ **November 2025** (LTS) |
| 1.17.1 - 1.19.4 | 3.6.8 | 17 | Paper | ❌ _Support ended_ |
| 1.16.5 | 3.2.1 | 16 | Paper | ❌ _Support ended_ |
HuskSync is primarily developed against the latest release. Old Minecraft versions are allocated a support channel based on popularity, mod support, etc:
* Long Term Support (LTS) &ndash; Supported for up to 12-18 months
* Non-Long Term Support (Non-LTS) &ndash; Supported for 3-6 months
## Incompatible
This plugin does not support the following software-Minecraft version combinations. The plugin will fail to load if you attempt to run it with these versions. Apologies for the inconvenience.
| Minecraft | Server Software | Notes |
|-------------------|-------------------------------------------|----------------------------------------|
| 1.19.4 | Only: `Purpur, Pufferfish`&dagger; | Older Paper builds also not supported. |
| 1.19.3 | Only: `Paper, Purpur, Pufferfish`&dagger; | Upgrade to 1.19.4 or use Spigot |
| 1.16.5 | _All_ | Please use v3.3.1 or lower |
| below 1.16.5 | _All_ | Upgrade to Minecraft 1.16.5 |
&dagger;Further downstream forks of this server software are also affected.

View File

@@ -109,7 +109,7 @@ synchronization:
sync_dead_players_changing_server: true sync_dead_players_changing_server: true
# Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing # Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing
compress_data: true compress_data: true
# Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE) # Where to display sync notifications (ACTION_BAR, CHAT or NONE)
notification_display_slot: ACTION_BAR notification_display_slot: ACTION_BAR
# Persist maps locked in a Cartography Table to let them be viewed on any server # Persist maps locked in a Cartography Table to let them be viewed on any server
persist_locked_maps: true persist_locked_maps: true
@@ -134,9 +134,26 @@ synchronization:
# Commands which should be blocked before a player has finished syncing (Use * to block all commands) # Commands which should be blocked before a player has finished syncing (Use * to block all commands)
blacklisted_commands_while_locked: blacklisted_commands_while_locked:
- '*' - '*'
# For attribute syncing, which attributes should be ignored/skipped when syncing # Configuration for how to sync attributes
# (e.g. ['minecraft:generic.max_health', 'minecraft:generic.attack_damage']) attributes:
ignored_attributes: [] # Which attribute types should be saved as part of attribute syncing. Supports wildcard matching.
# (e.g. ['minecraft:generic.max_health', 'minecraft:generic.*'])
synced_attributes:
- "minecraft:generic.max_health"
- "minecraft:max_health"
- "minecraft:generic.max_absorption"
- "minecraft:max_absorption"
- "minecraft:generic.luck"
- "minecraft:luck"
- "minecraft:generic.scale"
- "minecraft:scale"
- "minecraft:generic.step_height"
- "minecraft:step_height"
- "minecraft:generic.gravity"
- "minecraft:gravity"
# Which attribute modifiers should not be saved when syncing users. Supports wildcard matching.
# (e.g. ['minecraft:effect.speed', 'minecraft:effect.*'])
ignored_modifiers: ['minecraft:effect.*', 'minecraft:creative_mode_*']
# Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts # Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts
event_priorities: event_priorities:
quit_listener: LOWEST quit_listener: LOWEST

View File

@@ -18,12 +18,12 @@ This guide will walk you through how to upgrade from HuskSync v1.4.x to HuskSync
### 3. Configure the migrator ### 3. Configure the migrator
- With your servers back on and correctly configured to run HuskSync v3.x, ensure nobody is online. - With your servers back on and correctly configured to run HuskSync v3.x, ensure nobody is online.
- Use the console on one of your Spigot servers to enter: `husksync migrate legacy` - Use the console on one of your Spigot servers to enter: `husksync migrate help legacy`
- Carefully read the migration configuration instructions. In most cases, you won't have to change the settings, but if you do need to adjust them, use `husksync migrate legacy set <setting> <value>`. - Carefully read the migration configuration instructions. In most cases, you won't have to change the settings, but if you do need to adjust them, use `husksync migrate set legacy <setting> <value>`.
- Migration will be carried out *from* the database you specify with the settings in console *to* the database configured in `config.yml`. If you're migrating from multiple clusters, ensure you run the migrator on the correct servers corresponding to the migrator. - Migration will be carried out *from* the database you specify with the settings in console *to* the database configured in `config.yml`. If you're migrating from multiple clusters, ensure you run the migrator on the correct servers corresponding to the migrator.
### 4. Start the migrator ### 4. Start the migrator
- Run `husksync migrate legacy start` to begin the migration process. This may take some time, depending on the amount of data you're migrating. - Run `husksync migrate start legacy` to begin the migration process. This may take some time, depending on the amount of data you're migrating.
### 5. Ensure the migration was successful ### 5. Ensure the migration was successful
- HuskSync will notify in console when migration is complete. Verify that the migration went OK by logging in and using the `/userdata list <username>` command to see if the data was imported with the `legacy migration` saveCause. - HuskSync will notify in console when migration is complete. Verify that the migration went OK by logging in and using the `/userdata list <username>` command to see if the data was imported with the `legacy migration` saveCause.

View File

@@ -13,12 +13,12 @@ This guide will walk you through how to migrate from MySQLPlayerDataBridge (MPDB
### 2. Configure the migrator ### 2. Configure the migrator
- With your servers back on and correctly configured to run HuskSync v3.x, ensure nobody is online. - With your servers back on and correctly configured to run HuskSync v3.x, ensure nobody is online.
- Use the console on one of your Spigot servers to enter: `husksync migrate mpdb`. If the MPDB migrator is not available, ensure MySQLPlayerDataBridge is still installed. - Use the console on one of your Spigot servers to enter: `husksync migrate help mpdb`. If the MPDB migrator is not available, ensure MySQLPlayerDataBridge is still installed.
- Adjust the migration setting as needed using the following command: `husksync migrate mpdb set <setting> <value>`. - Adjust the migration setting as needed using the following command: `husksync migrate set mpdb <setting> <value>`.
- Note that migration will be carried out *from* the database you specify with the settings in console *to* the database configured in `config.yml`. - Note that migration will be carried out *from* the database you specify with the settings in console *to* the database configured in `config.yml`.
### 3. Start the migrator ### 3. Start the migrator
- Run `husksync migrate mpdb start` to begin the migration process. This may take some time, depending on the amount of data you're migrating. - Run `husksync migrate start mpdb` to begin the migration process. This may take some time, depending on the amount of data you're migrating.
### 4. Uninstall MySQLPlayerDataBridge ### 4. Uninstall MySQLPlayerDataBridge
- HuskSync will display a message in console when data migration is complete. - HuskSync will display a message in console when data migration is complete.

View File

@@ -1,17 +1,18 @@
> **Warning:** Fabric support is currently in beta and is not production ready yet. Customers can get in touch on Discord to request the Fabric build, or you can self-compile. > **Warning:** Fabric support is currently in beta and is not production ready yet.
This will walk you through installing HuskSync on your network of Spigot or Fabric servers. Please check your server's [[Compatibility]] and download the correct version of HuskSync for your server.
This will walk you through installing HuskSync on your network of Spigot or Fabric servers.
## Requirements ## Requirements
> **Warning:** Mixing and matching Fabric/Spigot servers is not supported, and all servers must be running the same Minecraft version. > **Warning:** Mixing and matching Fabric/Spigot servers is not supported, and all servers must be running the same Minecraft version.
> **Note:** Please also note some specific legacy Paper/Purpur versions are [not compatible](Unsupported-Versions) with HuskSync. > **Note:** Please also note some specific legacy Paper/Purpur versions are [not compatible](Compatibility) with HuskSync.
* A MySQL Database (v8.0+) * A MySQL Database (v8.0+)
* **OR** a MariaDB, PostrgreSQL or MongoDB database, which are also supported * **OR** a MariaDB, PostrgreSQL or MongoDB database, which are also supported
* A Redis Database (v5.0+) &mdash; see [[FAQs]] for more details. * A Redis Database (v5.0+) &mdash; see [[FAQs]] for more details.
* Any number of Spigot servers, connected by a BungeeCord or Velocity-based proxy (Minecraft v1.17.1+, running Java 17+) * Any number of Spigot servers, connected by a BungeeCord or Velocity-based proxy (see [[Compatibility]])
* **OR** a network of Fabric servers, connected by a Fabric proxy (Minecraft v1.20.1, running Java 17+) * **OR** a network of Fabric servers, connected by a Velocity-based proxy
## Setup Instructions ## Setup Instructions
### 1. Install the jar ### 1. Install the jar

View File

@@ -1,11 +0,0 @@
This plugin does not support the following software-Minecraft version combinations. The plugin will fail to load if you attempt to run it with these versions. Apologies for the inconvenience.
## Incompatibility table
| Minecraft Versions | Server Software | Notes |
|--------------------|-------------------------------------------|----------------------------------------|
| 1.19.4 | Only: `Purpur, Pufferfish`&dagger; | Older Paper builds also not supported. |
| 1.19.3 | Only: `Paper, Purpur, Pufferfish`&dagger; | Upgrade to 1.19.4 or use Spigot |
| 1.16.5 | _All_ | Please use v3.3.1 or lower |
| below 1.16.5 | _All_ | Upgrade Minecraft 1.16.5 |
&dagger;Further downstream forks of this server software are also affected.

View File

@@ -11,27 +11,28 @@ repositories {
} }
dependencies { dependencies {
minecraft "com.mojang:minecraft:${fabric_minecraft_version}" minecraft "com.mojang:minecraft:${minecraft_version}"
mappings "net.fabricmc:yarn:${fabric_yarn_mappings}:v2" mappings "net.fabricmc:yarn:${fabric_yarn_mappings}:v2"
modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}" modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}"
modImplementation include("net.kyori:adventure-platform-fabric:${adventure_platform_fabric_version}") modImplementation include("net.kyori:adventure-platform-fabric:${fabric_adventure_platform_version}")
modImplementation include("me.lucko:fabric-permissions-api:${fabric_permissions_api_version}") modImplementation include("me.lucko:fabric-permissions-api:${fabric_permissions_api_version}")
modImplementation include("eu.pb4:sgui:${sgui_version}") modImplementation include("eu.pb4:sgui:${fabric_sgui_version}")
modImplementation include('net.william278.uniform:uniform-fabric:1.1.4+1.20.1') modImplementation include('net.william278.uniform:uniform-fabric:1.2.1+1.20.1')
modCompileOnly "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}" modCompileOnly "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}"
implementation include('org.apache.commons:commons-pool2:2.12.0') implementation include('org.apache.commons:commons-pool2:2.12.0')
implementation include("redis.clients:jedis:$jedis_version") implementation include("redis.clients:jedis:$jedis_version")
implementation include("com.mysql:mysql-connector-j:$mysql_driver_version") implementation include("com.mysql:mysql-connector-j:$mysql_driver_version")
implementation include("org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version") implementation include("org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version")
implementation include("org.postgresql:postgresql:$postgres_driver_version")
implementation include("org.xerial.snappy:snappy-java:$snappy_version") implementation include("org.xerial.snappy:snappy-java:$snappy_version")
compileOnly 'org.jetbrains:annotations:24.1.0' compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'net.william278:DesertWell:2.0.4' compileOnly 'net.william278:DesertWell:2.0.4'
compileOnly 'org.projectlombok:lombok:1.18.32' compileOnly 'org.projectlombok:lombok:1.18.34'
annotationProcessor 'org.projectlombok:lombok:1.18.32' annotationProcessor 'org.projectlombok:lombok:1.18.34'
shadow project(path: ":common") shadow project(path: ":common")
} }

View File

@@ -49,6 +49,7 @@ import net.william278.husksync.database.MongoDbDatabase;
import net.william278.husksync.database.MySqlDatabase; import net.william278.husksync.database.MySqlDatabase;
import net.william278.husksync.database.PostgresDatabase; import net.william278.husksync.database.PostgresDatabase;
import net.william278.husksync.event.FabricEventDispatcher; import net.william278.husksync.event.FabricEventDispatcher;
import net.william278.husksync.event.ModLoadedCallback;
import net.william278.husksync.hook.PlanHook; import net.william278.husksync.hook.PlanHook;
import net.william278.husksync.listener.EventListener; import net.william278.husksync.listener.EventListener;
import net.william278.husksync.listener.FabricEventListener; import net.william278.husksync.listener.FabricEventListener;
@@ -78,12 +79,12 @@ import java.util.logging.Level;
@Getter @Getter
@NoArgsConstructor @NoArgsConstructor
public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync, FabricTask.Supplier, public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync, FabricTask.Supplier,
FabricEventDispatcher { FabricEventDispatcher {
private static final String PLATFORM_TYPE_ID = "fabric"; private static final String PLATFORM_TYPE_ID = "fabric";
private final TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap( private final TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap(
SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR
); );
private final Map<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap(); private final Map<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap();
private final Map<String, Boolean> permissions = Maps.newHashMap(); private final Map<String, Boolean> permissions = Maps.newHashMap();
@@ -143,6 +144,9 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
// Initial plugin setup // Initial plugin setup
this.audiences = FabricServerAudiences.of(minecraftServer); this.audiences = FabricServerAudiences.of(minecraftServer);
// Check compatibility
checkCompatibility();
// Prepare data adapter // Prepare data adapter
initialize("data adapter", (plugin) -> { initialize("data adapter", (plugin) -> {
if (getSettings().getSynchronization().isCompressData()) { if (getSettings().getSynchronization().isCompressData()) {
@@ -206,6 +210,16 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
// Check for updates // Check for updates
this.checkForUpdates(); this.checkForUpdates();
log(Level.WARNING, """
**************
WARNING:
HuskSync for Fabric is still in an alpha state and is
not considered production ready.
**************""");
ModLoadedCallback.EVENT.invoker().post(FabricHuskSyncAPI.getInstance());
} }
private void onDisable() { private void onDisable() {
@@ -264,15 +278,15 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
@Nullable @Nullable
public InputStream getResource(@NotNull String name) { public InputStream getResource(@NotNull String name) {
return this.mod.findPath(name) return this.mod.findPath(name)
.map(path -> { .map(path -> {
try { try {
return Files.newInputStream(path); return Files.newInputStream(path);
} catch (IOException e) { } catch (IOException e) {
log(Level.WARNING, "Failed to load resource: " + name, e); log(Level.WARNING, "Failed to load resource: " + name, e);
} }
return null; return null;
}) })
.orElse(this.getClass().getClassLoader().getResourceAsStream(name)); .orElse(this.getClass().getClassLoader().getResourceAsStream(name));
} }
@Override @Override
@@ -292,11 +306,11 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
@Override @Override
public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable) { public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable) {
LoggingEventBuilder logEvent = logger.makeLoggingEventBuilder( LoggingEventBuilder logEvent = logger.makeLoggingEventBuilder(
switch (level.getName()) { switch (level.getName()) {
case "WARNING" -> org.slf4j.event.Level.WARN; case "WARNING" -> org.slf4j.event.Level.WARN;
case "SEVERE" -> org.slf4j.event.Level.ERROR; case "SEVERE" -> org.slf4j.event.Level.ERROR;
default -> org.slf4j.event.Level.INFO; default -> org.slf4j.event.Level.INFO;
} }
); );
if (throwable.length >= 1) { if (throwable.length >= 1) {
logEvent = logEvent.setCause(throwable[0]); logEvent = logEvent.setCause(throwable[0]);
@@ -328,6 +342,14 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
return PLATFORM_TYPE_ID; return PLATFORM_TYPE_ID;
} }
@Override
@NotNull
public String getServerVersion() {
return String.format("%s %s/%s", getPlatformType(), FabricLoader.getInstance()
.getModContainer("fabricloader").map(l -> l.getMetadata().getVersion().getFriendlyString())
.orElse("unknown"), minecraftServer.getVersion());
}
@Override @Override
public Optional<LegacyConverter> getLegacyConverter() { public Optional<LegacyConverter> getLegacyConverter() {
return Optional.empty(); return Optional.empty();

View File

@@ -24,10 +24,9 @@ import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import lombok.*; import lombok.*;
import net.fabricmc.fabric.api.dimension.v1.FabricDimensions;
import net.minecraft.advancement.AdvancementProgress; import net.minecraft.advancement.AdvancementProgress;
import net.minecraft.advancement.PlayerAdvancementTracker; import net.minecraft.advancement.PlayerAdvancementTracker;
import net.minecraft.enchantment.EnchantmentHelper; import net.minecraft.component.DataComponentTypes;
import net.minecraft.entity.attribute.EntityAttribute; import net.minecraft.entity.attribute.EntityAttribute;
import net.minecraft.entity.attribute.EntityAttributeInstance; import net.minecraft.entity.attribute.EntityAttributeInstance;
import net.minecraft.entity.attribute.EntityAttributeModifier; import net.minecraft.entity.attribute.EntityAttributeModifier;
@@ -35,24 +34,26 @@ import net.minecraft.entity.effect.StatusEffect;
import net.minecraft.entity.effect.StatusEffectInstance; import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.entity.player.HungerManager; import net.minecraft.entity.player.HungerManager;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.registry.Registries; import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry; import net.minecraft.registry.Registry;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.stat.StatType; import net.minecraft.stat.StatType;
import net.minecraft.stat.Stats; import net.minecraft.stat.Stats;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.TeleportTarget; import net.minecraft.world.TeleportTarget;
import net.william278.desertwell.util.ThrowingConsumer; import net.william278.desertwell.util.ThrowingConsumer;
import net.william278.husksync.FabricHuskSync; import net.william278.husksync.FabricHuskSync;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.Adaptable; import net.william278.husksync.adapter.Adaptable;
import net.william278.husksync.config.Settings.SynchronizationSettings.AttributeSettings;
import net.william278.husksync.user.FabricUser; import net.william278.husksync.user.FabricUser;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range; import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable;
import java.util.*; import java.util.*;
@@ -86,13 +87,10 @@ public abstract class FabricData implements Data {
stack.getItem().toString(), stack.getItem().toString(),
stack.getCount(), stack.getCount(),
stack.getName().getString(), stack.getName().getString(),
Optional.ofNullable(stack.getSubNbt(ItemStack.DISPLAY_KEY)) stack.getComponents().get(DataComponentTypes.LORE).lines().stream().map(Text::getString).toList(),
.flatMap(display -> Optional.ofNullable(display.get(ItemStack.LORE_KEY)) stack.getEnchantments().getEnchantments().stream()
.map(lore -> ((List<String>) lore).stream().toList())) //todo check this is ok .map(RegistryEntry::getIdAsString)
.orElse(null), .filter(Objects::nonNull)
stack.getEnchantments().stream()
.map(element -> EnchantmentHelper.getIdFromNbt((NbtCompound) element))
.filter(Objects::nonNull).map(Identifier::toString)
.toList() .toList()
) : null) ) : null)
.toArray(Stack[]::new); .toArray(Stack[]::new);
@@ -237,8 +235,8 @@ public abstract class FabricData implements Data {
private final Collection<StatusEffectInstance> effects; private final Collection<StatusEffectInstance> effects;
@NotNull @NotNull
public static FabricData.PotionEffects from(@NotNull Collection<StatusEffectInstance> effects) { public static FabricData.PotionEffects from(@NotNull Collection<StatusEffectInstance> sei) {
return new FabricData.PotionEffects(effects); return new FabricData.PotionEffects(Lists.newArrayList(sei.stream().filter(e -> !e.isAmbient()).toList()));
} }
@NotNull @NotNull
@@ -247,7 +245,7 @@ public abstract class FabricData implements Data {
.map(effect -> { .map(effect -> {
final StatusEffect type = matchEffectType(effect.type()); final StatusEffect type = matchEffectType(effect.type());
return type != null ? new StatusEffectInstance( return type != null ? new StatusEffectInstance(
type, RegistryEntry.of(type),
effect.duration(), effect.duration(),
effect.amplifier(), effect.amplifier(),
effect.isAmbient(), effect.isAmbient(),
@@ -263,22 +261,26 @@ public abstract class FabricData implements Data {
@NotNull @NotNull
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static FabricData.PotionEffects empty() { public static FabricData.PotionEffects empty() {
return new FabricData.PotionEffects(List.of()); return new FabricData.PotionEffects(Lists.newArrayList());
} }
@Override @Override
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException { public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
final ServerPlayerEntity player = user.getPlayer(); final ServerPlayerEntity player = user.getPlayer();
player.getActiveStatusEffects().forEach((effect, instance) -> player.removeStatusEffect(effect)); //todo ambient check
List<StatusEffect> effectsToRemove = new ArrayList<>(player.getActiveStatusEffects().keySet().stream()
.map(RegistryEntry::value).toList());
effectsToRemove.forEach(effect -> player.removeStatusEffect(RegistryEntry.of(effect)));
getEffects().forEach(player::addStatusEffect); getEffects().forEach(player::addStatusEffect);
} }
@NotNull @NotNull
@Override @Override
@Unmodifiable
public List<Effect> getActiveEffects() { public List<Effect> getActiveEffects() {
return effects.stream() return effects.stream()
.map(potionEffect -> { .map(potionEffect -> {
final String key = getEffectId(potionEffect.getEffectType()); final String key = getEffectId(potionEffect.getEffectType().value());
return key != null ? new Effect( return key != null ? new Effect(
key, key,
potionEffect.getAmplifier(), potionEffect.getAmplifier(),
@@ -306,16 +308,16 @@ public abstract class FabricData implements Data {
public static FabricData.Advancements adapt(@NotNull ServerPlayerEntity player) { public static FabricData.Advancements adapt(@NotNull ServerPlayerEntity player) {
final MinecraftServer server = Objects.requireNonNull(player.getServer(), "Server is null"); final MinecraftServer server = Objects.requireNonNull(player.getServer(), "Server is null");
final List<Advancement> advancements = Lists.newArrayList(); final List<Advancement> advancements = Lists.newArrayList();
forEachAdvancement(server, advancement -> { forEachAdvancementEntry(server, advancementEntry -> {
final AdvancementProgress advancementProgress = player.getAdvancementTracker().getProgress(advancement); final AdvancementProgress advancementProgress = player.getAdvancementTracker().getProgress(advancementEntry);
final Map<String, Date> awardedCriteria = Maps.newHashMap(); final Map<String, Date> awardedCriteria = Maps.newHashMap();
advancementProgress.getObtainedCriteria().forEach((criteria) -> awardedCriteria.put(criteria, advancementProgress.getObtainedCriteria().forEach((criteria) -> awardedCriteria.put(criteria,
advancementProgress.getEarliestProgressObtainDate())); Date.from(advancementProgress.getEarliestProgressObtainDate())));
// Only save the advancement if criteria has been completed // Only save the advancement if criteria has been completed
if (!awardedCriteria.isEmpty()) { if (!awardedCriteria.isEmpty()) {
advancements.add(Advancement.adapt(advancement.getId().toString(), awardedCriteria)); advancements.add(Advancement.adapt(advancementEntry.id().asString(), awardedCriteria));
} }
}); });
return new FabricData.Advancements(advancements); return new FabricData.Advancements(advancements);
@@ -330,10 +332,10 @@ public abstract class FabricData implements Data {
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException { public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
final ServerPlayerEntity player = user.getPlayer(); final ServerPlayerEntity player = user.getPlayer();
final MinecraftServer server = Objects.requireNonNull(player.getServer(), "Server is null"); final MinecraftServer server = Objects.requireNonNull(player.getServer(), "Server is null");
plugin.runAsync(() -> forEachAdvancement(server, advancement -> { plugin.runAsync(() -> forEachAdvancementEntry(server, advancementEntry -> {
final AdvancementProgress progress = player.getAdvancementTracker().getProgress(advancement); final AdvancementProgress progress = player.getAdvancementTracker().getProgress(advancementEntry);
final Optional<Advancement> record = completed.stream() final Optional<Advancement> record = completed.stream()
.filter(r -> r.getKey().equals(advancement.getId().toString())) .filter(r -> r.getKey().equals(advancementEntry.id().toString()))
.findFirst(); .findFirst();
if (record.isEmpty()) { if (record.isEmpty()) {
return; return;
@@ -342,7 +344,7 @@ public abstract class FabricData implements Data {
final Map<String, Date> criteria = record.get().getCompletedCriteria(); final Map<String, Date> criteria = record.get().getCompletedCriteria();
final List<String> awarded = Lists.newArrayList(progress.getObtainedCriteria()); final List<String> awarded = Lists.newArrayList(progress.getObtainedCriteria());
this.setAdvancement( this.setAdvancement(
plugin, advancement, player, user, plugin, advancementEntry, player, user,
criteria.keySet().stream().filter(key -> !awarded.contains(key)).toList(), criteria.keySet().stream().filter(key -> !awarded.contains(key)).toList(),
awarded.stream().filter(key -> !criteria.containsKey(key)).toList() awarded.stream().filter(key -> !criteria.containsKey(key)).toList()
); );
@@ -350,7 +352,7 @@ public abstract class FabricData implements Data {
} }
private void setAdvancement(@NotNull FabricHuskSync plugin, private void setAdvancement(@NotNull FabricHuskSync plugin,
@NotNull net.minecraft.advancement.Advancement advancement, @NotNull net.minecraft.advancement.AdvancementEntry advancementEntry,
@NotNull ServerPlayerEntity player, @NotNull ServerPlayerEntity player,
@NotNull FabricUser user, @NotNull FabricUser user,
@NotNull List<String> toAward, @NotNull List<String> toAward,
@@ -362,21 +364,21 @@ public abstract class FabricData implements Data {
// Award and revoke advancement criteria // Award and revoke advancement criteria
final PlayerAdvancementTracker progress = player.getAdvancementTracker(); final PlayerAdvancementTracker progress = player.getAdvancementTracker();
toAward.forEach(a -> progress.grantCriterion(advancement, a)); toAward.forEach(a -> progress.grantCriterion(advancementEntry, a));
toRevoke.forEach(r -> progress.revokeCriterion(advancement, r)); toRevoke.forEach(r -> progress.revokeCriterion(advancementEntry, r));
// Restore player exp level & progress // Restore player exp level & progress
if (!toAward.isEmpty() if (!toAward.isEmpty()
&& (player.experienceLevel != expLevel || player.experienceProgress != expProgress)) { && (player.experienceLevel != expLevel || player.experienceProgress != expProgress)) {
player.setExperienceLevel(expLevel); player.setExperienceLevel(expLevel);
player.setExperiencePoints((int) (player.getNextLevelExperience() * expProgress)); player.setExperiencePoints((int) (player.getNextLevelExperience() * expProgress));
} }
}); });
} }
// Performs a consuming function for every advancement registered on the server // Performs a consuming function for every advancement entry registered on the server
private static void forEachAdvancement(@NotNull MinecraftServer server, private static void forEachAdvancementEntry(@NotNull MinecraftServer server,
@NotNull ThrowingConsumer<net.minecraft.advancement.Advancement> con) { @NotNull ThrowingConsumer<net.minecraft.advancement.AdvancementEntry> con) {
server.getAdvancementLoader().getAdvancements().forEach(con); server.getAdvancementLoader().getAdvancements().forEach(con);
} }
@@ -419,9 +421,9 @@ public abstract class FabricData implements Data {
player.getWorld(), "World is null" player.getWorld(), "World is null"
).getRegistryKey().getValue().toString(), ).getRegistryKey().getValue().toString(),
UUID.nameUUIDFromBytes( UUID.nameUUIDFromBytes(
player.getWorld().getDimensionKey().getValue().toString().getBytes() player.getWorld().getDimensionEntry().getIdAsString().getBytes()
), ),
player.getWorld().getDimensionKey().getValue().toString() player.getWorld().getDimensionEntry().getIdAsString()
) )
); );
} }
@@ -432,18 +434,15 @@ public abstract class FabricData implements Data {
final MinecraftServer server = plugin.getMinecraftServer(); final MinecraftServer server = plugin.getMinecraftServer();
try { try {
player.dismountVehicle(); player.dismountVehicle();
FabricDimensions.teleport( player.teleportTo(
player,
server.getWorld(server.getWorldRegistryKeys().stream()
.filter(key -> key.getValue().equals(Identifier.tryParse(world.name())))
.findFirst().orElseThrow(
() -> new IllegalStateException("Invalid world")
)),
new TeleportTarget( new TeleportTarget(
new Vec3d(x, y, z), server.getWorld(server.getWorldRegistryKeys().stream()
Vec3d.ZERO, .filter(key -> key.getValue().equals(Identifier.tryParse(world.name())))
yaw, .findFirst().orElseThrow(
pitch () -> new IllegalStateException("Invalid world")
)),
player,
TeleportTarget.NO_OP
) )
); );
} catch (Throwable e) { } catch (Throwable e) {
@@ -572,19 +571,19 @@ public abstract class FabricData implements Data {
@NotNull @NotNull
public static FabricData.Attributes adapt(@NotNull ServerPlayerEntity player, @NotNull HuskSync plugin) { public static FabricData.Attributes adapt(@NotNull ServerPlayerEntity player, @NotNull HuskSync plugin) {
final List<Attribute> attributes = Lists.newArrayList(); final List<Attribute> attributes = Lists.newArrayList();
final AttributeSettings settings = plugin.getSettings().getSynchronization().getAttributes();
Registries.ATTRIBUTE.forEach(id -> { Registries.ATTRIBUTE.forEach(id -> {
final EntityAttributeInstance instance = player.getAttributeInstance(id); final EntityAttributeInstance instance = player.getAttributeInstance(RegistryEntry.of(id));
final Identifier key = Registries.ATTRIBUTE.getId(id); final Identifier key = Registries.ATTRIBUTE.getId(id);
if (instance == null || key == null) { if (instance == null || key == null || settings.isIgnoredAttribute(key.asString())) {
return; return;
} }
final Set<Modifier> modifiers = Sets.newHashSet(); final Set<Modifier> modifiers = Sets.newHashSet();
instance.getModifiers().forEach(modifier -> modifiers.add(new Modifier( instance.getModifiers().forEach(modifier -> modifiers.add(new Modifier(
modifier.getId(), modifier.id().toString(),
modifier.getName(), modifier.value(),
modifier.getValue(), modifier.operation().getId(),
modifier.getOperation().getId(), Modifier.ANY_EQUIPMENT_SLOT_GROUP
-1
))); )));
attributes.add(new Attribute( attributes.add(new Attribute(
key.toString(), key.toString(),
@@ -611,10 +610,17 @@ public abstract class FabricData implements Data {
@Override @Override
protected void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) { protected void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) {
Registries.ATTRIBUTE.forEach(id -> applyAttribute( final AttributeSettings settings = plugin.getSettings().getSynchronization().getAttributes();
user.getPlayer().getAttributeInstance(id), Registries.ATTRIBUTE.forEach(id -> {
getAttribute(id).orElse(null) final Identifier key = Registries.ATTRIBUTE.getId(id);
)); if (key == null || settings.isIgnoredAttribute(key.toString())) {
return;
}
applyAttribute(
user.getPlayer().getAttributeInstance(RegistryEntry.of(id)),
getAttribute(id).orElse(null)
);
});
} }
@@ -623,14 +629,13 @@ public abstract class FabricData implements Data {
if (instance == null) { if (instance == null) {
return; return;
} }
instance.setBaseValue(attribute == null ? instance.getAttribute().getDefaultValue() : attribute.baseValue());
instance.getModifiers().forEach(instance::removeModifier); instance.getModifiers().forEach(instance::removeModifier);
instance.setBaseValue(attribute == null ? instance.getValue() : attribute.baseValue());
if (attribute != null) { if (attribute != null) {
attribute.modifiers().forEach(modifier -> instance.addPersistentModifier(new EntityAttributeModifier( attribute.modifiers().forEach(modifier -> instance.addTemporaryModifier(new EntityAttributeModifier(
modifier.uuid(), Identifier.of(modifier.uuid().toString()),
modifier.name(),
modifier.amount(), modifier.amount(),
EntityAttributeModifier.Operation.fromId(modifier.operationType()) EntityAttributeModifier.Operation.ID_TO_VALUE.apply(modifier.operation())
))); )));
} }
} }

View File

@@ -27,6 +27,7 @@ import lombok.AllArgsConstructor;
import net.minecraft.datafixer.TypeReferences; import net.minecraft.datafixer.TypeReferences;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.nbt.*; import net.minecraft.nbt.*;
import net.minecraft.registry.DynamicRegistryManager;
import net.william278.desertwell.util.Version; import net.william278.desertwell.util.Version;
import net.william278.husksync.FabricHuskSync; import net.william278.husksync.FabricHuskSync;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
@@ -58,7 +59,7 @@ public abstract class FabricSerializer {
} }
public static class Inventory extends FabricSerializer implements Serializer<FabricData.Items.Inventory>, public static class Inventory extends FabricSerializer implements Serializer<FabricData.Items.Inventory>,
ItemDeserializer { ItemDeserializer {
public Inventory(@NotNull HuskSync plugin) { public Inventory(@NotNull HuskSync plugin) {
super(plugin); super(plugin);
@@ -66,7 +67,7 @@ public abstract class FabricSerializer {
@Override @Override
public FabricData.Items.Inventory deserialize(@NotNull String serialized, @NotNull Version dataMcVersion) public FabricData.Items.Inventory deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
throws DeserializationException { throws DeserializationException {
// Read item NBT from string // Read item NBT from string
final FabricHuskSync plugin = (FabricHuskSync) getPlugin(); final FabricHuskSync plugin = (FabricHuskSync) getPlugin();
final NbtCompound root; final NbtCompound root;
@@ -79,8 +80,8 @@ public abstract class FabricSerializer {
// Deserialize the inventory data // Deserialize the inventory data
final NbtCompound items = root.contains(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null; final NbtCompound items = root.contains(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null;
return FabricData.Items.Inventory.from( return FabricData.Items.Inventory.from(
items != null ? getItems(items, dataMcVersion, plugin) : new ItemStack[INVENTORY_SLOT_COUNT], items != null ? getItems(items, dataMcVersion, plugin) : new ItemStack[INVENTORY_SLOT_COUNT],
root.contains(HELD_ITEM_SLOT_TAG) ? root.getInt(HELD_ITEM_SLOT_TAG) : 0 root.contains(HELD_ITEM_SLOT_TAG) ? root.getInt(HELD_ITEM_SLOT_TAG) : 0
); );
} }
@@ -94,7 +95,7 @@ public abstract class FabricSerializer {
public String serialize(@NotNull FabricData.Items.Inventory data) throws SerializationException { public String serialize(@NotNull FabricData.Items.Inventory data) throws SerializationException {
try { try {
final NbtCompound root = new NbtCompound(); final NbtCompound root = new NbtCompound();
root.put(ITEMS_TAG, serializeItemArray(data.getContents())); root.put(ITEMS_TAG, serializeItemArray(data.getContents(), (FabricHuskSync) getPlugin()));
root.putInt(HELD_ITEM_SLOT_TAG, data.getHeldItemSlot()); root.putInt(HELD_ITEM_SLOT_TAG, data.getHeldItemSlot());
return root.toString(); return root.toString();
} catch (Throwable e) { } catch (Throwable e) {
@@ -105,7 +106,7 @@ public abstract class FabricSerializer {
} }
public static class EnderChest extends FabricSerializer implements Serializer<FabricData.Items.EnderChest>, public static class EnderChest extends FabricSerializer implements Serializer<FabricData.Items.EnderChest>,
ItemDeserializer { ItemDeserializer {
public EnderChest(@NotNull HuskSync plugin) { public EnderChest(@NotNull HuskSync plugin) {
super(plugin); super(plugin);
@@ -113,7 +114,7 @@ public abstract class FabricSerializer {
@Override @Override
public FabricData.Items.EnderChest deserialize(@NotNull String serialized, @NotNull Version dataMcVersion) public FabricData.Items.EnderChest deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
throws DeserializationException { throws DeserializationException {
final FabricHuskSync plugin = (FabricHuskSync) getPlugin(); final FabricHuskSync plugin = (FabricHuskSync) getPlugin();
try { try {
final NbtCompound items = StringNbtReader.parse(serialized); final NbtCompound items = StringNbtReader.parse(serialized);
@@ -132,7 +133,7 @@ public abstract class FabricSerializer {
@Override @Override
public String serialize(@NotNull FabricData.Items.EnderChest data) throws SerializationException { public String serialize(@NotNull FabricData.Items.EnderChest data) throws SerializationException {
try { try {
return serializeItemArray(data.getContents()).toString(); return serializeItemArray(data.getContents(), (FabricHuskSync) getPlugin()).toString();
} catch (Throwable e) { } catch (Throwable e) {
throw new SerializationException("Failed to serialize ender chest item NBT to string", e); throw new SerializationException("Failed to serialize ender chest item NBT to string", e);
} }
@@ -161,9 +162,10 @@ public abstract class FabricSerializer {
final ItemStack[] contents = new ItemStack[tag.getInt("size")]; final ItemStack[] contents = new ItemStack[tag.getInt("size")];
final NbtList itemList = tag.getList("items", NbtElement.COMPOUND_TYPE); final NbtList itemList = tag.getList("items", NbtElement.COMPOUND_TYPE);
final DynamicRegistryManager registryManager = plugin.getMinecraftServer().getRegistryManager();
itemList.forEach(element -> { itemList.forEach(element -> {
final NbtCompound compound = (NbtCompound) element; final NbtCompound compound = (NbtCompound) element;
contents[compound.getInt("Slot")] = ItemStack.fromNbt(compound); contents[compound.getInt("Slot")] = ItemStack.fromNbt(registryManager, element).get();
}); });
plugin.debug(Arrays.toString(contents)); plugin.debug(Arrays.toString(contents));
return contents; return contents;
@@ -174,18 +176,18 @@ public abstract class FabricSerializer {
// Serialize items slot-by-slot // Serialize items slot-by-slot
@NotNull @NotNull
default NbtCompound serializeItemArray(@Nullable ItemStack @NotNull [] items) { default NbtCompound serializeItemArray(@Nullable ItemStack @NotNull [] items, @NotNull FabricHuskSync plugin) {
final NbtCompound container = new NbtCompound(); final NbtCompound container = new NbtCompound();
container.putInt("size", items.length); container.putInt("size", items.length);
final NbtList itemList = new NbtList(); final NbtList itemList = new NbtList();
final DynamicRegistryManager registryManager = plugin.getMinecraftServer().getRegistryManager();
for (int i = 0; i < items.length; i++) { for (int i = 0; i < items.length; i++) {
final ItemStack item = items[i]; final ItemStack item = items[i];
if (item == null || item.isEmpty()) { if (item == null || item.isEmpty()) {
continue; continue;
} }
NbtCompound entry = new NbtCompound(); NbtCompound entry = (NbtCompound) item.encode(registryManager);
entry.putInt("Slot", i); entry.putInt("Slot", i);
item.writeNbt(entry);
itemList.add(entry); itemList.add(entry);
} }
container.put(ITEMS_TAG, itemList); container.put(ITEMS_TAG, itemList);
@@ -198,6 +200,7 @@ public abstract class FabricSerializer {
final int size = items.getInt("size"); final int size = items.getInt("size");
final NbtList list = items.getList("items", NbtElement.COMPOUND_TYPE); final NbtList list = items.getList("items", NbtElement.COMPOUND_TYPE);
final ItemStack[] itemStacks = new ItemStack[size]; final ItemStack[] itemStacks = new ItemStack[size];
final DynamicRegistryManager registryManager = plugin.getMinecraftServer().getRegistryManager();
Arrays.fill(itemStacks, ItemStack.EMPTY); Arrays.fill(itemStacks, ItemStack.EMPTY);
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
if (list.getCompound(i) == null) { if (list.getCompound(i) == null) {
@@ -205,7 +208,7 @@ public abstract class FabricSerializer {
} }
final NbtCompound compound = list.getCompound(i); final NbtCompound compound = list.getCompound(i);
final int slot = compound.getInt("Slot"); final int slot = compound.getInt("Slot");
itemStacks[slot] = ItemStack.fromNbt(upgradeItemData(list.getCompound(i), mcVersion, plugin)); itemStacks[slot] = ItemStack.fromNbt(registryManager, upgradeItemData(list.getCompound(i), mcVersion, plugin)).get();
} }
return itemStacks; return itemStacks;
} }
@@ -216,8 +219,8 @@ public abstract class FabricSerializer {
private NbtCompound upgradeItemData(@NotNull NbtCompound tag, @NotNull Version mcVersion, private NbtCompound upgradeItemData(@NotNull NbtCompound tag, @NotNull Version mcVersion,
@NotNull FabricHuskSync plugin) { @NotNull FabricHuskSync plugin) {
return (NbtCompound) plugin.getMinecraftServer().getDataFixer().update( return (NbtCompound) plugin.getMinecraftServer().getDataFixer().update(
TypeReferences.ITEM_STACK, new Dynamic<Object>((DynamicOps) NbtOps.INSTANCE, tag), TypeReferences.ITEM_STACK, new Dynamic<Object>((DynamicOps) NbtOps.INSTANCE, tag),
getDataVersion(mcVersion), getDataVersion(plugin.getMinecraftVersion()) getDataVersion(mcVersion), getDataVersion(plugin.getMinecraftVersion())
).getValue(); ).getValue();
} }
@@ -251,7 +254,7 @@ public abstract class FabricSerializer {
@Override @Override
public FabricData.PotionEffects deserialize(@NotNull String serialized) throws DeserializationException { public FabricData.PotionEffects deserialize(@NotNull String serialized) throws DeserializationException {
return FabricData.PotionEffects.adapt( return FabricData.PotionEffects.adapt(
plugin.getGson().fromJson(serialized, TYPE.getType()) plugin.getGson().fromJson(serialized, TYPE.getType())
); );
} }
@@ -275,7 +278,7 @@ public abstract class FabricSerializer {
@Override @Override
public FabricData.Advancements deserialize(@NotNull String serialized) throws DeserializationException { public FabricData.Advancements deserialize(@NotNull String serialized) throws DeserializationException {
return FabricData.Advancements.from( return FabricData.Advancements.from(
plugin.getGson().fromJson(serialized, TYPE.getType()) plugin.getGson().fromJson(serialized, TYPE.getType())
); );
} }

View File

@@ -104,7 +104,7 @@ public interface FabricUserDataHolder extends UserDataHolder {
@Override @Override
default Optional<Data.Items.EnderChest> getEnderChest() { default Optional<Data.Items.EnderChest> getEnderChest() {
return Optional.of(FabricData.Items.EnderChest.adapt( return Optional.of(FabricData.Items.EnderChest.adapt(
getPlayer().getEnderChestInventory().stacks getPlayer().getEnderChestInventory().getHeldStacks()
)); ));
} }

View File

@@ -0,0 +1,39 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.event;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.william278.husksync.api.HuskSyncAPI;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
public interface ModLoadedCallback {
@NotNull
Event<ModLoadedCallback> EVENT = EventFactory.createArrayBacked(
ModLoadedCallback.class,
(listeners) -> (api) -> Arrays.stream(listeners).forEach(listener -> listener.post(api))
);
void post(@NotNull HuskSyncAPI api);
}

View File

@@ -54,8 +54,6 @@ import net.william278.husksync.user.OnlineUser;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.stream.Collectors;
public class FabricEventListener extends EventListener implements LockedHandler { public class FabricEventListener extends EventListener implements LockedHandler {
public FabricEventListener(@NotNull HuskSync plugin) { public FabricEventListener(@NotNull HuskSync plugin) {

View File

@@ -20,9 +20,11 @@
package net.william278.husksync.mixins; package net.william278.husksync.mixins;
import net.minecraft.enchantment.EnchantmentHelper; import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.enchantment.Enchantments;
import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory; import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.registry.tag.TagKey;
import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.network.ServerPlayerEntity;
import net.william278.husksync.event.PlayerDeathDropsCallback; import net.william278.husksync.event.PlayerDeathDropsCallback;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -54,7 +56,7 @@ public class PlayerEntityMixin {
final @Nullable ItemStack @NotNull [] toKeep = new ItemStack[inventory.size()]; final @Nullable ItemStack @NotNull [] toKeep = new ItemStack[inventory.size()];
for (int i = 0; i < inventory.size(); ++i) { for (int i = 0; i < inventory.size(); ++i) {
ItemStack itemStack = inventory.getStack(i); ItemStack itemStack = inventory.getStack(i);
if (!itemStack.isEmpty() && EnchantmentHelper.hasVanishingCurse(itemStack)) { if (!itemStack.isEmpty() && EnchantmentHelper.hasAnyEnchantmentsIn(itemStack, TagKey.of(Enchantments.VANISHING_CURSE.getRegistryRef(), Enchantments.VANISHING_CURSE.getValue()))) {
toKeep[i] = null; toKeep[i] = null;
continue; continue;
} }
@@ -69,7 +71,7 @@ public class PlayerEntityMixin {
final @Nullable ItemStack @NotNull [] toDrop = new ItemStack[inventory.size()]; final @Nullable ItemStack @NotNull [] toDrop = new ItemStack[inventory.size()];
for (int i = 0; i < inventory.size(); ++i) { for (int i = 0; i < inventory.size(); ++i) {
ItemStack itemStack = inventory.getStack(i); ItemStack itemStack = inventory.getStack(i);
if (!itemStack.isEmpty() && EnchantmentHelper.hasVanishingCurse(itemStack)) { if (!itemStack.isEmpty() && EnchantmentHelper.hasAnyEnchantmentsIn(itemStack, TagKey.of(Enchantments.VANISHING_CURSE.getRegistryRef(), Enchantments.VANISHING_CURSE.getValue()))) {
toDrop[i] = itemStack; toDrop[i] = itemStack;
continue; continue;
} }

View File

@@ -44,13 +44,12 @@ public abstract class ServerPlayNetworkHandlerMixin {
@Shadow @Shadow
public ServerPlayerEntity player; public ServerPlayerEntity player;
@Shadow
public abstract void sendPacket(Packet<?> packet); public abstract void sendPacket(Packet<?> packet);
@Inject(method = "onPlayerAction", at = @At("HEAD"), cancellable = true) @Inject(method = "onPlayerAction", at = @At("HEAD"), cancellable = true)
public void onPlayerAction(PlayerActionC2SPacket packet, CallbackInfo ci) { public void onPlayerAction(PlayerActionC2SPacket packet, CallbackInfo ci) {
if (packet.getAction() == PlayerActionC2SPacket.Action.DROP_ITEM if (packet.getAction() == PlayerActionC2SPacket.Action.DROP_ITEM
|| packet.getAction() == PlayerActionC2SPacket.Action.DROP_ALL_ITEMS) { || packet.getAction() == PlayerActionC2SPacket.Action.DROP_ALL_ITEMS) {
ItemStack stack = player.getStackInHand(Hand.MAIN_HAND); ItemStack stack = player.getStackInHand(Hand.MAIN_HAND);
ActionResult result = ItemDropCallback.EVENT.invoker().interact(player, stack); ActionResult result = ItemDropCallback.EVENT.invoker().interact(player, stack);
@@ -83,7 +82,7 @@ public abstract class ServerPlayNetworkHandlerMixin {
@Inject(method = "onCreativeInventoryAction", at = @At("HEAD"), cancellable = true) @Inject(method = "onCreativeInventoryAction", at = @At("HEAD"), cancellable = true)
public void onCreativeInventoryAction(CreativeInventoryActionC2SPacket packet, CallbackInfo ci) { public void onCreativeInventoryAction(CreativeInventoryActionC2SPacket packet, CallbackInfo ci) {
int slot = packet.getSlot(); int slot = packet.slot();
if (slot < 0) return; if (slot < 0) return;
ItemStack stack = this.player.getInventory().getStack(slot); ItemStack stack = this.player.getInventory().getStack(slot);

View File

@@ -21,7 +21,6 @@ package net.william278.husksync.mixins;
import net.minecraft.entity.ItemEntity; import net.minecraft.entity.ItemEntity;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.ActionResult; import net.minecraft.util.ActionResult;
import net.william278.husksync.event.ItemDropCallback; import net.william278.husksync.event.ItemDropCallback;

View File

@@ -40,6 +40,7 @@ import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.Level;
public class FabricUser extends OnlineUser implements FabricUserDataHolder { public class FabricUser extends OnlineUser implements FabricUserDataHolder {
@@ -70,9 +71,12 @@ public class FabricUser extends OnlineUser implements FabricUserDataHolder {
} }
@Override @Override
@Deprecated(since = "3.6.7")
public void sendToast(@NotNull MineDown title, @NotNull MineDown description, @NotNull String iconMaterial, public void sendToast(@NotNull MineDown title, @NotNull MineDown description, @NotNull String iconMaterial,
@NotNull String backgroundType) { @NotNull String backgroundType) {
getAudience().sendActionBar(title.toComponent()); // Toasts unimplemented for now plugin.log(Level.WARNING, "Toast notifications are deprecated. " +
"Please change your notification display slot to CHAT, ACTION_BAR or NONE.");
this.sendActionBar(title);
} }
@Override @Override

View File

@@ -19,18 +19,21 @@
package net.william278.husksync.util; package net.william278.husksync.util;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import net.william278.husksync.FabricHuskSync; import net.william278.husksync.FabricHuskSync;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.UserDataHolder; import net.william278.husksync.data.UserDataHolder;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public interface FabricTask extends Task { public interface FabricTask extends Task {
ScheduledExecutorService ASYNC_EXEC = Executors.newScheduledThreadPool(4,
new ThreadFactoryBuilder()
.setDaemon(true)
.setNameFormat("HuskSync-ThreadPool")
.build());
class Sync extends Task.Sync implements FabricTask { class Sync extends Task.Sync implements FabricTask {
@@ -46,7 +49,7 @@ public interface FabricTask extends Task {
@Override @Override
public void run() { public void run() {
if (!cancelled) { if (!cancelled) {
Executors.newSingleThreadScheduledExecutor().schedule( ASYNC_EXEC.schedule(
() -> ((FabricHuskSync) getPlugin()).getMinecraftServer().executeSync(runnable), () -> ((FabricHuskSync) getPlugin()).getMinecraftServer().executeSync(runnable),
delayTicks * 50, delayTicks * 50,
TimeUnit.MILLISECONDS TimeUnit.MILLISECONDS
@@ -73,7 +76,7 @@ public interface FabricTask extends Task {
@Override @Override
public void run() { public void run() {
if (!cancelled) { if (!cancelled) {
this.task = CompletableFuture.runAsync(runnable, ((FabricHuskSync) getPlugin()).getMinecraftServer()); this.task = CompletableFuture.runAsync(runnable, ASYNC_EXEC);
} }
} }
} }
@@ -97,7 +100,7 @@ public interface FabricTask extends Task {
@Override @Override
public void run() { public void run() {
if (!cancelled) { if (!cancelled) {
this.task = Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate( this.task = ASYNC_EXEC.scheduleAtFixedRate(
runnable, runnable,
0, 0,
repeatingTicks * 50, repeatingTicks * 50,
@@ -129,7 +132,7 @@ public interface FabricTask extends Task {
@Override @Override
default void cancelTasks() { default void cancelTasks() {
// Do nothing ASYNC_EXEC.shutdownNow();
} }
} }

View File

@@ -40,7 +40,7 @@
}, },
"depends": { "depends": {
"fabricloader": ">=${fabric_loader_version}", "fabricloader": ">=${fabric_loader_version}",
"minecraft": ">=${fabric_minecraft_version}", "minecraft": ">=${minecraft_version}",
"fabric-api": "*" "fabric-api": "*"
}, },
"suggests": { "suggests": {

View File

@@ -1,23 +1,30 @@
# Gradle settings
org.gradle.jvmargs='-Dfile.encoding=UTF-8' org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.daemon=true org.gradle.daemon=true
javaVersion=17 javaVersion=21
plugin_version=3.6.1 # Plugin settings
plugin_version=3.7
minecraft_version=1.21.1
plugin_archive=husksync plugin_archive=husksync
plugin_description=A modern, cross-server player data synchronization system plugin_description=A modern, cross-server player data synchronization system
jedis_version=5.1.3 # Drivers
mysql_driver_version=8.4.0 jedis_version=5.1.4
mariadb_driver_version=3.4.0 mysql_driver_version=9.0.0
mariadb_driver_version=3.4.1
postgres_driver_version=42.7.3 postgres_driver_version=42.7.3
mongodb_driver_version=5.1.0 mongodb_driver_version=5.1.2
snappy_version=1.1.10.5 snappy_version=1.1.10.6
fabric_minecraft_version=1.20.1 # Spigot/Paper build settings
fabric_loader_version=0.15.11 bukkit_spigot_api=1.21.1-R0.1-SNAPSHOT
fabric_yarn_mappings=1.20.1+build.10 bukkit_paper_api=1.21.1-R0.1-SNAPSHOT
fabric_api_version=0.92.2+1.20.1
adventure_platform_fabric_version=5.9.0 # Fabric build settings
fabric_permissions_api_version=0.2-SNAPSHOT fabric_loader_version=0.16.2
sgui_version=1.2.2+1.20 fabric_yarn_mappings=1.21.1+build.3
fabric_api_version=0.102.1+1.21.1
fabric_adventure_platform_version=5.14.1
fabric_permissions_api_version=0.3.1
fabric_sgui_version=1.6.0+1.21

View File

@@ -1,14 +1,18 @@
plugins {
id 'xyz.jpenilla.run-paper' version '2.3.1'
}
dependencies { dependencies {
implementation project(':bukkit') implementation project(':bukkit')
compileOnly project(':common') compileOnly project(':common')
implementation 'net.william278.uniform:uniform-paper:1.1.4' implementation 'net.william278.uniform:uniform-paper:1.2.1'
compileOnly 'io.papermc.paper:paper-api:1.19.4-R0.1-SNAPSHOT' compileOnly "io.papermc.paper:paper-api:${bukkit_paper_api}"
compileOnly 'org.jetbrains:annotations:24.1.0' compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'org.projectlombok:lombok:1.18.32' compileOnly 'org.projectlombok:lombok:1.18.34'
annotationProcessor 'org.projectlombok:lombok:1.18.32' annotationProcessor 'org.projectlombok:lombok:1.18.34'
} }
shadowJar { shadowJar {
@@ -30,7 +34,6 @@ shadowJar {
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell' relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown' relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi' relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
relocate 'net.william278.andjam', 'net.william278.husksync.libraries.andjam'
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter' relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter' relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
relocate 'org.json', 'net.william278.husksync.libraries.json' relocate 'org.json', 'net.william278.husksync.libraries.json'
@@ -43,3 +46,9 @@ shadowJar {
minimize() minimize()
} }
tasks {
runServer {
minecraftVersion('1.21.1')
}
}

View File

@@ -20,6 +20,7 @@
package net.william278.husksync; package net.william278.husksync;
import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.Audience;
import net.william278.desertwell.util.Version;
import net.william278.husksync.listener.BukkitEventListener; import net.william278.husksync.listener.BukkitEventListener;
import net.william278.husksync.listener.PaperEventListener; import net.william278.husksync.listener.PaperEventListener;
import net.william278.uniform.Uniform; import net.william278.uniform.Uniform;
@@ -45,6 +46,12 @@ public class PaperHuskSync extends BukkitHuskSync {
return player == null || !player.isOnline() ? Audience.empty() : player; return player == null || !player.isOnline() ? Audience.empty() : player;
} }
@NotNull
@Override
public Version getMinecraftVersion() {
return Version.fromString(getServer().getMinecraftVersion());
}
@Override @Override
@NotNull @NotNull
public Uniform getUniform() { public Uniform getUniform() {

View File

@@ -1,4 +1,4 @@
certifi==2023.7.22 certifi==2024.7.4
charset-normalizer==3.2.0 charset-normalizer==3.2.0
colorama==0.4.6 colorama==0.4.6
idna==3.7 idna==3.7

View File

@@ -13,7 +13,7 @@ from tqdm import tqdm
class Parameters: class Parameters:
root_dir = './servers/' root_dir = './servers/'
proxy_version = "1.21" proxy_version = "1.21"
minecraft_version = '1.21' minecraft_version = '1.21.1'
eula_agreement = 'true' eula_agreement = 'true'
backend_names = ['alpha', 'beta'] backend_names = ['alpha', 'beta']