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

Compare commits

..

160 Commits
3.6.1 ... 3.7.3

Author SHA1 Message Date
William278
a15739fbb9 build: bump Fabric & Runtime dependencies 2025-01-26 18:28:36 +00:00
William278
f4b9124636 build: set release year to 2025 2025-01-26 18:20:55 +00:00
William278
fecda83fcb build: bump to 3.7.3 2025-01-26 18:15:47 +00:00
William278
07228c3661 build: suppress some warnings in Packet-Events 2025-01-26 17:42:28 +00:00
William278
a0fb2e90b3 build: bump json to 20250107 2025-01-26 17:40:37 +00:00
William278
ae69c1c060 build: bump several dependencies at once 2025-01-26 17:40:11 +00:00
William278
4992f4492c feat: update Packet-Events support to 1.21.4 2025-01-26 17:38:01 +00:00
dependabot[bot]
58bd3acdc3 deps: bump net.kyori:adventure-api from 4.17.0 to 4.18.0 (#437)
Bumps [net.kyori:adventure-api](https://github.com/KyoriPowered/adventure) from 4.17.0 to 4.18.0.
- [Release notes](https://github.com/KyoriPowered/adventure/releases)
- [Commits](https://github.com/KyoriPowered/adventure/compare/v4.17.0...v4.18.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-09 12:21:59 +00:00
dependabot[bot]
af51c035a3 deps: bump org.apache.commons:commons-text from 1.12.0 to 1.13.0 (#435)
Bumps org.apache.commons:commons-text from 1.12.0 to 1.13.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-09 12:21:49 +00:00
dependabot[bot]
85ae2b5fb2 deps: bump net.william278.uniform:uniform-bukkit from 1.2.3 to 1.3 (#445)
Bumps net.william278.uniform:uniform-bukkit from 1.2.3 to 1.3.

---
updated-dependencies:
- dependency-name: net.william278.uniform:uniform-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>
2025-01-09 12:21:35 +00:00
Sóla Lusøt
7ff10b33a0 fix: Exception updating snapshots with Postgres due to LIMIT clause (#440)
Remove LIMIT clause which caused errors
2024-12-29 12:58:37 +00:00
dependabot[bot]
431c9e13c9 deps: bump de.tr7zw:item-nbt-api from 2.14.1-SNAPSHOT to 2.14.2-SNAPSHOT (#436)
Bumps de.tr7zw:item-nbt-api from 2.14.1-SNAPSHOT to 2.14.2-SNAPSHOT.

---
updated-dependencies:
- dependency-name: de.tr7zw:item-nbt-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-12-26 22:13:26 +00:00
dependabot[bot]
c8579fb987 deps: bump dev.triumphteam:triumph-gui from 3.1.10 to 3.1.11 (#431)
Bumps [dev.triumphteam:triumph-gui](https://github.com/TriumphTeam/triumph-gui) from 3.1.10 to 3.1.11.
- [Release notes](https://github.com/TriumphTeam/triumph-gui/releases)
- [Commits](https://github.com/TriumphTeam/triumph-gui/compare/3.1.10...3.1.11)

---
updated-dependencies:
- dependency-name: dev.triumphteam:triumph-gui
  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-12-14 13:26:17 +00:00
William278
2f4eb46456 docs: more README and Setup clarifications 2024-12-08 14:16:06 +00:00
William278
2d547507d5 docs: improve plugin setup documentation 2024-12-08 14:12:03 +00:00
William278
8e4678468e [ci skip] refactor: update config comment 2024-12-08 14:11:47 +00:00
William278
c2a32cabc5 fabric: remove alpha production ready warning.
Most folks ignore this messaging anyway and it's stable enough for general use.
2024-12-08 13:04:17 +00:00
William278
07f06aac68 docs: correct LTS date for 1.21.1 2024-12-08 13:02:55 +00:00
William
7ae1001b1b [ci skip] docs: update download URL 2024-12-07 21:51:12 +00:00
William
e04c19acf5 build: general dependency bump 2024-12-07 21:28:26 +00:00
William
1820a810f4 feat: add method for getting OnlineUser in common module 2024-12-07 20:58:40 +00:00
William
cedd12a048 feat: target Minecraft 1.21.4, replacing 1.21.3 2024-12-07 20:58:38 +00:00
dependabot[bot]
7967d00208 deps: bump commons-io:commons-io from 2.17.0 to 2.18.0 (#426)
Bumps commons-io:commons-io from 2.17.0 to 2.18.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-12-06 22:01:50 +00:00
dependabot[bot]
00a68be2ad deps: bump com.zaxxer:HikariCP from 6.2.0 to 6.2.1 (#427)
Bumps [com.zaxxer:HikariCP](https://github.com/brettwooldridge/HikariCP) from 6.2.0 to 6.2.1.
- [Changelog](https://github.com/brettwooldridge/HikariCP/blob/dev/CHANGES)
- [Commits](https://github.com/brettwooldridge/HikariCP/compare/HikariCP-6.2.0...HikariCP-6.2.1)

---
updated-dependencies:
- dependency-name: com.zaxxer:HikariCP
  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-12-06 22:01:41 +00:00
William278
da5d991d2a build: bump to 3.7.2 2024-11-21 16:16:48 +00:00
dependabot[bot]
c2f6d240ad deps: bump com.zaxxer:HikariCP from 6.1.0 to 6.2.0 (#422)
Bumps [com.zaxxer:HikariCP](https://github.com/brettwooldridge/HikariCP) from 6.1.0 to 6.2.0.
- [Changelog](https://github.com/brettwooldridge/HikariCP/blob/dev/CHANGES)
- [Commits](https://github.com/brettwooldridge/HikariCP/compare/HikariCP-6.1.0...HikariCP-6.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-21 16:09:56 +00:00
dependabot[bot]
4cde24c536 deps: bump org.projectlombok:lombok from 1.18.34 to 1.18.36 (#420)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.34 to 1.18.36.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.34...v1.18.36)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  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-11-21 16:09:45 +00:00
dependabot[bot]
029617bc45 deps: bump com.gradleup.shadow from 8.3.4 to 8.3.5 (#423)
Bumps [com.gradleup.shadow](https://github.com/GradleUp/shadow) from 8.3.4 to 8.3.5.
- [Release notes](https://github.com/GradleUp/shadow/releases)
- [Commits](https://github.com/GradleUp/shadow/compare/8.3.4...8.3.5)

---
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-11-21 16:09:31 +00:00
William
0627fb20e4 refactor: adjust equals impl in Identifier 2024-11-15 12:11:02 +00:00
William278
bc1f983684 build: actually checkout 1.21.1 on 1.21.1 2024-11-14 17:11:27 +00:00
William278
31eb747c55 build: fix release script 2024-11-14 17:00:17 +00:00
William278
e8facf52ce fix: correct env in release file 2024-11-14 15:42:35 +00:00
William278
5ee4bdd644 fix: fix broken sendPacket mixin on Fabric 2024-11-14 15:33:26 +00:00
William278
92c371e201 fix: disable "other" gui actions.
Fixes an issue where you could double-click stack to collect unstacked items without edit perms.

Unfortunately this breaks pick block on creative without edit permission, but this is  considered a necessary compromise.
2024-11-14 15:06:46 +00:00
William278
d27278454a feat: warn if server name matches default, close #314 2024-11-14 14:58:43 +00:00
William278
16780c149c build: bump deps 2024-11-14 14:54:50 +00:00
William278
0445ba63bc fix: use correct map render logic, close #406 2024-11-14 14:38:11 +00:00
dependabot[bot]
b6aefd6f57 ci: bump mikepenz/action-junit-report from 4 to 5 (#412)
Bumps [mikepenz/action-junit-report](https://github.com/mikepenz/action-junit-report) from 4 to 5.
- [Release notes](https://github.com/mikepenz/action-junit-report/releases)
- [Commits](https://github.com/mikepenz/action-junit-report/compare/v4...v5)

---
updated-dependencies:
- dependency-name: mikepenz/action-junit-report
  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-11-05 23:47:27 +00:00
William
f803af0225 feat: support newer Map data format, close #406 2024-11-04 21:49:24 +00:00
William
2675f4a377 fix: Paper event listener not registering events 2024-11-04 19:44:53 +00:00
William
03341c981f build: bump nbt-api to 2.14.0 2024-11-04 19:29:52 +00:00
William
38cc654167 fix: fixup some compiler warns 2024-11-01 22:44:29 +00:00
William
b347a8d060 fix: API not publishing, close #399 2024-11-01 22:32:03 +00:00
William
8733b86b45 [ci skip]: build 1.21.1 ver 2024-11-01 14:31:56 +00:00
William
eda8e72633 build: target Minecraft 1.21.3 2024-10-31 23:52:09 +00:00
William
c942a015d1 feat: start 1.21.3 2024-10-31 20:57:27 +00:00
William
c00265f1f9 fix: add getPlayerCustomDataStore impl on Fabric 2024-10-27 00:14:20 +01:00
dependabot[bot]
e303984dcf deps: bump org.jetbrains:annotations from 26.0.0 to 26.0.1 (#407)
Bumps [org.jetbrains:annotations](https://github.com/JetBrains/java-annotations) from 26.0.0 to 26.0.1.
- [Release notes](https://github.com/JetBrains/java-annotations/releases)
- [Changelog](https://github.com/JetBrains/java-annotations/blob/master/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/java-annotations/compare/26.0.0...26.0.1)

---
updated-dependencies:
- dependency-name: org.jetbrains:annotations
  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-10-23 11:40:45 +01:00
Preva1l
b449b5dee6 fix: cache save causes, fix Fabric data save on shutdown (#405)
Co-authored-by: seeruk <wright.elliot@gmail.com>
2024-10-17 16:21:42 +01:00
dependabot[bot]
48f8c0c967 deps: bump org.jetbrains:annotations from 25.0.0 to 26.0.0 (#400)
Bumps [org.jetbrains:annotations](https://github.com/JetBrains/java-annotations) from 25.0.0 to 26.0.0.
- [Release notes](https://github.com/JetBrains/java-annotations/releases)
- [Changelog](https://github.com/JetBrains/java-annotations/blob/master/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/java-annotations/compare/25.0.0...26.0.0)

---
updated-dependencies:
- dependency-name: org.jetbrains:annotations
  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-15 16:20:30 +01:00
dependabot[bot]
f88c4c3e2c deps: bump org.ajoberstar.grgit from 5.2.2 to 5.3.0 (#401)
Bumps [org.ajoberstar.grgit](https://github.com/ajoberstar/grgit) from 5.2.2 to 5.3.0.
- [Release notes](https://github.com/ajoberstar/grgit/releases)
- [Commits](https://github.com/ajoberstar/grgit/compare/5.2.2...5.3.0)

---
updated-dependencies:
- dependency-name: org.ajoberstar.grgit
  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-10-15 16:20:23 +01:00
dependabot[bot]
e6273fa9a0 deps: bump org.junit.jupiter:junit-jupiter-params from 5.11.1 to 5.11.2 (#402)
Bumps [org.junit.jupiter:junit-jupiter-params](https://github.com/junit-team/junit5) from 5.11.1 to 5.11.2.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.1...r5.11.2)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-params
  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-10-15 16:20:09 +01:00
dependabot[bot]
1ba5585d0d deps: bump org.junit.jupiter:junit-jupiter-api from 5.11.1 to 5.11.2 (#395)
Bumps [org.junit.jupiter:junit-jupiter-api](https://github.com/junit-team/junit5) from 5.11.1 to 5.11.2.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.1...r5.11.2)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-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-10-09 12:31:29 +01:00
dependabot[bot]
73547371ae deps: bump org.junit.jupiter:junit-jupiter-engine from 5.11.1 to 5.11.2 (#394)
Bumps [org.junit.jupiter:junit-jupiter-engine](https://github.com/junit-team/junit5) from 5.11.1 to 5.11.2.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.1...r5.11.2)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  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-10-09 12:31:21 +01:00
dependabot[bot]
fca6825394 deps: bump org.jetbrains:annotations from 24.1.0 to 25.0.0 (#396)
Bumps [org.jetbrains:annotations](https://github.com/JetBrains/java-annotations) from 24.1.0 to 25.0.0.
- [Release notes](https://github.com/JetBrains/java-annotations/releases)
- [Changelog](https://github.com/JetBrains/java-annotations/blob/master/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/java-annotations/compare/24.1.0...25.0.0)

---
updated-dependencies:
- dependency-name: org.jetbrains:annotations
  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-09 12:31:15 +01:00
dependabot[bot]
53af114f44 deps: bump com.gradleup.shadow from 8.3.2 to 8.3.3 (#397)
Bumps [com.gradleup.shadow](https://github.com/GradleUp/shadow) from 8.3.2 to 8.3.3.
- [Release notes](https://github.com/GradleUp/shadow/releases)
- [Commits](https://github.com/GradleUp/shadow/compare/8.3.2...8.3.3)

---
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-10-09 12:31:07 +01:00
William
311cc85c92 docs: document incompatibility with /restart 2024-10-05 17:09:14 +01:00
William
099a258cf8 docs: update compatibility table 2024-10-03 15:06:22 +01:00
William
480f59a166 build: fix build, getEnv -> getProperty 2024-10-03 15:03:53 +01:00
William
45c2f5350f Merge remote-tracking branch 'origin/master' 2024-10-03 14:58:25 +01:00
William
ed88d77852 build: bump guava, junit and HikariCP 2024-10-03 14:58:12 +01:00
dependabot[bot]
e7fc9f015e deps: bump com.zaxxer:HikariCP from 5.1.0 to 6.0.0 (#388)
Bumps [com.zaxxer:HikariCP](https://github.com/brettwooldridge/HikariCP) from 5.1.0 to 6.0.0.
- [Changelog](https://github.com/brettwooldridge/HikariCP/blob/dev/CHANGES)
- [Commits](https://github.com/brettwooldridge/HikariCP/compare/HikariCP-5.1.0...HikariCP-6.0.0)

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

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

---
updated-dependencies:
- dependency-name: com.gradleup.shadow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-28 14:33:23 +01:00
dependabot[bot]
1bd703641b deps: bump org.bstats:bstats-bukkit from 3.0.3 to 3.1.0 (#379)
Bumps [org.bstats:bstats-bukkit](https://github.com/Bastian/bStats-Metrics) from 3.0.3 to 3.1.0.
- [Release notes](https://github.com/Bastian/bStats-Metrics/releases)
- [Commits](https://github.com/Bastian/bStats-Metrics/compare/v3.0.3...v3.1.0)

---
updated-dependencies:
- dependency-name: org.bstats:bstats-bukkit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-28 14:33:14 +01:00
dependabot[bot]
1b1d4c8e8d deps: bump commons-io:commons-io from 2.16.1 to 2.17.0 (#381)
Bumps commons-io:commons-io from 2.16.1 to 2.17.0.

---
updated-dependencies:
- dependency-name: commons-io:commons-io
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-28 14:33:07 +01:00
Coded
842ec0e28d feat: add create_tables config option to disable automatic DDL operations (#377)
* Add config option for creating tables

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

---
updated-dependencies:
- dependency-name: org.bstats:bstats-bukkit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

---
updated-dependencies:
- dependency-name: net.kyori:adventure-platform-bukkit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-14 13:39:00 +01:00
dependabot[bot]
ff2531539e deps: bump net.kyori:adventure-platform-api from 4.3.3 to 4.3.4 (#361)
Bumps [net.kyori:adventure-platform-api](https://github.com/KyoriPowered/adventure-platform) from 4.3.3 to 4.3.4.
- [Release notes](https://github.com/KyoriPowered/adventure-platform/releases)
- [Commits](https://github.com/KyoriPowered/adventure-platform/compare/v4.3.3...v4.3.4)

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

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

* fix silly mistake with postgresql

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

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

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

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

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

View File

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

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

@@ -0,0 +1,70 @@
name: CI Tests
on:
push:
branches: [ 'minecraft/1.20.1' ]
paths-ignore:
- 'docs/**'
- 'workflows/**'
- 'README.md'
permissions:
contents: read
checks: write
jobs:
build:
name: 'Build - 1.20.1'
runs-on: ubuntu-latest
steps:
- name: 'Setup JDK 21 📦'
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: 'Setup Gradle 8.8 🏗️'
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: '8.8'
- name: 'Checkout for CI 🛎️'
uses: actions/checkout@v4
with:
ref: 'minecraft/1.20.1'
- name: '[Current - 1.20.1] Build 🛎️'
run: |
./gradlew clean build publish
env:
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
- name: 'Publish Test Report 📊'
uses: mikepenz/action-junit-report@v5
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

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

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

68
.github/workflows/ci_master.yml vendored Normal file
View File

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

View File

@@ -14,17 +14,17 @@ jobs:
steps:
- name: 'Checkout for CI 🛎'
uses: actions/checkout@v4
- name: 'Set up JDK 17 📦'
- name: 'Set up JDK 21 📦'
uses: actions/setup-java@v4
with:
java-version: '17'
java-version: '21'
distribution: 'temurin'
- name: 'Build with Gradle 🏗️'
uses: gradle/gradle-build-action@v3
with:
arguments: test
- name: 'Publish Test Report 📊'
uses: mikepenz/action-junit-report@v4
uses: mikepenz/action-junit-report@v5
if: success() || failure() # Continue on failure
with:
report_paths: '**/build/test-results/test/TEST-*.xml'

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
<!--suppress ALL -->
<p align="center">
<img src="images/banner.png" alt="HuskSync" />
<a href="https://github.com/WiIIiam278/HuskSync/actions/workflows/ci.yml">
<img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/ci.yml?branch=master&logo=github"/>
<a href="https://github.com/WiIIiam278/HuskSync/actions/workflows/ci_master.yml">
<img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/ci_master.yml?branch=master&logo=github"/>
</a>
<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" />
@@ -43,8 +43,29 @@
**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 Status |
|:---------------:|:---------------:|:------------:|:--------------|:-----------------------------|
| 1.21.4 | _latest_ | 21 | Paper, Fabric | ✅ **Active Release** |
| 1.21.3 | 3.7.1 | 21 | Paper, Fabric | 🗃️ Archived (December 2024) |
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **November 2025** (LTS) |
| 1.20.6 | 3.6.8 | 17 | Paper | 🗃️ Archived (October 2024) |
| 1.20.4 | 3.6.8 | 17 | Paper | 🗃️ Archived (July 2024) |
| 1.20.1 | _latest_ | 17 | Paper, Fabric | ✅ **November 2025** (LTS) |
| 1.17.1 - 1.19.4 | 3.6.8 | 17 | Paper | 🗃️ Archived |
| 1.16.5 | 3.2.1 | 16 | Paper | 🗃️ Archived |
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
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/MariaDB/Mongo/PostgreSQL database](https://william278.net/docs/husksync/database), a [Redis (v5.0+) server]((https://william278.net/docs/husksync/redis)) and a network of [compatible Spigot or Fabric Minecraft servers](https://william278.net/docs/husksync/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.
2. Start, then stop every server to let HuskSync generate the config file.
@@ -52,7 +73,7 @@ Requires a MySQL/Mongo/PostgreSQL database, a Redis (v5.0+) server and a network
4. Start every server again and synchronization will begin.
## 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
./gradlew clean build
@@ -82,4 +103,4 @@ Translations of the plugin locales are welcome to help make the plugin more acce
- [bStats](https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140) &mdash; View plugin metrics
---
&copy; [William278](https://william278.net/), 2023. Licensed under the Apache-2.0 License.
&copy; [William278](https://william278.net/), 2025. Licensed under the Apache-2.0 License.

View File

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

View File

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

View File

@@ -23,6 +23,7 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import de.tr7zw.changeme.nbtapi.utils.DataFixerUtil;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -78,6 +79,7 @@ import java.util.stream.Collectors;
@Getter
@NoArgsConstructor
@SuppressWarnings("unchecked")
public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier,
BukkitEventDispatcher, BukkitMapPersister {
@@ -127,6 +129,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
loadSettings();
loadLocales();
loadServer();
validateConfigFiles();
});
this.eventListener = createEventListener();
@@ -137,6 +140,9 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
public void onEnable() {
this.audiences = BukkitAudiences.create(this);
// Check compatibility
checkCompatibility();
// Register commands
initialize("commands", (plugin) -> getUniform().register(PluginCommand.Type.create(this)));
@@ -290,7 +296,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@Override
public boolean isDependencyLoaded(@NotNull String name) {
final Plugin plugin = getServer().getPluginManager().getPlugin(name);
return plugin != null && plugin.isEnabled();
return plugin != null;
}
// Register bStats metrics
@@ -327,12 +333,34 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
return Version.fromString(getServer().getBukkitVersion());
}
public int getDataVersion(@NotNull Version mcVersion) {
return switch (mcVersion.toStringWithoutMetadata()) {
case "1.16", "1.16.1", "1.16.2", "1.16.3", "1.16.4", "1.16.5" -> DataFixerUtil.VERSION1_16_5;
case "1.17", "1.17.1" -> DataFixerUtil.VERSION1_17_1;
case "1.18", "1.18.1", "1.18.2" -> DataFixerUtil.VERSION1_18_2;
case "1.19", "1.19.1", "1.19.2" -> DataFixerUtil.VERSION1_19_2;
case "1.20", "1.20.1", "1.20.2" -> DataFixerUtil.VERSION1_20_2;
case "1.20.3", "1.20.4" -> DataFixerUtil.VERSION1_20_4;
case "1.20.5", "1.20.6" -> DataFixerUtil.VERSION1_20_5;
case "1.21", "1.21.1" -> DataFixerUtil.VERSION1_21;
case "1.21.2", "1.21.3" -> DataFixerUtil.VERSION1_21_2;
case "1.21.4" -> 4189/*DataFixerUtil.VERSION1_21_4*/;
default -> DataFixerUtil.getCurrentVersion();
};
}
@NotNull
@Override
public String getPlatformType() {
return PLATFORM_TYPE_ID;
}
@Override
@NotNull
public String getServerVersion() {
return String.format("%s/%s", getServer().getName(), getServer().getVersion());
}
@Override
public Optional<LegacyConverter> getLegacyConverter() {
return Optional.of(legacyConverter);

View File

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

View File

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

View File

@@ -153,20 +153,11 @@ public class BukkitSerializer {
@NotNull
private ReadWriteNBT upgradeItemData(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion)
throws NoSuchFieldException, IllegalAccessException {
return DataFixerUtil.fixUpItemData(tag, getDataVersion(mcVersion), DataFixerUtil.getCurrentVersion());
}
private int getDataVersion(@NotNull Version mcVersion) {
return switch (mcVersion.toStringWithoutMetadata()) {
case "1.16", "1.16.1", "1.16.2", "1.16.3", "1.16.4", "1.16.5" -> DataFixerUtil.VERSION1_16_5;
case "1.17", "1.17.1" -> DataFixerUtil.VERSION1_17_1;
case "1.18", "1.18.1", "1.18.2" -> DataFixerUtil.VERSION1_18_2;
case "1.19", "1.19.1", "1.19.2" -> DataFixerUtil.VERSION1_19_2;
case "1.20", "1.20.1", "1.20.2" -> DataFixerUtil.VERSION1_20_2;
case "1.20.3", "1.20.4" -> DataFixerUtil.VERSION1_20_4;
case "1.20.5", "1.20.6" -> DataFixerUtil.VERSION1_20_5;
default -> DataFixerUtil.getCurrentVersion();
};
return DataFixerUtil.fixUpItemData(
tag,
getPlugin().getDataVersion(mcVersion),
DataFixerUtil.getCurrentVersion()
);
}
@NotNull
@@ -228,7 +219,7 @@ public class BukkitSerializer {
@Override
public BukkitData.PersistentData deserialize(@NotNull String serialized) throws DeserializationException {
return BukkitData.PersistentData.from(new NBTContainer(serialized));
return BukkitData.PersistentData.from((NBTContainer) NBT.parseNBT(serialized));
}
@NotNull

View File

@@ -23,6 +23,7 @@ import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
import com.github.retrooper.packetevents.event.PacketListenerPriority;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.google.common.collect.Sets;
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
@@ -39,12 +40,11 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
}
@Override
@SuppressWarnings("UnstableApiUsage")
public void onLoad() {
super.onLoad();
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(getPlugin()));
PacketEvents.getAPI().getSettings().reEncodeByDefault(false)
.checkForUpdates(false)
.bStats(true);
PacketEvents.getAPI().getSettings().reEncodeByDefault(false).checkForUpdates(false);
PacketEvents.getAPI().load();
}
@@ -60,6 +60,7 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
private static final Set<PacketType.Play.Client> ALLOWED_PACKETS = Set.of(
PacketType.Play.Client.KEEP_ALIVE, PacketType.Play.Client.PONG, PacketType.Play.Client.PLUGIN_MESSAGE, // Connection packets
PacketType.Play.Client.PLAYER_LOADED, PacketType.Play.Client.CLIENT_TICK_END, // Connection packets
PacketType.Play.Client.CHAT_MESSAGE, PacketType.Play.Client.CHAT_COMMAND, PacketType.Play.Client.CHAT_SESSION_UPDATE, // Chat / command packets
PacketType.Play.Client.PLAYER_POSITION, PacketType.Play.Client.PLAYER_POSITION_AND_ROTATION, PacketType.Play.Client.PLAYER_ROTATION, // Movement packets
PacketType.Play.Client.HELD_ITEM_CHANGE, PacketType.Play.Client.ANIMATION, PacketType.Play.Client.TELEPORT_CONFIRM, // Animation packets
@@ -78,7 +79,20 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if(!(event.getPacketType() instanceof PacketType.Play.Client client)) {
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
return;
}
if (!CANCEL_PACKETS.contains(client)) {
return;
}
if (listener.cancelPlayerEvent(event.getUser().getUUID())) {
event.setCancelled(true);
}
}
@Override
public void onPacketSend(PacketSendEvent event) {
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
return;
}
if (!CANCEL_PACKETS.contains(client)) {

View File

@@ -49,7 +49,7 @@ public class BukkitProtocolLibLockedPacketListener extends BukkitLockedEventList
private static class PlayerPacketAdapter extends PacketAdapter {
// Packets we want the player to still be able to send/receiver to/from the server
// Packets we want the player to still be able to send/receiver to/from the server - //todo update 1.21.4
private static final Set<PacketType> ALLOWED_PACKETS = Set.of(
Client.KEEP_ALIVE, Client.PONG, Client.CUSTOM_PAYLOAD, // Connection packets
Client.CHAT_COMMAND, Client.CLIENT_COMMAND, Client.CHAT, Client.CHAT_SESSION_UPDATE, // Chat / command packets

View File

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

View File

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

View File

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

View File

@@ -51,7 +51,7 @@ public final class BukkitKeyedAdapter {
@Nullable
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) {

View File

@@ -31,7 +31,7 @@ import net.william278.mapdataapi.MapData;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.ShulkerBox;
import org.bukkit.block.Container;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BlockStateMeta;
@@ -96,7 +96,7 @@ public interface BukkitMapPersister {
}
if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) {
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);
b.setBlockState(box);
}
@@ -122,7 +122,8 @@ public interface BukkitMapPersister {
}
// Render the map
final PersistentMapCanvas canvas = new PersistentMapCanvas(view);
final int dataVersion = getPlugin().getDataVersion(getPlugin().getMinecraftVersion());
final PersistentMapCanvas canvas = new PersistentMapCanvas(view, dataVersion);
for (MapRenderer renderer : view.getRenderers()) {
renderer.render(view, canvas, delegateRenderer);
getPlugin().debug(String.format("Rendered locked map canvas to view (#%s)", view.getId()));
@@ -140,6 +141,7 @@ public interface BukkitMapPersister {
@NotNull
private ItemStack applyMapView(@NotNull ItemStack map) {
final int dataVersion = getPlugin().getDataVersion(getPlugin().getMinecraftVersion());
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
NBT.get(map, nbt -> {
if (!nbt.hasTag(MAP_DATA_KEY)) {
@@ -155,8 +157,8 @@ public interface BukkitMapPersister {
Optional<String> world = Optional.empty();
for (String worldUid : mapIds.getKeys()) {
world = getPlugin().getServer().getWorlds().stream()
.map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid))
.findFirst();
.map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid))
.findFirst();
if (world.isPresent()) {
break;
}
@@ -178,8 +180,9 @@ public interface BukkitMapPersister {
final MapData canvasData;
try {
getPlugin().debug("Deserializing map data from NBT and generating view...");
canvasData = MapData.fromByteArray(Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY),
"Map pixel data is null"));
canvasData = MapData.fromByteArray(
dataVersion,
Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY), "Pixel data null!"));
} catch (Throwable e) {
getPlugin().log(Level.WARNING, "Failed to deserialize map data from NBT", e);
return;
@@ -195,8 +198,8 @@ public interface BukkitMapPersister {
// Set the map view ID in NBT
NBT.modify(map, editable -> {
Objects.requireNonNull(editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY),
"Map view ID mappings compound is null")
.setInteger(worldUid, view.getId());
"Map view ID mappings compound is null")
.setInteger(worldUid, view.getId());
});
getPlugin().debug(String.format("Generated view (#%s) and updated map (UID: %s)", view.getId(), worldUid));
});
@@ -276,7 +279,7 @@ public interface BukkitMapPersister {
@NotNull
private static World getDefaultMapWorld() {
final World world = Bukkit.getWorlds().get(0);
final World world = Bukkit.getWorlds().getFirst();
if (world == null) {
throw new IllegalStateException("No worlds are loaded on the server!");
}
@@ -294,6 +297,7 @@ public interface BukkitMapPersister {
/**
* A {@link MapRenderer} that can be used to render persistently serialized {@link MapData} to a {@link MapView}
*/
@SuppressWarnings("deprecation")
class PersistentMapRenderer extends MapRenderer {
private final MapData canvasData;
@@ -326,42 +330,45 @@ public interface BukkitMapPersister {
@NotNull
private static MapCursor createBannerCursor(@NotNull MapBanner banner) {
return new MapCursor(
(byte) banner.getPosition().getX(),
(byte) banner.getPosition().getZ(),
(byte) 8, // Always rotate banners upright
switch (banner.getColor().toLowerCase(Locale.ENGLISH)) {
case "white" -> MapCursor.Type.BANNER_WHITE;
case "orange" -> MapCursor.Type.BANNER_ORANGE;
case "magenta" -> MapCursor.Type.BANNER_MAGENTA;
case "light_blue" -> MapCursor.Type.BANNER_LIGHT_BLUE;
case "yellow" -> MapCursor.Type.BANNER_YELLOW;
case "lime" -> MapCursor.Type.BANNER_LIME;
case "pink" -> MapCursor.Type.BANNER_PINK;
case "gray" -> MapCursor.Type.BANNER_GRAY;
case "light_gray" -> MapCursor.Type.BANNER_LIGHT_GRAY;
case "cyan" -> MapCursor.Type.BANNER_CYAN;
case "purple" -> MapCursor.Type.BANNER_PURPLE;
case "blue" -> MapCursor.Type.BANNER_BLUE;
case "brown" -> MapCursor.Type.BANNER_BROWN;
case "green" -> MapCursor.Type.BANNER_GREEN;
case "red" -> MapCursor.Type.BANNER_RED;
default -> MapCursor.Type.BANNER_BLACK;
},
true,
banner.getText().isEmpty() ? null : banner.getText()
(byte) banner.getPosition().getX(),
(byte) banner.getPosition().getZ(),
(byte) 8, // Always rotate banners upright
switch (banner.getColor().toLowerCase(Locale.ENGLISH)) {
case "white" -> MapCursor.Type.BANNER_WHITE;
case "orange" -> MapCursor.Type.BANNER_ORANGE;
case "magenta" -> MapCursor.Type.BANNER_MAGENTA;
case "light_blue" -> MapCursor.Type.BANNER_LIGHT_BLUE;
case "yellow" -> MapCursor.Type.BANNER_YELLOW;
case "lime" -> MapCursor.Type.BANNER_LIME;
case "pink" -> MapCursor.Type.BANNER_PINK;
case "gray" -> MapCursor.Type.BANNER_GRAY;
case "light_gray" -> MapCursor.Type.BANNER_LIGHT_GRAY;
case "cyan" -> MapCursor.Type.BANNER_CYAN;
case "purple" -> MapCursor.Type.BANNER_PURPLE;
case "blue" -> MapCursor.Type.BANNER_BLUE;
case "brown" -> MapCursor.Type.BANNER_BROWN;
case "green" -> MapCursor.Type.BANNER_GREEN;
case "red" -> MapCursor.Type.BANNER_RED;
default -> MapCursor.Type.BANNER_BLACK;
},
true,
banner.getText().isEmpty() ? null : banner.getText()
);
}
/**
* A {@link MapCanvas} implementation used for pre-rendering maps to be converted into {@link MapData}
*/
@SuppressWarnings("deprecation")
class PersistentMapCanvas implements MapCanvas {
private final int mapDataVersion;
private final MapView mapView;
private final int[][] pixels = new int[128][128];
private MapCursorCollection cursors;
private PersistentMapCanvas(@NotNull MapView mapView) {
private PersistentMapCanvas(@NotNull MapView mapView, int mapDataVersion) {
this.mapDataVersion = mapDataVersion;
this.mapView = mapView;
}
@@ -383,18 +390,38 @@ public interface BukkitMapPersister {
}
@Override
@Deprecated
public void setPixel(int x, int y, byte color) {
pixels[x][y] = color;
}
@Override
@Deprecated
public byte getPixel(int x, int y) {
return (byte) pixels[x][y];
}
@Override
@Deprecated
public byte getBasePixel(int x, int y) {
return getPixel(x, y);
return (byte) pixels[x][y];
}
@Override
public void setPixelColor(int x, int y, @Nullable Color color) {
pixels[x][y] = color == null ? -1 : MapPalette.matchColor(color);
}
@Nullable
@Override
public Color getPixelColor(int x, int y) {
return MapPalette.getColor((byte) pixels[x][y]);
}
@NotNull
@Override
public Color getBasePixelColor(int x, int y) {
return MapPalette.getColor((byte) pixels[x][y]);
}
@Override
@@ -427,18 +454,19 @@ public interface BukkitMapPersister {
final String BANNER_PREFIX = "banner_";
for (int i = 0; i < getCursors().size(); i++) {
final MapCursor cursor = getCursors().getCursor(i);
final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH);
final String type = cursor.getType().getKey().getKey();
if (type.startsWith(BANNER_PREFIX)) {
banners.add(new MapBanner(
type.replaceAll(BANNER_PREFIX, ""),
cursor.getCaption() == null ? "" : cursor.getCaption(),
cursor.getX(),
mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128,
cursor.getY()
type.replaceAll(BANNER_PREFIX, ""),
cursor.getCaption() == null ? "" : cursor.getCaption(),
cursor.getX(),
mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128,
cursor.getY()
));
}
}
return MapData.fromPixels(pixels, getDimension(), (byte) 2, banners, List.of());
return MapData.fromPixels(mapDataVersion, pixels, getDimension(), (byte) 2, banners, List.of());
}
}

View File

@@ -3,26 +3,26 @@ plugins {
}
dependencies {
api 'commons-io:commons-io:2.16.1'
api 'org.apache.commons:commons-text:1.12.0'
api 'commons-io:commons-io:2.18.0'
api 'org.apache.commons:commons-text:1.13.0'
api 'net.william278:minedown:1.8.2'
api 'org.json:json:20240303'
api 'org.json:json:20250107'
api 'com.google.code.gson:gson:2.11.0'
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
api 'de.exlll:configlib-yaml:4.5.0'
api 'net.william278:paginedown:1.1.2'
api 'net.william278:DesertWell:2.0.4'
api('com.zaxxer:HikariCP:5.1.0') {
api('com.zaxxer:HikariCP:6.2.1') {
exclude module: 'slf4j-api'
}
compileOnly 'net.william278.uniform:uniform-common:1.1.4'
compileOnly 'net.william278.uniform:uniform-common:1.3'
compileOnly 'com.mojang:brigadier:1.1.8'
compileOnly 'org.projectlombok:lombok:1.18.32'
compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'net.kyori:adventure-api:4.17.0'
compileOnly 'net.kyori:adventure-platform-api:4.3.3'
compileOnly 'com.google.guava:guava:33.2.1-jre'
compileOnly 'org.projectlombok:lombok:1.18.36'
compileOnly 'org.jetbrains:annotations:26.0.1'
compileOnly 'net.kyori:adventure-api:4.18.0'
compileOnly 'net.kyori:adventure-platform-api:4.3.4'
compileOnly 'com.google.guava:guava:33.4.0-jre'
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
compileOnly "redis.clients:jedis:$jedis_version"
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
@@ -33,10 +33,10 @@ dependencies {
testImplementation "redis.clients:jedis:$jedis_version"
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
testImplementation 'com.google.guava:guava:33.2.1-jre'
testImplementation 'com.google.guava:guava:33.4.0-jre'
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
testCompileOnly 'de.exlll:configlib-yaml:4.5.0'
testCompileOnly 'org.jetbrains:annotations:24.1.0'
testCompileOnly 'org.jetbrains:annotations:26.0.1'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.36'
}

View File

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

View File

@@ -99,6 +99,18 @@ public class HuskSyncAPI {
return plugin.supplyAsync(() -> plugin.getDatabase().getUser(uuid));
}
/**
* Get an {@link OnlineUser} by their UUID
*
* @param uuid the UUID of the user to get
* @return The {@link OnlineUser} wrapped in an optional, if they are online on <i>this</i> server.
* @since 3.7.2
*/
@NotNull
public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
return plugin.getOnlineUser(uuid);
}
/**
* Get a {@link User} by their username
*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -131,6 +131,15 @@ public interface ConfigProvider {
));
}
default void validateConfigFiles() {
// Validate server name is default
if (getServerName().equals("server")) {
getPlugin().log(Level.WARNING, "The server name set in ~/plugins/HuskSync/server.yml appears to" +
"be unchanged from the default (currently set to: \"server\"). Please check that this value has" +
"been updated to match the case-sensitive ID of this server in your proxy config file!");
}
}
/**
* Get a plugin resource
*

View File

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

View File

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

View File

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

View File

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

View File

@@ -395,7 +395,7 @@ public class DataSnapshot {
.map(entry -> Map.entry(plugin.getIdentifier(entry.getKey()).orElseThrow(), entry.getValue()))
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> plugin.deserializeData(entry.getKey(), entry.getValue()),
entry -> plugin.deserializeData(entry.getKey(), entry.getValue(), getMinecraftVersion()),
(a, b) -> b, () -> Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR)
));
}
@@ -535,9 +535,9 @@ public class DataSnapshot {
public Builder timestamp(@NotNull OffsetDateTime timestamp) {
if (timestamp.isAfter(OffsetDateTime.now())) {
throw new IllegalArgumentException("Data snapshots cannot have a timestamp set in the future! "
+ "Make sure your database server time matches the server time.\n"
+ "Current game server timestamp: " + OffsetDateTime.now() + " / "
+ "Snapshot timestamp: " + timestamp);
+ "Make sure your database server time matches the server time.\n"
+ "Current game server timestamp: " + OffsetDateTime.now() + " / "
+ "Snapshot timestamp: " + timestamp);
}
this.timestamp = timestamp;
return this;
@@ -913,6 +913,8 @@ public class DataSnapshot {
private final boolean fireDataSaveEvent;
private static Map<String, SaveCause> registry;
/**
* Get or create a {@link SaveCause} from a name
*
@@ -921,7 +923,7 @@ public class DataSnapshot {
*/
@NotNull
public static SaveCause of(@NotNull String name) {
return new SaveCause(name.length() > 32 ? name.substring(0, 31) : name, true);
return of(name,true);
}
/**
@@ -933,7 +935,14 @@ public class DataSnapshot {
*/
@NotNull
public static SaveCause of(@NotNull String name, boolean firesSaveEvent) {
return new SaveCause(name.length() > 32 ? name.substring(0, 31) : name, firesSaveEvent);
name = name.length() > 32 ? name.substring(0, 31) : name;
if (registry == null) registry = new HashMap<>();
if (registry.containsKey(name)) return registry.get(name);
SaveCause cause = new SaveCause(name, firesSaveEvent);
registry.put(cause.name(), cause);
return cause;
}
@NotNull
@@ -944,11 +953,10 @@ public class DataSnapshot {
}
@NotNull
@ApiStatus.Obsolete
public static SaveCause[] values() {
return new SaveCause[]{
DISCONNECT, WORLD_SAVE, DEATH, SERVER_SHUTDOWN, INVENTORY_COMMAND, ENDERCHEST_COMMAND,
BACKUP_RESTORE, API, MPDB_MIGRATION, LEGACY_MIGRATION, CONVERTED_FROM_V2
};
if (registry == null) registry = new HashMap<>();
return registry.values().toArray(new SaveCause[0]);
}
}

View File

@@ -25,6 +25,7 @@ import net.kyori.adventure.key.Key;
import org.intellij.lang.annotations.Subst;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.Comparator;
@@ -50,7 +51,8 @@ public class Identifier {
Dependency.optional("game_mode")
);
public static final Identifier ATTRIBUTES = huskSync("attributes", true,
Dependency.required("potion_effects")
Dependency.optional("inventory"),
Dependency.optional("potion_effects")
);
public static final Identifier HEALTH = huskSync("health", true,
Dependency.optional("attributes")
@@ -228,11 +230,8 @@ public class Identifier {
* @return {@code true} if the given object is an identifier with the same key as this identifier
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof Identifier other) {
return key.equals(other.key);
}
return false;
public boolean equals(@Nullable Object obj) {
return obj instanceof Identifier other ? toString().equals(other.toString()) : super.equals(obj);
}
// Get the config entry for the identifier

View File

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

View File

@@ -107,6 +107,14 @@ public abstract class Database {
@Blocking
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.

View File

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

View File

@@ -115,6 +115,9 @@ public class MySqlDatabase extends Database {
);
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
try (Connection connection = dataSource.getConnection()) {
final String[] databaseSchema = getSchemaStatements(String.format("database/%s_schema.sql", flavor));
@@ -124,11 +127,11 @@ public class MySqlDatabase extends Database {
}
} catch (SQLException e) {
throw new IllegalStateException("Failed to create database tables. Please ensure you are running MySQL v8.0+ " +
"and that your connecting user account has privileges to create tables.", e);
"and that your connecting user account has privileges to create tables.", e);
}
} catch (SQLException | IOException e) {
throw new IllegalStateException("Failed to establish a connection to the MySQL database. " +
"Please check the supplied database credentials in the config file", e);
"Please check the supplied database credentials in the config file", e);
}
}
@@ -218,6 +221,27 @@ public class MySqlDatabase extends Database {
return Optional.empty();
}
@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
@Override
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
> **Warning:** API v2 is no longer supported or compatible with HuskSync v3.0. See [[Data Snapshot API]] for the equivalent v3 API. 🚨
HuskSync v2.0 provides an API for fetching and retrieving `UserData`; a snapshot of a user's synchronization.
> **Warning:** API v2 is no longer supported or compatible with HuskSync v3.0. See [[Data Snapshot API]] for the equivalent v3 API. 🚨
This page assumes you've read the general [[API]] introduction and imported HuskSync (v2.x) into your project, and added it as a dependency.
🚨 HuskSync API v2 only targets HuskSync v2.0-2.2.8. It is **not compatible with HuskSync v3.0+**. The equivalent API for HuskSync v3 is the [[Data Snapshot API]].

View File

@@ -20,7 +20,6 @@ The HuskSync API is available for the following platforms:
* `fabric` - Fabric API for Minecraft. Provides Fabric API event listeners and adapters to `net.minecraft` objects.
* `common` - Common API for all platforms.
<details>
<summary>Targeting older versions</summary>
@@ -53,12 +52,12 @@ Add the repository to your `pom.xml` as per below. You can alternatively specify
</repository>
</repositories>
```
Add the dependency to your `pom.xml` as per below. Replace `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
<dependency>
<groupId>net.william278.husksync</groupId>
<artifactId>husksync-PLATFORM</artifactId>
<version>VERSION</version>
<version>HUSKSYNC_VERSION+MINECRAFT_VERSION</version>
<scope>provided</scope>
</dependency>
```
@@ -76,11 +75,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
dependencies {
compileOnly 'net.william278.husksync:husksync-PLATFORM:VERSION'
compileOnly 'net.william278.husksync:husksync-PLATFORM:HUSKSYNC_VERSION+MINECRAFT_VERSION'
}
```
</details>

35
docs/Compatibility.md Normal file
View File

@@ -0,0 +1,35 @@
HuskSync supports the following versions of Minecraft. Since v3.7, you must download the correct version of HuskSync for your server:
| Minecraft | Latest HuskSync | Java Version | Platforms | Support Status |
|:---------------:|:---------------:|:------------:|:--------------|:-----------------------------|
| 1.21.4 | _latest_ | 21 | Paper, Fabric | ✅ **Active Release** |
| 1.21.3 | 3.7.1 | 21 | Paper, Fabric | 🗃️ Archived (December 2024) |
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **November 2025** (LTS) |
| 1.20.6 | 3.6.8 | 17 | Paper | 🗃️ Archived (October 2024) |
| 1.20.4 | 3.6.8 | 17 | Paper | 🗃️ Archived (July 2024) |
| 1.20.1 | _latest_ | 17 | Paper, Fabric | ✅ **November 2025** (LTS) |
| 1.17.1 - 1.19.4 | 3.6.8 | 17 | Paper | 🗃️ Archived |
| 1.16.5 | 3.2.1 | 16 | Paper | 🗃️ Archived |
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 versions
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.
## Incompatible plugins / mods
Please note the following plugins / mods can cause issues with HuskSync:
* Restart plugins / mods are not supported. These will cause [player data to not save correctly when your server restarts](troubleshooting#issues-with-player-data-going-out-of-sync-during-a-server-restart) due to the way these plugins utilise bash scripts. It's important to understand that restart plugins don't actually restart yur server, they just trigger some (often unstable) process-killing scripting logic to occur!
* Combat logging plugins / mods are not supported. Some have built-in support for HuskSync and should work as expected, but for others you may wish to modify the [[Event Priorities]]

View File

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

View File

@@ -43,6 +43,7 @@ huskSyncAPI.getUser(uuid).thenAccept(optionalUser -> {
</details>
* If you have an online `org.bukkit.Player` object, you can use `BukkitPlayer#adapt(player)` to get an `OnlineUser` (extends `User`), representing a logged-in user.
* You can also use `#getOnlineUser(UUID)` to get an OnlineUser by their UUID - note this only works for players online on the server the logic is called from, however.
<details>
<summary>Code Example &mdash; Getting an online user</summary>

73
docs/Database.md Normal file
View File

@@ -0,0 +1,73 @@
HuskSync persists player data and snapshots in a database of your choice. This is separate from a [[Redis]] server, which HuskSync uses for caching and inter-server messaging, which is also required to use HuskSync.
## Database types
> **Warning:** There is no automatic way of migrating between _database_ types. Changing the database type will cause data to be lost.
| Type | Database Software |
|:--------------------------|:--------------------------|
| `MYSQL` | MySQL 8.0 or newer |
| `MARIADB` | MariaDB 5.0 or newer |
| `POSTGRES` | PostgreSQL |
| [`MONGO`](#mongodb-setup) | MongoDB |
## Configuring
To change the database type, navigate to your [`config.yml`](Config-File) file and modify the properties under `database`.
<details>
<summary>Database options (config.yml)</summary>
```yaml
# Database settings
database:
# Type of database to use (MYSQL, MARIADB, POSTGRES, MONGO)
type: MYSQL
# Specify credentials here for your MYSQL, MARIADB, POSTGRES OR MONGO database
credentials:
host: localhost
port: 3306
database: minecraft
username: root
password: ''
# Only change this if you're using MARIADB or POSTGRES
parameters: ?autoReconnect=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8
# MYSQL, MARIADB, POSTGRES database Hikari connection pool properties. Don't modify this unless you know what you're doing!
connection_pool:
maximum_pool_size: 10
minimum_idle: 10
maximum_lifetime: 1800000
keepalive_time: 0
connection_timeout: 5000
# Advanced MongoDB settings. Don't modify unless you know what you're doing!
mongo_settings:
using_atlas: false
parameters: ?retryWrites=true&w=majority&authSource=HuskSync
# Names of tables to use on your database. Don't modify this unless you know what you're doing!
table_names:
users: husksync_users
user_data: husksync_user_data
```
</details>
### Credentials
You will need to specify the credentials (hostname, port, username, password and the database). These credentials are used to connect to your database server.
If your database server account doesn't have a password (not recommended), leave the password field blank (`password: ''`') and the plugin will attempt to connect without a password.
### Connection Pool properties
If you're using MySQL, MariaDB, or PostgreSQL as your database type, you can modify the HikariCP connection pool properties if you know what you're doing.
Please note that modifying these values can cause issues if you don't know what you're doing. The default values should be fine for most users.
## MongoDB Setup
If you're using a MongoDB database, in addition to setting the database type to `MONGO`, you'll need to perform slightly different configuration steps.
- Under `credentials` in the `database` section, enter the credentials of your MongoDB Database. You shouldn't touch the `connection_pool` properties.
- Under `parameters` in the `mongo_settings` section, ensure the specified `&authSource=` matches the database you are using (default is `HuskSync`).
### MongoDB Atlas setup
If you're using a MongoDB Atlas database, you'll also need to set the Atlas settings and adjust the connection parameters string.
- Set `using_atlas` in the `mongo_settings` section to `true`.
- Remove `&authSource=HuskSync` from `parameters` in the `mongo_settings`.
Note that the `port` setting in `credentials` is ignored when using Atlas.

View File

@@ -1,9 +1,9 @@
This page addresses a number of frequently asked questions about the plugin.
This page addresses a number of frequently asked questions about HuskSync.
## Frequently Asked Questions
<details>
<summary>&nbsp;<b>What data can be synchronized?</b></summary>
<summary>&nbsp;<b>What data can be synced?</b></summary>
HuskSync supports synchronising a wide range of different data elements, each of which can be toggled to your liking. Please check out the [[Sync Features]] page for a full list.
@@ -30,16 +30,56 @@ Please note we cannot guarantee compatibility with everything &mdash; test thoro
</details>
<details>
<summary>&nbsp;<b>Is Redis required? What is Redis?</b></summary>
<summary>&nbsp;<b>What versions of Minecraft does HuskSync support?</b></summary>
Yes! HuskSync requires Redis to operate (for reasons demonstrated below).
Check the [[Compatibility]] table. In addition to the latest release of Minecraft, the latest version of HuskSync will support specific older versions based on popularity and mod support.
Redis is an in-memory database server used for caching data at scale and sending messages across a network. You have a Redis server in a similar fashion to the way you have a MySQL database server. If you're using a Minecraft hosting company, you'll want to contact their support and ask if they offer Redis. If you're looking for a host, I have a list of some popular hosts and whether they support Redis [available to read here.](https://william278.net/redis-hosts)
If your server's version of Minecraft isn't supported by the latest release, there's plenty of older, stable versions of HuskSync you can download, though note support for these versions will be limited.
</details>
<details>
<summary>&nbsp;<b>How does the plugin synchronize data?</b></summary>
<summary>&nbsp;<b>What do I need to run HuskSync?</b></summary>
See the [Requirements](setup#requirements) section under Setup.
You need a [[Database]] server, a [[Redis]] server, and [compatible Minecraft servers](compatibility).
</details>
<details>
<summary>&nbsp;<b>Is Redis required? What is Redis?</b></summary>
Yes, HuskSync requires a [[Redis]] server **in addition to a [[Database]] server** to operate.
Redis is an in-memory database server used for caching data at scale and sending messages across a network. You have a Redis server in a similar fashion to the way you have a MySQL database server. If you're using a Minecraft hosting company, you'll want to contact their support and ask if they offer Redis. If you're looking for a host, I have a list of some popular hosts and whether they support Redis [available to view here.](https://william278.net/docs/website/redis-hosts)
For more information, check our [Redis setup instructions](redis).
</details>
<details>
<summary>&nbsp;<b>How much RAM does my Redis server need?</b></summary>
We recommend your Redis server has 1GB of RAM, and that your Redis server is installed locally (on the same server as your game servers, or at least on the server running your Velocity/BungeeCord/Waterfall proxy).
</details>
<details>
<summary>&nbsp;<b>Is a Database required? What Databases are supported?</b></summary>
Yes. HuskSync requires both a [[Database]] server and a [[Redis]] server to operate.
HuskSync supports the following database types:
* MySQL v8.0+
* MariaDB v5.0+
* PostgreSQL
* MongoDB
</details>
<details>
<summary>&nbsp;<b>How does data syncing work?</b></summary>
HuskSync makes use of both MySQL and Redis for optimal data synchronization. You have the option of using one of two [[Sync Modes]], which synchronize data between servers (`DELAY` or `LOCKSTEP`)
@@ -71,9 +111,10 @@ Indeed, there exist economy plugins &mdash; such as [XConomy](https://github.com
</details>
<details>
<summary>&nbsp;<b>Is this better than MySQLPlayerDataBridge?</b></summary>
<summary>&nbsp;<b>Is HuskSync better than MySQLPlayerDataBridge?</b></summary>
I can't provide a fair answer to this question! What I can say is that your mileage will of course vary.
The performance improvements offered by HuskSync's synchronization method will depend on your network environment and the economies of scale that come with your player count. In terms of featureset, HuskSync does feature greater rollback and snapshot backup/management features if this is something you are looking for.
</details>

View File

@@ -1,24 +1,31 @@
# [![HuskSync banner](https://raw.githubusercontent.com/WiIIiam278/HuskSync/master/images/banner.png)](https://github.com/WiIIiam278/HuskSync)
Welcome! This is the plugin documentation for HuskSync v3.x+. Please click through to the topic you'd like to read about.
## Guides
## Setup
* 📚 [[Setup]]
* 💾 [[Database]]
* ✨ [[Redis]]
* ⚠️ [[Compatibility]]
* 📄 [[Config File]]
* 🔗 [[Troubleshooting]]
* ↪️ [[Data Rotation]]
* ↗️ [[Legacy Migration]]
* ✨ [[MPDB Migration]]
* 🎏 [[Translations]]
* ❓ [[FAQs]]
## Documentation
## Features
* 🖥️ [[Commands]]
* ✅ [[Sync Features]]
* ⚙️ [[Sync Modes]]
* 🟩 [[Plan Hook]]
* ↪️ [[Data Rotation]]
* ❓ [[FAQs]]
## Guides
* ↗️ [[Legacy Migration]]
* ✨ [[MPDB Migration]]
* ☂️ [[Dumping UserData]]
* 🟩 [[Plan Hook]]
* 📋 [[Event Priorities]]
* ⚔️ [[Keep Inventory]]
* 🎏 [[Translations]]
## Developers
* 📦 [[API]] v3
* 📝 [[Data Snapshot API]]
* 📝 [[Custom Data API]]

View File

@@ -1,4 +1,4 @@
If your server uses the `keepInventory` gamerule, where players keep the contents of their inventory after dying, HuskSync's built-in snapshot-on-death and dead-player synchronization features can saveCause a conflict leading to synchronization issues.
If your server uses the [`keepInventory` game rule](https://minecraft.wiki/w/Keep_inventory), where players keep the contents of their inventory after dying, HuskSync's built-in snapshot-on-death and dead-player synchronization features can saveCause a conflict leading to synchronization issues.
To solve this issue, you will need to adjust three settings in your `config.yml` file, as described below.

View File

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

View File

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

80
docs/Redis.md Normal file
View File

@@ -0,0 +1,80 @@
Redis is a piece of server used for data caching and cross-server messaging. A Redis server running Redis v5.0+ is **required** in addition to a compatible [[Database]] to use HuskSync. There are a number of ways of [installing or getting a Redis server](#getting-a-redis-server).
For the best results, we recommend a Redis server with 1GB of RAM, hosted locally (on the same machine as all your other servers). If your setup has multiple machines, install Redis on the machine with your Velocity/BungeeCord/Waterfall proxy server and ensure lockstep syncing mode is in use.
## What is Redis?
[Redis](http://redis.io/) (**RE**mote **DI**ctionary **S**erver) is an open-source, in-memory data store server that can be used as a cache, message broker, streaming engine, or database.
HuskSync requires Redis and uses it for caching player data when they change server, and for pub/sub messaging to facilitate cross-server admin actions (such as the [`/invsee` command](Commands) to update a player's data on other servers). Check the [[FAQs]] for more details.
## Configuring
To configure Redis, navigate to your [`config.yml`](Config-File) file and modify the properties under `redis`.
<details>
<summary>Database options (config.yml)</summary>
```yaml
# Redis settings
redis:
# Specify the credentials of your Redis server here. Set "password" to '' if you don't have one
credentials:
host: localhost
port: 6379
password: ''
use_ssl: false
# Options for if you're using Redis sentinel. Don't modify this unless you know what you're doing!
sentinel:
# The master set name for the Redis sentinel.
master: ''
# List of host:port pairs
nodes: []
password: ''
```
</details>
### Credentials
Enter the hostname, port, and default user password of your Redis server.
If your Redis default user doesn't have a password, leave the password field blank (`password: ''`') and the plugin will attempt to connect without a password.
### Default user password
Depending on the version of Redis you've installed, Redis may or may not set a random default user password. Please check this in your Redis server config. You can clear the password of the default user with the below command in `redis-cli`.
```bash
requirepass thepassword
user default on nopass ~* &* +@all
```
### Using Redis Sentinel
If you're using [Redis Sentinel](https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/), set this up by filling out the properties under the `sentinel` subsection.
You'll need to supply your master set name, your sentinel password, and a list of hosts/ports in the format `host:port`.
## Getting a Redis Server
HuskSync requires a Redis server. Instructions for getting Redis on different servers are detailed below. HuskSync is tested for the official Redis package, but should also work with Redis forks or other compatible software.
For the best results, we recommend a Redis server with 1GB of RAM, hosted locally (on the same machine as all your other servers). If your setup has multiple machines, install Redis on the machine with your Velocity/BungeeCord/Waterfall proxy server and ensure lockstep syncing mode is in use.
### If you're using a Minecraft server hosting provider
Please contact your host's customer support and request Redis. You can direct them to this page if you wish. Looking for a Minecraft Server host that supports Redis? We maintain a list of [server hosts which offer Redis](https://william278.net/docs/website/redis-hosts).
If your host doesn't offer Redis, you should consider whether HuskSync is the right plugin for you. If you still want to use HuskSync, you could choose to rent a cheap Redis server externally from a provider such as DigitalOcean, though note we don't recommend this as it increases the latency between your game servers and cache, which will impact syncing performance.
### Redis on Linux or macOS
You can [install Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/) on your distribution of Linux. Redis is widely available on most package manager repositories.
You can also [install Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-mac-os/) on your macOS server.
### Redis on Windows
Redis isn't officially supported on Windows, but there's a number of [unofficial ports](https://github.com/tporadowski/redis/releases) you can install which work great and run Redis as a Windows service.
You can also [install Redis via WSL](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-windows/) if you prefer.
### Pterodactyl / Pelican panel hosts
If you're self-hosting your server on a Pterodactyl or Pelican panel, you will already have Redis installed and can use this server for HuskSync, too.
If you are hosting your Redis server on the same node as your servers, you need to use `172.18.0.1` as your host (or equivalent if you changed your network settings), and bind it in the Redis config `nano /etc/redis/redis.conf`.
You will also need to uncomment the `requirepass` directive and set a password to allow outside connections, or disable `protected-mode`. Once a password is set and Redis is restarted `systemctl restart redis`, you will also need to update the password in your pterodactyl `.env` (`nano /var/www/pterodactyl/.env`) and refresh the cache `cd /var/www/pterodactyl && php artisan config:clear`.
You may also need to allow connections from your firewall depending on your Linux distribution.

View File

@@ -1,19 +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.
This will walk you through installing HuskSync on your network of Spigot or Fabric servers.
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.
## Requirements
> **Warning:** Mixing and matching Fabric/Spigot servers is not supported, and all servers must be running the same Minecraft version.
HuskSync requires a Database server, a Redis server, and any number of compatible Minecraft servers:
> **Note:** Please also note some specific legacy Paper/Purpur versions are [not compatible](Unsupported-Versions) with HuskSync.
* A MySQL Database (v8.0+)
* **OR** a MariaDB, PostrgreSQL or MongoDB database, which are also supported
* 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+)
* **OR** a network of Fabric servers, connected by a Fabric proxy (Minecraft v1.20.1, running Java 17+)
* Any number of [compatible Fabric or Spigot/Paper-based](Compatibility) servers
* Each server must be running the same exact version of Minecraft
* It is not possible to sync data between a mixture of Fabric and Spigot servers
* HuskSync should not be installed on your Velocity, BungeeCord, or Waterfall proxy
* A [[Database]] server running MySQL v8.0+, MariaDB v5.0+, PostgreSQL or MongoDB
* A [[Redis]] server running Redis v5.0+
## Setup Instructions
Before you begin, switch off all servers on your network. It is recommended that you also take a backup.
### 1. Install the jar
- Place the plugin jar file in the `/plugins/` or `/mods/` directory of each Spigot/Fabric server respectively.
- You do not need to install HuskSync as a proxy plugin.
@@ -24,10 +23,10 @@ This will walk you through installing HuskSync on your network of Spigot or Fabr
- Start, then stop every server to let HuskSync generate the [[config file]].
- HuskSync will throw an error in the console and disable itself as it is unable to connect to the database. You haven't set the credentials yet, so this is expected.
### 3. Enter Mysql & Redis database credentials
### 3. Enter Database & Redis server credentials
- Navigate to the new config file on each server (`~/plugins/HuskSync/config.yml` on Spigot, `~/config/husksync/config.yml` on Fabric)
- Under `credentials` in the `database` section, enter the credentials of your (MySQL/MariaDB/MongoDB/PostgreSQL) Database. You shouldn't touch the `connection_pool` properties.
- Under `credentials` in the `redis` section, enter the credentials of your Redis Database. If your Redis server doesn't have a password, leave the password blank as it is.
- Under `credentials` in the [`database`](Database) section, enter your database credentials. If you're using a Mongo database, [follow the instructions](database#mongodb-setup) here. You shouldn't need to modify the `connection_pool` properties.
- Under `credentials` in the [`redis`](Redis) section, enter the credentials of your Redis server. If your Redis server doesn't have a password, leave the password blank as it is.
- Unless you want to have multiple clusters of servers within your network, each with separate user data, you should not change the value of `cluster_id`.
<details>
@@ -46,15 +45,7 @@ This will walk you through installing HuskSync on your network of Spigot or Fabr
(The `port` setting in `credentials` is disregarded when using Atlas.)
</details>
<details>
<summary>Pterodactyl self-hosts &mdash; Redis setup instructions</summary>
If you are hosting your Redis server on the same node as your servers, you need to use `172.18.0.1` as your host (or equivalent if you changed your network settings), and bind it in the Redis config `nano /etc/redis/redis.conf`.
You will also need to uncomment the `requirepass` directive and set a password to allow outside connections, or disable `protected-mode`. Once a password is set and Redis is restarted `systemctl restart redis`, you will also need to update the password in your pterodactyl `.env` (`nano /var/www/pterodactyl/.env`) and refresh the cache `cd /var/www/pterodactyl && php artisan config:clear`.
You may also need to allow connections from your firewall depending on your distribution.
</details>
### 4. Set server names in server.yml files
- Navigate to the server name file on each server (`~/plugins/HuskSync/server.yml` on Spigot, `~/config/husksync/server.yml` on Fabric)

View File

@@ -1,4 +1,4 @@
This page contains a list of the features HuskSync is and isn't able to syncrhonise on your server.
This page contains a list of the features HuskSync is and isn't able to synchronise on your server.
You can customise how much data HuskSync saves about a player by [turning each synchronization feature on or off](#toggling-sync-features). When a synchronization feature is turned off, HuskSync won't touch that part of a player's profile; in other words, the data they will inherit when changing servers will be read from their player data file on the local server.

View File

@@ -1,27 +1,41 @@
This page contains a number of common issues and how you can troubleshoot and resolve them.
This page contains a number of common issues when using HuskSync and how you can troubleshoot and resolve them.
## Topics
### Duplicate UUIDs in database
This is most frequently caused by running a cracked "offline mode" network of servers. We [don't provide support](https://william278.net/terms) for problems caused by cracked servers and the most advice we can offer you is:
- Ensure `bungee_online_mode` is set to the correct value in the `paper.yml` config file on each of your Bukkit servers
- Ensure your authenticator plugin is passing valid, unique IDs to each backend Spigot server.
- Ensure your authenticator plugin is passing valid, unique IDs to each backend Spigot/Fabric server.
### Cannot set data with newer Minecraft version than the server
This is caused when you attempt to downgrade user data from a newer version of Minecraft to an older one, or when your Spigot servers are running mismatched Minecraft versions.
This is caused when you attempt to downgrade user data from a newer version of Minecraft to an older one, or when your Spigot/Fabric servers are running mismatched Minecraft versions.
HuskSync will identify this and safely prevent the synchronization from occuring. Your Spigot servers must be running the same version of both Minecraft and HuskSync.
HuskSync will identify this and safely prevent the synchronization from occurring. Your Spigot/Fabric servers must be running the same version of both Minecraft and HuskSync.
### User data failing to synchronize
This can occur due to misaligned timings between your Spigot servers and your Redis server. HuskSync has a built in way of tuning this. Try continously increasing the `network_latency_milliseconds` option in your config to a higher value.
This can occur due to misaligned timings between your Spigot/Fabric servers and your Redis server. HuskSync has a built in way of tuning this. Try continously increasing the `network_latency_milliseconds` option in your config to a higher value.
### Synchronization issues with Keep Inventory enabled
On servers that use Keep Inventory move (where players keep their items when they die), you can run into synchronization issues. See [[Keep Inventory]] for details on why this happens and how to resolve it.
On servers that use [[Keep Inventory]] (where players keep their items when they die), you can run into synchronization issues. See [[Keep Inventory]] for details on why this happens and how to resolve it.
### Exceptions when compressing data via Snappy (lightweight Linux distros)
Some lightweight Linux distros such as Alpine Linux (used on Pterodactyl) might not have the dependencies needed for the [Snappy](https://github.com/xerial/snappy-java) compressor. It's possible to disable data compression by changing `compress_data` to false in your config. Note that after changing this setting you will need to reset your database. Alternatively, find the right libraries for your distro!
### Redis connection problems on Pterodactyl
If you are hosting your Redis server on the same node as your servers, you need to use 172.18.0.1 (or equivelant if you changed your network settings) as your host. You may also need to [allow connections from your firewall](https://pterodactyl.io/community/games/minecraft.html#firewalls) depending on your distribution.
### Redis connection problems on Pterodactyl / Pelican
If you are hosting your [[Redis]] server on the same node as your servers, you need to use 172.18.0.1 (or equivelant if you changed your network settings) as your host. You may also need to [allow connections from your firewall](https://pterodactyl.io/community/games/minecraft.html#firewalls) depending on your distribution. See our tips for running [Redis on a Pterodactyl or Pelican panel](Redis#pterodactyl--pelican-panel-hosts)
### MySQL connection problems on Pterodactyl
If you have more than one MySQL server connected to your panel, you may need to set `useSSL=true` in the parameters.
### Database connection problems on Pterodactyl / Pelican
If you have more than one [[Database]] server connected to your panel, you may need to set `useSSL=true` in the parameters.
### Issues with player data going out of sync during a server restart
This can happen due to the way in which your server restarts. If your server uses either:
* `/restart` (this is a weird Spigot/Fabric command that uses legacy bash scripting)
* ANY restart plugin, e.g. UltimateAutoRestart (these basically execute an API-called restart using the same legacy bash logic as per above)
These are **not compatible** with HuskSync in most cases due to the way in which this causes restart servers causing shutdown logic to process in strange and unpredictable orders, usually before HuskSync has had a chance to scan and perform its shutdown logic. To safely restart your server, please use:
* A Pterodactyl task to perform a Restart. This executes the Power Action program stopcode (and then execute the startup command when the container has terminated)
* A cronjob to send a stop command / Power Action program stopcode, listen for the service to fully terminate, and then execute your startup command
* For manual restarts, executing `/stop` and starting your server up with the startup command is totally fine.
It's not a great idea to use a plugin to handle restarts. Plugins are only able to operate when your server is turned on and must rely on scripts which don't safely shutdown servers when restarting.

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

@@ -1,21 +1,28 @@
## Guides
## Setup
* 📚 [[Setup]]
* 💾 [[Database]]
* ✨ [[Redis]]
* ⚠️ [[Compatibility]]
* 📄 [[Config File]]
* 🔗 [[Troubleshooting]]
* ↪️ [[Data Rotation]]
* ↗️ [[Legacy Migration]]
* ✨ [[MPDB Migration]]
* 🎏 [[Translations]]
* ❓ [[FAQs]]
## Documentation
## Features
* 🖥️ [[Commands]]
* ✅ [[Sync Features]]
* ⚙️ [[Sync Modes]]
* 🟩 [[Plan Hook]]
* ↪️ [[Data Rotation]]
* ❓ [[FAQs]]
## Guides
* ↗️ [[Legacy Migration]]
* ✨ [[MPDB Migration]]
* ☂️ [[Dumping UserData]]
* 🟩 [[Plan Hook]]
* 📋 [[Event Priorities]]
* ⚔️ [[Keep Inventory]]
* 🎏 [[Translations]]
## Developers
* 📦 [[API]] v3
* 📝 [[Data Snapshot API]]
* 📝 [[Custom Data API]]
@@ -28,4 +35,4 @@
* 🚰 [Spigot](https://www.spigotmc.org/resources/husksync.97144/)
* 🛒 [Polymart](https://polymart.org/resource/husksync.1634)
* ⚒️ [BuiltByBit](https://craftaro.com/marketplace/product/husksync.758)
* 💬 [Discord Support](https://discord.gg/tVYhJfyDWG)
* 💬 [Discord Support](https://discord.gg/tVYhJfyDWG)

View File

@@ -1,5 +1,5 @@
plugins {
id 'fabric-loom' version '1.7-SNAPSHOT'
id 'fabric-loom' version "$fabric_loom_version"
}
apply plugin: 'fabric-loom'
@@ -11,27 +11,28 @@ repositories {
}
dependencies {
minecraft "com.mojang:minecraft:${fabric_minecraft_version}"
minecraft "com.mojang:minecraft:${minecraft_version}"
mappings "net.fabricmc:yarn:${fabric_yarn_mappings}:v2"
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("eu.pb4:sgui:${sgui_version}")
modImplementation include('net.william278.uniform:uniform-fabric:1.1.4+1.20.1')
modImplementation include("eu.pb4:sgui:${fabric_sgui_version}")
modImplementation include("net.william278.uniform:uniform-fabric:1.3+${minecraft_version}")
modCompileOnly "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}"
implementation include('org.apache.commons:commons-pool2:2.12.0')
implementation include("redis.clients:jedis:$jedis_version")
implementation include("com.mysql:mysql-connector-j:$mysql_driver_version")
implementation include("org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version")
implementation include("org.postgresql:postgresql:$postgres_driver_version")
implementation include("org.xerial.snappy:snappy-java:$snappy_version")
compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'org.jetbrains:annotations:26.0.1'
compileOnly 'net.william278:DesertWell:2.0.4'
compileOnly 'org.projectlombok:lombok:1.18.32'
compileOnly 'org.projectlombok:lombok:1.18.36'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.36'
shadow project(path: ":common")
}

View File

@@ -32,7 +32,7 @@ import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.kyori.adventure.platform.AudienceProvider;
import net.kyori.adventure.platform.fabric.FabricServerAudiences;
import net.kyori.adventure.platform.modcommon.MinecraftServerAudiences;
import net.minecraft.server.MinecraftServer;
import net.william278.desertwell.util.Version;
import net.william278.husksync.adapter.DataAdapter;
@@ -49,6 +49,7 @@ import net.william278.husksync.database.MongoDbDatabase;
import net.william278.husksync.database.MySqlDatabase;
import net.william278.husksync.database.PostgresDatabase;
import net.william278.husksync.event.FabricEventDispatcher;
import net.william278.husksync.event.ModLoadedCallback;
import net.william278.husksync.hook.PlanHook;
import net.william278.husksync.listener.EventListener;
import net.william278.husksync.listener.FabricEventListener;
@@ -77,13 +78,27 @@ import java.util.logging.Level;
@Getter
@NoArgsConstructor
@SuppressWarnings("unchecked")
public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync, FabricTask.Supplier,
FabricEventDispatcher {
FabricEventDispatcher {
private static final String PLATFORM_TYPE_ID = "fabric";
private static final int VERSION1_16_5 = 2586;
private static final int VERSION1_17_1 = 2730;
private static final int VERSION1_18_2 = 2975;
private static final int VERSION1_19_2 = 3120;
private static final int VERSION1_19_4 = 3337;
private static final int VERSION1_20_1 = 3465;
private static final int VERSION1_20_2 = 3578;
private static final int VERSION1_20_4 = 3700;
private static final int VERSION1_20_5 = 3837;
private static final int VERSION1_21_1 = 3955;
private static final int VERSION1_21_3 = 4082;
private static final int VERSION1_21_4 = 4189; // Current
private final TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap(
SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR
SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR
);
private final Map<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap();
private final Map<String, Boolean> permissions = Maps.newHashMap();
@@ -124,6 +139,7 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
loadSettings();
loadLocales();
loadServer();
validateConfigFiles();
});
// Register commands
@@ -141,7 +157,10 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
private void onEnable() {
// Initial plugin setup
this.audiences = FabricServerAudiences.of(minecraftServer);
this.audiences = MinecraftServerAudiences.of(minecraftServer);
// Check compatibility
checkCompatibility();
// Prepare data adapter
initialize("data adapter", (plugin) -> {
@@ -206,6 +225,8 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
// Check for updates
this.checkForUpdates();
ModLoadedCallback.EVENT.invoker().post(FabricHuskSyncAPI.getInstance());
}
private void onDisable() {
@@ -260,19 +281,28 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
return FabricUniform.getInstance(mod.getMetadata().getId());
}
@NotNull
@Override
public Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
return playerCustomDataStore.compute(
user.getUuid(),
(uuid, data) -> data == null ? Maps.newHashMap() : data
);
}
@Override
@Nullable
public InputStream getResource(@NotNull String name) {
return this.mod.findPath(name)
.map(path -> {
try {
return Files.newInputStream(path);
} catch (IOException e) {
log(Level.WARNING, "Failed to load resource: " + name, e);
}
return null;
})
.orElse(this.getClass().getClassLoader().getResourceAsStream(name));
.map(path -> {
try {
return Files.newInputStream(path);
} catch (IOException e) {
log(Level.WARNING, "Failed to load resource: " + name, e);
}
return null;
})
.orElse(this.getClass().getClassLoader().getResourceAsStream(name));
}
@Override
@@ -292,11 +322,11 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
@Override
public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable) {
LoggingEventBuilder logEvent = logger.makeLoggingEventBuilder(
switch (level.getName()) {
case "WARNING" -> org.slf4j.event.Level.WARN;
case "SEVERE" -> org.slf4j.event.Level.ERROR;
default -> org.slf4j.event.Level.INFO;
}
switch (level.getName()) {
case "WARNING" -> org.slf4j.event.Level.WARN;
case "SEVERE" -> org.slf4j.event.Level.ERROR;
default -> org.slf4j.event.Level.INFO;
}
);
if (throwable.length >= 1) {
logEvent = logEvent.setCause(throwable[0]);
@@ -322,12 +352,39 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
return Version.fromString(minecraftServer.getVersion());
}
@NotNull
public int getDataVersion(@NotNull Version mcVersion) {
return switch (mcVersion.toStringWithoutMetadata()) {
case "1.16", "1.16.1", "1.16.2", "1.16.3", "1.16.4", "1.16.5" -> VERSION1_16_5;
case "1.17", "1.17.1" -> VERSION1_17_1;
case "1.18", "1.18.1", "1.18.2" -> VERSION1_18_2;
case "1.19", "1.19.1", "1.19.2" -> VERSION1_19_2;
case "1.19.4" -> VERSION1_19_4;
case "1.20", "1.20.1" -> VERSION1_20_1;
case "1.20.2" -> VERSION1_20_2;
case "1.20.4" -> VERSION1_20_4;
case "1.20.5", "1.20.6" -> VERSION1_20_5;
case "1.21", "1.21.1" -> VERSION1_21_1;
case "1.21.2", "1.21.3" -> VERSION1_21_3;
case "1.21.4" -> VERSION1_21_4;
default -> VERSION1_21_4; // Current supported ver
};
}
@NotNull
@Override
public String getPlatformType() {
return PLATFORM_TYPE_ID;
}
@Override
@NotNull
public String getServerVersion() {
return String.format("%s %s/%s", getPlatformType(), FabricLoader.getInstance()
.getModContainer("fabricloader").map(l -> l.getMetadata().getVersion().getFriendlyString())
.orElse("unknown"), minecraftServer.getVersion());
}
@Override
public Optional<LegacyConverter> getLegacyConverter() {
return Optional.empty();

View File

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

View File

@@ -27,6 +27,7 @@ import lombok.AllArgsConstructor;
import net.minecraft.datafixer.TypeReferences;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.*;
import net.minecraft.registry.DynamicRegistryManager;
import net.william278.desertwell.util.Version;
import net.william278.husksync.FabricHuskSync;
import net.william278.husksync.HuskSync;
@@ -58,7 +59,7 @@ public abstract class FabricSerializer {
}
public static class Inventory extends FabricSerializer implements Serializer<FabricData.Items.Inventory>,
ItemDeserializer {
ItemDeserializer {
public Inventory(@NotNull HuskSync plugin) {
super(plugin);
@@ -66,7 +67,7 @@ public abstract class FabricSerializer {
@Override
public FabricData.Items.Inventory deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
throws DeserializationException {
throws DeserializationException {
// Read item NBT from string
final FabricHuskSync plugin = (FabricHuskSync) getPlugin();
final NbtCompound root;
@@ -79,8 +80,8 @@ public abstract class FabricSerializer {
// Deserialize the inventory data
final NbtCompound items = root.contains(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null;
return FabricData.Items.Inventory.from(
items != null ? getItems(items, dataMcVersion, plugin) : new ItemStack[INVENTORY_SLOT_COUNT],
root.contains(HELD_ITEM_SLOT_TAG) ? root.getInt(HELD_ITEM_SLOT_TAG) : 0
items != null ? getItems(items, dataMcVersion, plugin) : new ItemStack[INVENTORY_SLOT_COUNT],
root.contains(HELD_ITEM_SLOT_TAG) ? root.getInt(HELD_ITEM_SLOT_TAG) : 0
);
}
@@ -94,7 +95,7 @@ public abstract class FabricSerializer {
public String serialize(@NotNull FabricData.Items.Inventory data) throws SerializationException {
try {
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());
return root.toString();
} catch (Throwable e) {
@@ -105,7 +106,7 @@ public abstract class FabricSerializer {
}
public static class EnderChest extends FabricSerializer implements Serializer<FabricData.Items.EnderChest>,
ItemDeserializer {
ItemDeserializer {
public EnderChest(@NotNull HuskSync plugin) {
super(plugin);
@@ -113,7 +114,7 @@ public abstract class FabricSerializer {
@Override
public FabricData.Items.EnderChest deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
throws DeserializationException {
throws DeserializationException {
final FabricHuskSync plugin = (FabricHuskSync) getPlugin();
try {
final NbtCompound items = StringNbtReader.parse(serialized);
@@ -132,7 +133,7 @@ public abstract class FabricSerializer {
@Override
public String serialize(@NotNull FabricData.Items.EnderChest data) throws SerializationException {
try {
return serializeItemArray(data.getContents()).toString();
return serializeItemArray(data.getContents(), (FabricHuskSync) getPlugin()).toString();
} catch (Throwable e) {
throw new SerializationException("Failed to serialize ender chest item NBT to string", e);
}
@@ -141,17 +142,6 @@ public abstract class FabricSerializer {
private interface ItemDeserializer {
int VERSION1_16_5 = 2586;
int VERSION1_17_1 = 2730;
int VERSION1_18_2 = 2975;
int VERSION1_19_2 = 3120;
int VERSION1_19_4 = 3337;
int VERSION1_20_1 = 3465;
int VERSION1_20_2 = 3578; // Future
int VERSION1_20_4 = 3700; // Future
int VERSION1_20_5 = 3837; // Future
int VERSION1_21 = 3953; // Future
@NotNull
default ItemStack[] getItems(@NotNull NbtCompound tag, @NotNull Version mcVersion, @NotNull FabricHuskSync plugin) {
try {
@@ -161,9 +151,10 @@ public abstract class FabricSerializer {
final ItemStack[] contents = new ItemStack[tag.getInt("size")];
final NbtList itemList = tag.getList("items", NbtElement.COMPOUND_TYPE);
final DynamicRegistryManager registryManager = plugin.getMinecraftServer().getRegistryManager();
itemList.forEach(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));
return contents;
@@ -174,18 +165,18 @@ public abstract class FabricSerializer {
// Serialize items slot-by-slot
@NotNull
default NbtCompound serializeItemArray(@Nullable ItemStack @NotNull [] items) {
default NbtCompound serializeItemArray(@Nullable ItemStack @NotNull [] items, @NotNull FabricHuskSync plugin) {
final NbtCompound container = new NbtCompound();
container.putInt("size", items.length);
final NbtList itemList = new NbtList();
final DynamicRegistryManager registryManager = plugin.getMinecraftServer().getRegistryManager();
for (int i = 0; i < items.length; i++) {
final ItemStack item = items[i];
if (item == null || item.isEmpty()) {
continue;
}
NbtCompound entry = new NbtCompound();
NbtCompound entry = (NbtCompound) item.toNbt(registryManager);
entry.putInt("Slot", i);
item.writeNbt(entry);
itemList.add(entry);
}
container.put(ITEMS_TAG, itemList);
@@ -198,6 +189,7 @@ public abstract class FabricSerializer {
final int size = items.getInt("size");
final NbtList list = items.getList("items", NbtElement.COMPOUND_TYPE);
final ItemStack[] itemStacks = new ItemStack[size];
final DynamicRegistryManager registryManager = plugin.getMinecraftServer().getRegistryManager();
Arrays.fill(itemStacks, ItemStack.EMPTY);
for (int i = 0; i < size; i++) {
if (list.getCompound(i) == null) {
@@ -205,38 +197,21 @@ public abstract class FabricSerializer {
}
final NbtCompound compound = list.getCompound(i);
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;
}
@NotNull
@SuppressWarnings({"rawtypes", "unchecked"}) // For NBTOps lookup
private NbtCompound upgradeItemData(@NotNull NbtCompound tag, @NotNull Version mcVersion,
@NotNull FabricHuskSync plugin) {
return (NbtCompound) plugin.getMinecraftServer().getDataFixer().update(
TypeReferences.ITEM_STACK, new Dynamic<Object>((DynamicOps) NbtOps.INSTANCE, tag),
getDataVersion(mcVersion), getDataVersion(plugin.getMinecraftVersion())
TypeReferences.ITEM_STACK, new Dynamic<Object>((DynamicOps) NbtOps.INSTANCE, tag),
plugin.getDataVersion(mcVersion), plugin.getDataVersion(plugin.getMinecraftVersion())
).getValue();
}
private int getDataVersion(@NotNull Version mcVersion) {
return switch (mcVersion.toStringWithoutMetadata()) {
case "1.16", "1.16.1", "1.16.2", "1.16.3", "1.16.4", "1.16.5" -> VERSION1_16_5;
case "1.17", "1.17.1" -> VERSION1_17_1;
case "1.18", "1.18.1", "1.18.2" -> VERSION1_18_2;
case "1.19", "1.19.1", "1.19.2" -> VERSION1_19_2;
case "1.19.4" -> VERSION1_19_4;
case "1.20", "1.20.1" -> VERSION1_20_1;
case "1.20.2" -> VERSION1_20_2; // Future
case "1.20.4" -> VERSION1_20_4; // Future
case "1.20.5", "1.20.6" -> VERSION1_20_5; // Future
case "1.21" -> VERSION1_21; // Future
default -> VERSION1_20_1; // Current supported ver
};
}
}
public static class PotionEffects extends FabricSerializer implements Serializer<FabricData.PotionEffects> {
@@ -251,7 +226,7 @@ public abstract class FabricSerializer {
@Override
public FabricData.PotionEffects deserialize(@NotNull String serialized) throws DeserializationException {
return FabricData.PotionEffects.adapt(
plugin.getGson().fromJson(serialized, TYPE.getType())
plugin.getGson().fromJson(serialized, TYPE.getType())
);
}
@@ -275,7 +250,7 @@ public abstract class FabricSerializer {
@Override
public FabricData.Advancements deserialize(@NotNull String serialized) throws DeserializationException {
return FabricData.Advancements.from(
plugin.getGson().fromJson(serialized, TYPE.getType())
plugin.getGson().fromJson(serialized, TYPE.getType())
);
}

View File

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

View File

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

View File

@@ -39,7 +39,6 @@ import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.ActionResult;
import net.minecraft.util.Hand;
import net.minecraft.util.TypedActionResult;
import net.minecraft.util.hit.BlockHitResult;
import net.minecraft.util.hit.EntityHitResult;
import net.minecraft.util.math.BlockPos;
@@ -54,8 +53,6 @@ import net.william278.husksync.user.OnlineUser;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.stream.Collectors;
public class FabricEventListener extends EventListener implements LockedHandler {
public FabricEventListener(@NotNull HuskSync plugin) {
@@ -126,9 +123,8 @@ public class FabricEventListener extends EventListener implements LockedHandler
return (cancelPlayerEvent(player.getUuid())) ? ActionResult.FAIL : ActionResult.PASS;
}
private TypedActionResult<ItemStack> handleItemInteract(PlayerEntity player, World world, Hand hand) {
ItemStack stackInHand = player.getStackInHand(hand);
return (cancelPlayerEvent(player.getUuid())) ? TypedActionResult.fail(stackInHand) : TypedActionResult.pass(stackInHand);
private ActionResult handleItemInteract(PlayerEntity player, World world, Hand hand) {
return (cancelPlayerEvent(player.getUuid())) ? ActionResult.FAIL : ActionResult.PASS;
}
private boolean handleBlockBreak(World world, PlayerEntity player, BlockPos blockPos, BlockState blockState, BlockEntity blockEntity) {

View File

@@ -0,0 +1,35 @@
/*
* 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.mixins;
import net.minecraft.entity.player.HungerManager;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(HungerManager.class)
public interface HungerManagerMixin {
@Accessor
float getExhaustion();
@Accessor("exhaustion")
void setExhaustion(float exhaustion);
}

View File

@@ -20,9 +20,11 @@
package net.william278.husksync.mixins;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.enchantment.Enchantments;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.registry.tag.TagKey;
import net.minecraft.server.network.ServerPlayerEntity;
import net.william278.husksync.event.PlayerDeathDropsCallback;
import org.jetbrains.annotations.NotNull;
@@ -54,7 +56,7 @@ public class PlayerEntityMixin {
final @Nullable ItemStack @NotNull [] toKeep = new ItemStack[inventory.size()];
for (int i = 0; i < inventory.size(); ++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;
continue;
}
@@ -69,7 +71,7 @@ public class PlayerEntityMixin {
final @Nullable ItemStack @NotNull [] toDrop = new ItemStack[inventory.size()];
for (int i = 0; i < inventory.size(); ++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;
continue;
}

View File

@@ -34,6 +34,7 @@ import net.william278.husksync.event.ItemDropCallback;
import net.william278.husksync.event.PlayerCommandCallback;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@@ -44,8 +45,10 @@ public abstract class ServerPlayNetworkHandlerMixin {
@Shadow
public ServerPlayerEntity player;
@Shadow
public abstract void sendPacket(Packet<?> packet);
@Unique
private void sendToPlayer(Packet<?> packet) {
this.player.networkHandler.sendPacket(packet);
}
@Inject(method = "onPlayerAction", at = @At("HEAD"), cancellable = true)
public void onPlayerAction(PlayerActionC2SPacket packet, CallbackInfo ci) {
@@ -56,7 +59,7 @@ public abstract class ServerPlayNetworkHandlerMixin {
if (result == ActionResult.FAIL) {
ci.cancel();
this.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(
sendToPlayer(new ScreenHandlerSlotUpdateS2CPacket(
-2,
1,
player.getInventory().getSlotWithStack(stack),
@@ -69,30 +72,34 @@ public abstract class ServerPlayNetworkHandlerMixin {
@Inject(method = "onClickSlot", at = @At("HEAD"), cancellable = true)
public void onClickSlot(ClickSlotC2SPacket packet, CallbackInfo ci) {
int slot = packet.getSlot();
if (slot < 0) return;
if (slot < 0) {
return;
}
ItemStack stack = this.player.getInventory().getStack(slot);
ActionResult result = ItemDropCallback.EVENT.invoker().interact(player, stack);
if (result == ActionResult.FAIL) {
ci.cancel();
this.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(-2, 1, slot, stack));
this.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(-1, 1, -1, ItemStack.EMPTY));
sendToPlayer(new ScreenHandlerSlotUpdateS2CPacket(-2, 1, slot, stack));
sendToPlayer(new ScreenHandlerSlotUpdateS2CPacket(-1, 1, -1, ItemStack.EMPTY));
}
}
@Inject(method = "onCreativeInventoryAction", at = @At("HEAD"), cancellable = true)
public void onCreativeInventoryAction(CreativeInventoryActionC2SPacket packet, CallbackInfo ci) {
int slot = packet.getSlot();
if (slot < 0) return;
int slot = packet.slot();
if (slot < 0) {
return;
}
ItemStack stack = this.player.getInventory().getStack(slot);
ActionResult result = ItemDropCallback.EVENT.invoker().interact(player, stack);
if (result == ActionResult.FAIL) {
ci.cancel();
this.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(-2, 1, slot, stack));
this.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(-1, 1, -1, ItemStack.EMPTY));
sendToPlayer(new ScreenHandlerSlotUpdateS2CPacket(-2, 1, slot, stack));
sendToPlayer(new ScreenHandlerSlotUpdateS2CPacket(-1, 1, -1, ItemStack.EMPTY));
}
}

View File

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

View File

@@ -36,7 +36,7 @@ public class ServerWorldMixin {
@Shadow
private MinecraftServer server;
@Inject(method = "saveLevel", at = @At("HEAD"))
@Inject(method = "savePersistentState", at = @At("HEAD"))
public void saveLevel(CallbackInfo ci) {
if (server.isStopping() || server.isStopped()) {
return;

View File

@@ -25,7 +25,7 @@ import eu.pb4.sgui.api.elements.GuiElementInterface;
import eu.pb4.sgui.api.gui.SimpleGui;
import me.lucko.fabric.api.permissions.v0.Permissions;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.fabric.FabricServerAudiences;
import net.kyori.adventure.platform.modcommon.MinecraftServerAudiences;
import net.minecraft.item.ItemStack;
import net.minecraft.screen.GenericContainerScreenHandler;
import net.minecraft.screen.ScreenHandlerType;
@@ -40,6 +40,7 @@ import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer;
import java.util.logging.Level;
public class FabricUser extends OnlineUser implements FabricUserDataHolder {
@@ -70,9 +71,12 @@ public class FabricUser extends OnlineUser implements FabricUserDataHolder {
}
@Override
@Deprecated(since = "3.6.7")
public void sendToast(@NotNull MineDown title, @NotNull MineDown description, @NotNull String iconMaterial,
@NotNull String backgroundType) {
getAudience().sendActionBar(title.toComponent()); // Toasts unimplemented for now
plugin.log(Level.WARNING, "Toast notifications are deprecated. " +
"Please change your notification display slot to CHAT, ACTION_BAR or NONE.");
this.sendActionBar(title);
}
@Override
@@ -98,7 +102,7 @@ public class FabricUser extends OnlineUser implements FabricUserDataHolder {
this.editable = editable;
// Set title, items
this.setTitle(((FabricServerAudiences) plugin.getAudiences()).toNative(title.toComponent()));
this.setTitle(((MinecraftServerAudiences) plugin.getAudiences()).asNative(title.toComponent()));
this.setLockPlayerInventory(!editable);
for (int i = 0; i < size; i++) {
final ItemStack item = items.getContents()[i];

View File

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

View File

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

View File

@@ -4,6 +4,7 @@
"package": "net.william278.husksync.mixins",
"compatibilityLevel": "JAVA_17",
"server": [
"HungerManagerMixin",
"ItemEntityMixin",
"PlayerEntityMixin",
"ServerPlayerEntityMixin",

View File

@@ -1,23 +1,31 @@
# Gradle settings
org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.daemon=true
javaVersion=17
javaVersion=21
plugin_version=3.6.1
# Plugin settings
plugin_version=3.7.3
minecraft_version=1.21.4
plugin_archive=husksync
plugin_description=A modern, cross-server player data synchronization system
jedis_version=5.1.3
mysql_driver_version=8.4.0
mariadb_driver_version=3.4.0
postgres_driver_version=42.7.3
mongodb_driver_version=5.1.0
snappy_version=1.1.10.5
# Drivers
jedis_version=5.2.0
mysql_driver_version=9.2.0
mariadb_driver_version=3.5.1
postgres_driver_version=42.7.5
mongodb_driver_version=5.3.1
snappy_version=1.1.10.7
fabric_minecraft_version=1.20.1
fabric_loader_version=0.15.11
fabric_yarn_mappings=1.20.1+build.10
fabric_api_version=0.92.2+1.20.1
adventure_platform_fabric_version=5.9.0
fabric_permissions_api_version=0.2-SNAPSHOT
sgui_version=1.2.2+1.20
# Spigot/Paper build settings
bukkit_spigot_api=1.21.4-R0.1-SNAPSHOT
bukkit_paper_api=1.21.4-R0.1-SNAPSHOT
# Fabric build settings
fabric_loom_version=1.9-SNAPSHOT
fabric_loader_version=0.16.10
fabric_yarn_mappings=1.21.4+build.8
fabric_api_version=0.115.0+1.21.4
fabric_adventure_platform_version=6.2.0
fabric_permissions_api_version=0.3.3
fabric_sgui_version=1.8.2+1.21.4

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

6
gradlew vendored
View File

@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -84,7 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

186
gradlew.bat vendored
View File

@@ -1,92 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -1,14 +1,18 @@
plugins {
id 'xyz.jpenilla.run-paper' version '2.3.1'
}
dependencies {
implementation project(':bukkit')
compileOnly project(':common')
implementation 'net.william278.uniform:uniform-paper:1.1.4'
implementation 'net.william278.uniform:uniform-paper:1.3'
compileOnly 'io.papermc.paper:paper-api:1.19.4-R0.1-SNAPSHOT'
compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'org.projectlombok:lombok:1.18.32'
compileOnly "io.papermc.paper:paper-api:${bukkit_paper_api}"
compileOnly 'org.jetbrains:annotations:26.0.1'
compileOnly 'org.projectlombok:lombok:1.18.36'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.36'
}
shadowJar {
@@ -30,7 +34,6 @@ shadowJar {
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
relocate 'net.william278.andjam', 'net.william278.husksync.libraries.andjam'
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
relocate 'org.json', 'net.william278.husksync.libraries.json'
@@ -42,4 +45,10 @@ shadowJar {
relocate 'de.tr7zw.changeme.nbtapi', 'net.william278.husksync.libraries.nbtapi'
minimize()
}
tasks {
runServer {
minecraftVersion('1.21.4')
}
}

View File

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

View File

@@ -43,6 +43,12 @@ public class PaperEventListener extends BukkitEventListener {
super(plugin);
}
@Override
public void onEnable() {
getPlugin().getServer().getPluginManager().registerEvents(this, getPlugin());
lockedHandler.onEnable();
}
@Override
public void handlePlayerDeath(@NotNull PlayerDeathEvent event) {
// If the player is locked or the plugin disabling, clear their drops

View File

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

View File

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