9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-26 01:59:20 +00:00

Compare commits

...

58 Commits
3.6.8 ... 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
Dong Heon Hee
f456443da0 Merge branch 'master' into master 2024-08-05 07:39:13 +09: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
42 changed files with 739 additions and 458 deletions

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

View File

@@ -14,32 +14,36 @@ permissions:
jobs: jobs:
build: build:
name: 'Build - 1.21.1'
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: 'Checkout for CI 🛎️'
uses: actions/checkout@v4
env: env:
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
- name: '[Current - 1.21.1] Build 🛎️'
run: |
./gradlew clean build publish
- 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: 'Fetch Version Name 📝' - name: 'Fetch Version String 📝'
run: | run: |
echo "::set-output name=VERSION_NAME::$(${{github.workspace}}/gradlew properties --no-daemon --console=plain -q | grep "^version:" | awk '{printf $2}')" echo "::set-output name=VERSION_NAME::$(./gradlew properties --no-daemon --console=plain -q | grep "^version:" | awk '{printf $2}')"
id: fetch-version id: fetch-version
- name: Get Version - name: 'Set Version Variable 📝'
run: | run: |
echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV
- name: 'Publish to William278.net 🚀' - name: 'Publish to William278.net 🚀'
@@ -51,14 +55,14 @@ jobs:
version: ${{ env.version_name }} version: ${{ env.version_name }}
changelog: ${{ github.event.head_commit.message }} changelog: ${{ github.event.head_commit.message }}
distro-names: | distro-names: |
paper paper-1.21.1
fabric-1.20.1 fabric-1.21.1
distro-groups: | distro-groups: |
paper paper
fabric fabric
distro-descriptions: | distro-descriptions: |
Paper Paper 1.21.1
Fabric 1.20.1 Fabric 1.21.1
files: | files: |
target/HuskSync-Paper-${{ env.version_name }}.jar target/HuskSync-Paper-${{ env.version_name }}+mc.1.21.1.jar
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.20.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,24 +8,46 @@ 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
@@ -40,14 +62,22 @@ jobs:
version: ${{ github.event.release.tag_name }} version: ${{ github.event.release.tag_name }}
changelog: ${{ github.event.release.body }} changelog: ${{ github.event.release.body }}
distro-names: | distro-names: |
paper paper-1.21.1
fabric-1.21.1
paper-1.20.1
fabric-1.20.1 fabric-1.20.1
distro-groups: | distro-groups: |
paper paper
fabric fabric
paper
fabric
distro-descriptions: | distro-descriptions: |
Paper Paper 1.21.1
Fabric 1.21.1
Paper 1.20.1
Fabric 1.20.1 Fabric 1.20.1
files: | files: |
target/HuskSync-Paper-${{ github.event.release.tag_name }}.jar 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 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.3' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.1'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.3' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.1'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.3' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.1'
} }
test { test {
@@ -125,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
@@ -163,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
@@ -176,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
@@ -190,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

@@ -5,21 +5,21 @@ dependencies {
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 'org.bstats:bstats-bukkit:3.0.2' implementation 'org.bstats:bstats-bukkit:3.1.0'
implementation 'net.kyori:adventure-platform-bukkit:4.3.3' implementation 'net.kyori:adventure-platform-bukkit:4.3.4'
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.2' 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.34' 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"

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

View File

@@ -25,9 +25,7 @@ import com.google.gson.annotations.SerializedName;
import de.tr7zw.changeme.nbtapi.NBTCompound; import de.tr7zw.changeme.nbtapi.NBTCompound;
import de.tr7zw.changeme.nbtapi.NBTPersistentDataContainer; import de.tr7zw.changeme.nbtapi.NBTPersistentDataContainer;
import lombok.*; import lombok.*;
import net.kyori.adventure.util.TriState;
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;
@@ -37,9 +35,10 @@ import org.bukkit.*;
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;
@@ -49,7 +48,6 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range; import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable; import org.jetbrains.annotations.Unmodifiable;
import java.lang.reflect.Constructor;
import java.util.*; import java.util.*;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -157,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
} }
} }
@@ -283,7 +278,7 @@ public abstract class BukkitData implements Data {
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(),
@@ -458,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);
@@ -481,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);
@@ -531,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);
} }
} }
@@ -566,13 +551,9 @@ 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 static final String EQUIPMENT_SLOT_GROUP = "org.bukkit.inventory.EquipmentSlotGroup";
private static final String EQUIPMENT_SLOT_GROUP$ANY = "ANY";
private static final String EQUIPMENT_SLOT$getGroup = "getGroup";
private static TriState USE_KEYED_MODIFIERS = TriState.NOT_SET;
private List<Attribute> attributes; private List<Attribute> attributes;
@NotNull @NotNull
@@ -581,9 +562,8 @@ public abstract class BukkitData implements Data {
final AttributeSettings settings = plugin.getSettings().getSynchronization().getAttributes(); 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 || Double.compare(instance.getValue(), instance.getDefaultValue()) == 0 if (settings.isIgnoredAttribute(id.getKey().toString()) || instance == null) {
|| settings.isIgnoredAttribute(id.getKey().toString())) { return; // We don't sync attributes not marked as to be synced
return; // We don't sync unmodified or disabled attributes
} }
attributes.add(adapt(instance, settings)); attributes.add(adapt(instance, settings));
}); });
@@ -610,6 +590,7 @@ public abstract class BukkitData implements Data {
instance.getBaseValue(), instance.getBaseValue(),
instance.getModifiers().stream() instance.getModifiers().stream()
.filter(modifier -> !settings.isIgnoredModifier(modifier.getName())) .filter(modifier -> !settings.isIgnoredModifier(modifier.getName()))
.filter(modifier -> modifier.getSlotGroup() != EquipmentSlotGroup.ANY)
.map(BukkitData.Attributes::adapt).collect(Collectors.toSet()) .map(BukkitData.Attributes::adapt).collect(Collectors.toSet())
); );
} }
@@ -617,89 +598,47 @@ public abstract class BukkitData implements Data {
@NotNull @NotNull
private static Modifier adapt(@NotNull AttributeModifier modifier) { private static Modifier adapt(@NotNull AttributeModifier modifier) {
return new Modifier( return new Modifier(
getModifierId(modifier), 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()
); );
} }
@Nullable private static void applyAttribute(@Nullable AttributeInstance instance, @Nullable Attribute attribute) {
private static UUID getModifierId(@NotNull AttributeModifier modifier) {
try {
return modifier.getUniqueId();
} catch (Throwable e) {
return null;
}
}
private static boolean useKeyedModifiers(@NotNull HuskSync plugin) {
if (USE_KEYED_MODIFIERS == TriState.NOT_SET) {
boolean is1_21 = plugin.getMinecraftVersion().compareTo(Version.fromString("1.21")) >= 0;
USE_KEYED_MODIFIERS = TriState.byBoolean(is1_21);
return is1_21;
}
return Boolean.TRUE.equals(USE_KEYED_MODIFIERS.toBoolean());
}
private static void applyAttribute(@Nullable AttributeInstance instance, @Nullable Attribute attribute,
@NotNull HuskSync plugin) {
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().stream() attribute.modifiers().stream()
.filter(mod -> instance.getModifiers().stream().map(AttributeModifier::getName) .filter(mod -> instance.getModifiers().stream().map(AttributeModifier::getName)
.noneMatch(n -> n.equals(mod.name()))) .noneMatch(n -> n.equals(mod.name())))
.distinct() .distinct().filter(mod -> !mod.hasUuid())
.filter(mod -> useKeyedModifiers(plugin) == !mod.hasUuid()) .forEach(mod -> instance.addModifier(adapt(mod)));
.forEach(mod -> instance.addModifier(adapt(mod, plugin)));
} }
} }
@SuppressWarnings("JavaReflectionMemberAccess")
@NotNull @NotNull
private static AttributeModifier adapt(@NotNull Modifier modifier, @NotNull HuskSync plugin) { private static AttributeModifier adapt(@NotNull Modifier modifier) {
final int slotId = modifier.equipmentSlot();
if (useKeyedModifiers(plugin)) {
try {
// Reflexively create a modern keyed attribute modifier instance. Remove in favor of API long-term.
final EquipmentSlot slot = slotId != -1 ? EquipmentSlot.values()[slotId] : null;
final Class<?> slotGroup = Class.forName(EQUIPMENT_SLOT_GROUP);
final String modifierName = modifier.name() == null ? modifier.uuid().toString() : modifier.name();
final NamespacedKey modifierKey = Objects.requireNonNull(NamespacedKey.fromString(modifierName),
"Modifier key returned null");
final Constructor<AttributeModifier> constructor = AttributeModifier.class.getDeclaredConstructor(
NamespacedKey.class, double.class, AttributeModifier.Operation.class, slotGroup);
return constructor.newInstance(
modifierKey,
modifier.amount(),
AttributeModifier.Operation.values()[modifier.operationType()],
slot == null ? slotGroup.getField(EQUIPMENT_SLOT_GROUP$ANY).get(null)
: EquipmentSlot.class.getDeclaredMethod(EQUIPMENT_SLOT$getGroup).invoke(slot)
);
} catch (Throwable e) {
plugin.log(Level.WARNING, "Error reflectively creating keyed attribute modifier", e);
USE_KEYED_MODIFIERS = TriState.FALSE;
}
}
return new AttributeModifier( return new AttributeModifier(
modifier.uuid(), Objects.requireNonNull(NamespacedKey.fromString(modifier.name())),
modifier.name(),
modifier.amount(), modifier.amount(),
AttributeModifier.Operation.values()[modifier.operationType()], AttributeModifier.Operation.values()[modifier.operation()],
slotId != -1 ? EquipmentSlot.values()[slotId] : null Optional.ofNullable(EquipmentSlotGroup.getByName(modifier.slotGroup())).orElse(EquipmentSlotGroup.ANY)
); );
} }
@Override @Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException { public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
Registry.ATTRIBUTE.forEach(id -> applyAttribute( final AttributeSettings settings = plugin.getSettings().getSynchronization().getAttributes();
user.getPlayer().getAttribute(id), getAttribute(id).orElse(null), plugin Registry.ATTRIBUTE.forEach(id -> {
)); if (settings.isIgnoredAttribute(id.getKey().toString())) {
return;
}
applyAttribute(user.getPlayer().getAttribute(id), getAttribute(id).orElse(null));
});
} }
} }

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);
} }
@@ -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));
} }
} }
@@ -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
@@ -424,23 +444,20 @@ public interface BukkitMapPersister {
@NotNull @NotNull
private MapData extractMapData() { private MapData extractMapData() {
final List<MapBanner> banners = Lists.newArrayList(); final List<MapBanner> banners = Lists.newArrayList();
try { 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().getKey().getKey();
final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH); 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() ));
));
}
} }
} catch (Throwable ignored) {
} }
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,7 +12,7 @@ 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'
} }
@@ -21,8 +21,8 @@ dependencies {
compileOnly 'org.projectlombok:lombok:1.18.34' 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,7 +33,7 @@ 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'

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;
@@ -338,7 +340,11 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
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

@@ -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;
@@ -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());
} }
@@ -182,6 +187,35 @@ public class HuskSyncCommand extends PluginCommand {
}; };
} }
@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 -> {

View File

@@ -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());
@@ -275,12 +278,19 @@ public class Settings {
@NoArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class AttributeSettings { public static class AttributeSettings {
@Comment({"Which attributes should not be saved when syncing users. Supports wildcard matching.", @Comment({"Which attribute types should be saved as part of attribute syncing. Supports wildcard matching.",
"(e.g. ['minecraft:generic.max_health', 'minecraft:generic.*'])"}) "(e.g. ['minecraft:generic.max_health', 'minecraft:generic.*'])"})
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
private List<String> ignoredAttributes = new ArrayList<>(List.of("")); 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 modifiers should not be saved when syncing users. Supports wildcard matching.", @Comment({"Which attribute modifiers should be saved. Supports wildcard matching.",
"(e.g. ['minecraft:effect.speed', 'minecraft:effect.*'])"}) "(e.g. ['minecraft:effect.speed', 'minecraft:effect.*'])"})
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
private List<String> ignoredModifiers = new ArrayList<>(List.of( private List<String> ignoredModifiers = new ArrayList<>(List.of(
@@ -298,7 +308,7 @@ public class Settings {
} }
public boolean isIgnoredAttribute(@NotNull String attribute) { public boolean isIgnoredAttribute(@NotNull String attribute) {
return ignoredAttributes.stream().anyMatch(wildcard -> matchesWildcard(wildcard, attribute)); return syncedAttributes.stream().noneMatch(wildcard -> matchesWildcard(wildcard, attribute));
} }
public boolean isIgnoredModifier(@NotNull String modifier) { public boolean isIgnoredModifier(@NotNull String modifier) {

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
@@ -341,42 +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) {
if (obj instanceof Modifier other) { if (obj instanceof Modifier other) {
if (uuid == null || other.uuid == null) { if (uuid != null && other.uuid != null) {
return name.equals(other.name); return uuid.equals(other.uuid);
} }
return uuid.equals(other.uuid); return name.equals(other.name);
} }
return super.equals(obj); 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);

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

@@ -57,11 +57,6 @@ public class MongoDbDatabase extends Database {
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();
@@ -69,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);
} }
@@ -94,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) {
@@ -136,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) {
@@ -158,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) {
@@ -181,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) {
@@ -209,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
@@ -238,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) {
@@ -266,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) {
@@ -297,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) {
@@ -320,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) {
@@ -352,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) {
@@ -374,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) {
@@ -396,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() {
@@ -410,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));
@@ -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));
@@ -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

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

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

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

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

@@ -136,10 +136,22 @@ synchronization:
- '*' - '*'
# Configuration for how to sync attributes # Configuration for how to sync attributes
attributes: attributes:
# Which attributes should not be saved when syncing users. Supports wildcard matching. # Which attribute types should be saved as part of attribute syncing. Supports wildcard matching.
# (e.g. ['minecraft:generic.max_health', 'minecraft:generic.*']) # (e.g. ['minecraft:generic.max_health', 'minecraft:generic.*'])
ignored_attributes: [] synced_attributes:
# Which modifiers should not be saved when syncing users. Supports wildcard matching. - "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.*']) # (e.g. ['minecraft:effect.speed', 'minecraft:effect.*'])
ignored_modifiers: ['minecraft:effect.*', 'minecraft:creative_mode_*'] 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

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,13 +11,13 @@ 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.2.1+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}"

View File

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

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,20 +34,21 @@ 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;
@@ -87,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);
@@ -248,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(),
@@ -270,9 +267,10 @@ public abstract class FabricData implements Data {
@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();
final List<StatusEffect> effectsToRemove = player.getActiveStatusEffects().entrySet().stream() //todo ambient check
.filter(e -> !e.getValue().isAmbient()).map(Map.Entry::getKey).toList(); List<StatusEffect> effectsToRemove = new ArrayList<>(player.getActiveStatusEffects().keySet().stream()
effectsToRemove.forEach(player::removeStatusEffect); .map(RegistryEntry::value).toList());
effectsToRemove.forEach(effect -> player.removeStatusEffect(RegistryEntry.of(effect)));
getEffects().forEach(player::addStatusEffect); getEffects().forEach(player::addStatusEffect);
} }
@@ -282,7 +280,7 @@ public abstract class FabricData implements Data {
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(),
@@ -310,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);
@@ -334,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;
@@ -346,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()
); );
@@ -354,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,
@@ -366,8 +364,8 @@ 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()
@@ -378,9 +376,9 @@ public abstract class FabricData implements Data {
}); });
} }
// 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);
} }
@@ -423,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()
) )
); );
} }
@@ -436,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) {
@@ -576,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(),
@@ -615,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)
);
});
} }
@@ -627,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;
@@ -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) {
@@ -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;
} }

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

@@ -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,7 +44,6 @@ 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)
@@ -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

@@ -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,12 +1,15 @@
# 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.8 # 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
# Drivers
jedis_version=5.1.4 jedis_version=5.1.4
mysql_driver_version=9.0.0 mysql_driver_version=9.0.0
mariadb_driver_version=3.4.1 mariadb_driver_version=3.4.1
@@ -14,10 +17,14 @@ postgres_driver_version=42.7.3
mongodb_driver_version=5.1.2 mongodb_driver_version=5.1.2
snappy_version=1.1.10.6 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,5 +1,5 @@
plugins { plugins {
id 'xyz.jpenilla.run-paper' version '2.3.0' id 'xyz.jpenilla.run-paper' version '2.3.1'
} }
dependencies { dependencies {
@@ -8,7 +8,7 @@ dependencies {
implementation 'net.william278.uniform:uniform-paper:1.2.1' 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.34' compileOnly 'org.projectlombok:lombok:1.18.34'

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