9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-19 14:59:21 +00:00

Compare commits

...

651 Commits

Author SHA1 Message Date
William278
70d6b671f2 Merge remote-tracking branch 'origin/test/debug-log-advancements' into test/debug-log-advancements 2025-03-20 19:34:16 +00:00
William278
98576c72fb feat: debug log all advancements 2025-03-20 19:34:00 +00:00
William278
ae657acee3 feat: skip unserializable items on Fabric 2025-03-20 19:29:20 +00:00
William278
34dc6a537d feat: add count check on fabric item array serializer 2025-03-17 20:06:41 +00:00
William278
e99ba66271 refactor: check if read map data was null 2025-03-17 19:48:10 +00:00
William278
546e663e4e feat: debug log all advancements 2025-03-14 18:04:43 +00:00
William278
0111f25865 Merge remote-tracking branch 'origin/master' 2025-03-09 15:02:07 +00:00
William278
02c8b899dc feat: add ability to run /userdata dump without args 2025-03-08 12:43:27 +00:00
William278
b725015318 feat: bump to 3.8, dont set user data on update 2025-03-07 18:16:46 +00:00
William278
11550e0ba3 refactor: use pastes.dev for viewing userdata dumps 2025-03-07 17:30:18 +00:00
William278
33e20a0c0b fix: return bytebin URL for viewing dumps 2025-03-07 17:25:03 +00:00
William278
0ae13d730d feat: use bytebin for dump uploading
`UserDataDumper` now implements `Flusher` from `toilet`
2025-03-07 17:23:24 +00:00
William278
f5ad5c079f locales: add locale for save_cause_save_command 2025-03-07 17:07:52 +00:00
William278
305f90f697 feat: various logical improvements to data syncing
* Update data in Redis during world saves & commands
* Always set data time to 1 year regardless of sync mode
2025-03-07 16:54:19 +00:00
William278
e56041eae2 feat: add /userdata save
Also adds docs for `/husksync dump`
2025-03-07 16:36:34 +00:00
William
904c65ba39 feat: rework locked maps syncing (#464)
* Better maps syncing (#2)

* Do not create new views for maps from current world

* Fix maps in shulkers not converting

* Add bundle support for map conversion

* Rework map sync

* Fix empty statements in database

* Fix missing imports

* Rename connectMapIds -> bindMapIds

* Use data adapter to save maps

* Split Mongo readMapData

* Split MySQL readMapData

* Split Postgres readMapData

* Update database schemas

Use server names instead of world UUIDs

* Update Database class

* Update MongoDbDatabase class

* Update MySqlDatabase class

* Update PostgresDatabase class

* Update BukkitMapPersister class

Use server names instead of world UUIDs

* Remove unused code

* Add my nickname to contributors :)

* Start implementing Redis map caching

* Continue implementing Redis map caching

* Bind map ids on Redis before writing to DB

* Finish implementing Redis map data caching

* refactor: decouple new map logic Redis caching from DB

* test: enable debug logging in test suite

* docs: update docs with new username method

* feat: adjust a method name

---------

Co-authored-by: Sóla Lusøt <60041069+solaluset@users.noreply.github.com>
2025-03-07 16:06:27 +00:00
William278
fbb8ec3048 test: improve test suite
use velocity instead of Waterfall
add some basic docs
include the default test config
2025-03-07 14:26:10 +00:00
William278
2a59a0b3f5 feat: implement hashCode in Identifier classes 2025-03-06 14:38:01 +00:00
William278
b108d38598 feat: add /husksync dump status dumping, close #460 2025-03-06 14:36:33 +00:00
William278
8b7e891ab6 build: use JitPack for ProtocolLib 2025-03-05 17:17:43 +00:00
William
be6bebe361 build: Use multi-version and preprocessor to build all target versions from one branch (#463)
* feat: convert Fabric to use essential-multi-version

* fix: populate compatibility.yml with mc version

* feat: start WIP work on doing the same for bukkit

* refactor: use preprocessor plugin to multi-version bukkit

* docs: update README to mention multi-version stuff

* build: also include javadocs for Bukkit

* build: update CI workflows

also slightly simplifies buildscript
2025-03-05 16:52:21 +00:00
William278
1ff4cab88d build: bump uniform to 1.3.1 2025-03-04 15:28:57 +00:00
dependabot[bot]
e3c40a231b deps: bump org.apache.commons:commons-pool2 from 2.12.0 to 2.12.1 (#452)
Bumps org.apache.commons:commons-pool2 from 2.12.0 to 2.12.1.

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-09 15:29:11 +00:00
dependabot[bot]
c8fd3f88fa deps: bump com.google.code.gson:gson from 2.11.0 to 2.12.1 (#453)
Bumps [com.google.code.gson:gson](https://github.com/google/gson) from 2.11.0 to 2.12.1.
- [Release notes](https://github.com/google/gson/releases)
- [Changelog](https://github.com/google/gson/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google/gson/compare/gson-parent-2.11.0...gson-parent-2.12.1)

---
updated-dependencies:
- dependency-name: com.google.code.gson:gson
  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-02-09 15:29:02 +00:00
dependabot[bot]
f9ec1f3ebb deps: bump com.gradleup.shadow from 8.3.5 to 8.3.6 (#454)
Bumps [com.gradleup.shadow](https://github.com/GradleUp/shadow) from 8.3.5 to 8.3.6.
- [Release notes](https://github.com/GradleUp/shadow/releases)
- [Commits](https://github.com/GradleUp/shadow/compare/8.3.5...8.3.6)

---
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>
2025-02-09 15:28:54 +00:00
dependabot[bot]
033af3126c deps: bump org.jetbrains:annotations from 26.0.1 to 26.0.2 (#450)
Bumps [org.jetbrains:annotations](https://github.com/JetBrains/java-annotations) from 26.0.1 to 26.0.2.
- [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.1...26.0.2)

---
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>
2025-01-31 16:46:48 +00:00
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
William
268b279fdf feat: add the ability to disable HuskSync commands 2024-06-18 13:26:21 +01:00
William
a8ca3314d8 refactor: minor userdata dump refactor 2024-06-18 13:20:24 +01:00
William
2bdd3dae37 fix: enable game mode syncing by default
not sure why this is off by default
2024-06-18 13:11:29 +01:00
William
e29564c4ad deps: bump Uniform to 1.1.4
Fixes namespaced-backed commands being missing
2024-06-18 13:02:58 +01:00
William
6b8bb23af9 fix: cleanup leftover todo 2024-06-18 12:34:56 +01:00
William
91bbe05851 fix: fix various Fabric issues
Adjusted a mixin
Fixed Uniform being relocated causing a ClassNotFound exception (it's a JiJ mod now)
2024-06-18 12:31:58 +01:00
William
8ed6869aad docs: update maven README badge 2024-06-18 01:03:08 +01:00
dependabot[bot]
7efdf0d329 build(deps): bump urllib3 from 2.0.7 to 2.2.2 in /test (#324)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.7 to 2.2.2.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.0.7...2.2.2)

---
updated-dependencies:
- dependency-name: urllib3
  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-06-18 01:01:26 +01:00
William
49c32e3f98 build: adjust repos order 2024-06-18 00:59:56 +01:00
William
f0574527b9 build: bump gradle to 8.8, uniform to 1.1.3 2024-06-18 00:46:29 +01:00
William
ad510a8fca deps: bump uniform to 1.1.2 2024-06-17 23:07:17 +01:00
William
303b287705 deps: bump uniform to 1.1.1 2024-06-17 22:44:04 +01:00
William
549508b9c1 fix: shadow fabric, not implement! 2024-06-17 22:21:48 +01:00
William
6c8a577701 fix: suppress IncompatibleClassChangeError on paper
Paper plugins don't get run through bytecode fixups by Spigot's Commodore. Spigot changed InventoryView to an interface recently, which causes this to be thrown.
2024-06-17 22:14:42 +01:00
William
862177bec7 build: bump to 3.6.1 2024-06-17 21:43:33 +01:00
William
dbed4d83a2 deps: bump NBT-API to 2.13.1-SNAPSHOT
Fixes 1.21 support on Paper
2024-06-17 21:28:35 +01:00
William
aa2090d97a docs: remove brigadier tab completion 2024-06-17 21:19:32 +01:00
William
b168ede7c5 fix: locked maps in shulker boxes not saving, close #322 2024-06-17 21:18:05 +01:00
William
0e706d36c4 refactor: use Uniform for native command support (#323)
* refactor: use Uniform for commands

* refactor: remove commodore

* fix: update Uniform, fix commands

* fix: bump uniform, fix commands on fabric

* feat: use new Uniform command permission system

* test: target 1.21
2024-06-17 21:07:09 +01:00
William
69d68de5c0 build: adjust Fabric build to append MC version 2024-06-15 18:20:30 +01:00
William
3d5395e5ae refactor: Remove debug print statements 2024-06-15 18:16:56 +01:00
William
332c71f041 fix/fabric: fix first item slot not syncing 2024-06-15 14:16:03 +01:00
William
b9fbcd72dd fix/fabric: slightly adjust item applying 2024-06-15 13:55:38 +01:00
William
68897e6265 fix/fabric: fix way game mode is changed 2024-06-15 13:51:29 +01:00
William
04606a7c9a docs: improve setup instructions
Improve Mongo instructions & add advice for Pterodactyl self-hosts
2024-06-15 13:46:04 +01:00
Stampede
6286bbe2ad fix: mongo breaking due to mixed use of UUIDs and strings (#321)
All UUIDs are now read and written as actual UUID objects, which was before causing errors due to a mixed use of UUID objects and string representations.
2024-06-15 13:41:52 +01:00
William
24ba209f8f feat: support 1.21
Fixes attribute modifier syncing, adjust apache dep
2024-06-14 12:34:05 +01:00
William
05d588f681 fix: wrong syntax message on /userdata 2024-06-14 11:51:15 +01:00
William
9aa3606f54 build: update Item-NBT-API to support 1.21 2024-06-14 11:18:16 +01:00
William
fc05e4b17a fix: only MySQL being supported on Fabric 2024-06-10 15:22:36 +01:00
dependabot[bot]
7b2b47de83 deps: bump org.projectlombok:lombok from 1.18.30 to 1.18.32 (#319)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.30 to 1.18.32.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.30...v1.18.32)

---
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-06-10 11:20:40 +01:00
dependabot[bot]
be0b4e3397 deps: bump dev.triumphteam:triumph-gui from 3.1.7 to 3.1.10 (#318)
Bumps [dev.triumphteam:triumph-gui](https://github.com/TriumphTeam/triumph-gui) from 3.1.7 to 3.1.10.
- [Release notes](https://github.com/TriumphTeam/triumph-gui/releases)
- [Commits](https://github.com/TriumphTeam/triumph-gui/compare/3.1.7...3.1.10)

---
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-06-10 11:20:32 +01:00
dependabot[bot]
dd1ba594de deps: bump org.jetbrains:annotations from 24.0.1 to 24.1.0 (#317)
Bumps [org.jetbrains:annotations](https://github.com/JetBrains/java-annotations) from 24.0.1 to 24.1.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.0.1...24.1.0)

---
updated-dependencies:
- dependency-name: org.jetbrains:annotations
  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-06-10 11:20:24 +01:00
William
89368778f3 feat: add support for Fabric targeting Minecraft 1.20.1 (#217)
* Upgrade the Fabric version and rewrite the code.

* Migrate the completed code of version 1.19.2.

* fabric: some events.

* Updated open source license to Apache 2.0.

* Add Plan analyzer support.

* Fix build.

* `UnsupportedOperationException`

* More fabric implementation work, update to v3's structure

* Suppress compiler warnings

* Add commands, adjust registration order

* Inventory and ender chest data/serializers

* Update license headers

* Fixup shaded library relocations

* Fix build

* Potion effects & location serializers

* Catch `Files.createDirectory(path);` in `#getDataFolder`

* Update fabric.mod.json metadata, correct icon

* Events for Fabric (#218)

* Added apache commons pool2 dependency

A NoClassDefFoundError would get thrown without this dependency. Relocation appears to not work very well either, so it has been excluded for now

* Added in Item Pickup and Drop events and mixins

* Update husksync.mixins.json

* Switch drop item event to using Network Handler mixin

* Implemented even more events

- Interact block (place too)
- Interact Entity
- Use Item
- Block Break
- Player damage
- Inventory Click (handles drops)
- Player Commands

* Re-implement the dropItem mixin

* Set dropItem mixin as cancellable

* deps: Include all bukkit runtime deps

* fix/fabric: Supply AudienceProvider to `ConsoleUser` constructor

* docs: credit Fabric porters :)

* fix: Item deserialization now working

* refactor: Remove inventory debug log

* docs: Update `fabric.mod.json`

* refactor: update with upstream changes

* fix: dangling JD comment

* fix: config file reference fixes

* refactor: optimize imports, fix relocation

* refactor: move tag references to common

* refactor: use lombok for data / serializer methods

* fix: bad annotating

* refactor: adjust callback formatting

* fabric: bump deps, refactor to match main branch

* fabric: more serializer type work

* feat: register more fabric data serializers

also fixes a compile issue on bukkit, and refactors the JSON serializer to be in the common module

* feat: implement remaining Fabric serializers

* feat: add on-the-fly DFU for Fabric

Now auto-upgrades item data to support version bumps. Also improved the schema a lil' bit.

* feat: add missing mixins

* feat: implement toKeep/toDrop option on Fabric

* feat: apply stats on sync

* build: append fabric MC version to file name

* feat: add HuskSync API support for Fabric

Also updates the docs

* refactor: fixup a deprecation in the wrong spot

* refactor: optimize fabric item serializing in-line with Bukkit

* feat: implement viewer GUIs on Fabric

* docs: Fabric is in Alpha for now

---------

Co-authored-by: hanbings <hanbings@hanbings.io>
Co-authored-by: Stampede <carterblowers01@gmail.com>
2024-06-09 22:41:37 +01:00
William
e3fb1762a1 fix: display correct NotRegisteredException cause 2024-06-09 14:47:26 +01:00
William
516c243df8 refactor: throw NotRegisteredException if API class provider is bad 2024-06-09 14:46:27 +01:00
William
b7aa75fcd5 docs: correct typos 2024-06-09 14:33:26 +01:00
小蔡
549f013e0f locales: update zh-tw.yml (#316)
* Update zh-tw.yml

I also corrected some redundant words.

* Update zh-tw.yml
2024-06-05 12:30:34 +01:00
dependabot[bot]
14c56af465 deps: bump net.kyori:adventure-platform-api from 4.3.2 to 4.3.3 (#311)
Bumps [net.kyori:adventure-platform-api](https://github.com/KyoriPowered/adventure-platform) from 4.3.2 to 4.3.3.
- [Release notes](https://github.com/KyoriPowered/adventure-platform/releases)
- [Commits](https://github.com/KyoriPowered/adventure-platform/compare/v4.3.2...v4.3.3)

---
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-06-04 20:19:41 +01:00
dependabot[bot]
bee01dd15a deps: bump net.kyori:adventure-platform-bukkit from 4.3.2 to 4.3.3 (#313)
Bumps [net.kyori:adventure-platform-bukkit](https://github.com/KyoriPowered/adventure-platform) from 4.3.2 to 4.3.3.
- [Release notes](https://github.com/KyoriPowered/adventure-platform/releases)
- [Commits](https://github.com/KyoriPowered/adventure-platform/compare/v4.3.2...v4.3.3)

---
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-06-04 19:37:43 +01:00
dependabot[bot]
e97551e67e deps: bump com.google.guava:guava from 33.2.0-jre to 33.2.1-jre (#312)
Bumps [com.google.guava:guava](https://github.com/google/guava) from 33.2.0-jre to 33.2.1-jre.
- [Release notes](https://github.com/google/guava/releases)
- [Commits](https://github.com/google/guava/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-04 19:37:26 +01:00
William
97023e8425 refactor: check that a dependency Plugin#isEnabled 2024-06-02 17:07:05 +01:00
William
4fa7106a46 refactor: gracefully handle missing data deps 2024-06-01 18:10:16 +01:00
William
e0b81e4c76 refactor: add serialization identifier dependencies for applying data (#309)
* refactor: add serialization identifier dependencies for applying data

* fix: correct issues with deterministic sync order

* refactor: adjust base data type dependencies

* refactor: cleanup imports/trim whitespace

* docs: Document Identifier dependencies

* feat: fix issues with health scaling
2024-06-01 15:35:08 +01:00
William
c4adec3082 refactor: make more resilient against invalid effect types
Spigot has a potion effect API lookup mismatch bug (SPIGOT-7674) due to the deprecated methods we use to support 1.17
2024-05-29 00:08:40 +01:00
William
107238360c build: bump to 3.5.3 2024-05-28 21:57:56 +01:00
William
6141adbdb9 fix: attribute base values not being applied
modifiers were being applied, but in cases where the base value was edited, this was not
2024-05-28 21:57:26 +01:00
dependabot[bot]
eaa2ed74a6 deps: bump com.github.retrooper.packetevents:spigot from 2.2.1 to 2.3.0 (#305)
Bumps com.github.retrooper.packetevents:spigot from 2.2.1 to 2.3.0.

---
updated-dependencies:
- dependency-name: com.github.retrooper.packetevents:spigot
  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-05-27 17:04:08 +01:00
William
44c652c452 Merge remote-tracking branch 'origin/master' 2024-05-27 01:52:24 +01:00
William
78cf6bff63 docs: add PacketEvents docs to config 2024-05-27 01:52:18 +01:00
William
8ad4158ec0 docs: document PacketEvents support 2024-05-27 01:52:08 +01:00
William
405e6d7162 deps: bump runtime dependencies 2024-05-27 01:50:27 +01:00
AlexDev_
cff1c8f982 feat: added PacketEvents support as ProtocolLib alternative (#296) 2024-05-27 01:46:44 +01:00
William
f43ca2f043 refactor: adjust BukkitKeyedAdapter logic, close #304 2024-05-27 01:43:47 +01:00
dependabot[bot]
3114ab1a62 deps: bump com.google.code.gson:gson from 2.10.1 to 2.11.0 (#302)
Bumps [com.google.code.gson:gson](https://github.com/google/gson) from 2.10.1 to 2.11.0.
- [Release notes](https://github.com/google/gson/releases)
- [Changelog](https://github.com/google/gson/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google/gson/compare/gson-parent-2.10.1...gson-parent-2.11.0)

---
updated-dependencies:
- dependency-name: com.google.code.gson:gson
  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-05-27 01:11:02 +01:00
dependabot[bot]
2da9749b0c deps: bump net.kyori:adventure-api from 4.16.0 to 4.17.0 (#300)
Bumps [net.kyori:adventure-api](https://github.com/KyoriPowered/adventure) from 4.16.0 to 4.17.0.
- [Release notes](https://github.com/KyoriPowered/adventure/releases)
- [Commits](https://github.com/KyoriPowered/adventure/compare/v4.16.0...v4.17.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>
2024-05-27 01:10:54 +01:00
dependabot[bot]
d4d510e100 --- (#303)
updated-dependencies:
- dependency-name: requests
  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-05-27 01:10:29 +01:00
Preva1l
550ea26097 fix: MongoDB duplicate user table entries & not updating cached username (#301) 2024-05-13 10:50:58 +01:00
Preva1l
2b1e72a42e fix: wrong type in PostgreSQL schema (#299)
* Started impl for mongo

* fix silly mistake with postgresql

* fix silly mistake with postgresql
2024-05-10 19:06:34 +01:00
William
75f8bee706 build: adjust version meta for release builds 2024-05-06 17:05:52 +01:00
William
a3b50a0bf5 fix: advancement messages being improperly canceled 2024-05-06 16:48:15 +01:00
dependabot[bot]
e9ab0909ce deps: bump com.google.guava:guava from 33.1.0-jre to 33.2.0-jre (#298)
Bumps [com.google.guava:guava](https://github.com/google/guava) from 33.1.0-jre to 33.2.0-jre.
- [Release notes](https://github.com/google/guava/releases)
- [Commits](https://github.com/google/guava/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 16:28:05 +01:00
dependabot[bot]
1e91b4b4ce build(deps): bump tqdm from 4.66.1 to 4.66.3 in /test (#297)
Bumps [tqdm](https://github.com/tqdm/tqdm) from 4.66.1 to 4.66.3.
- [Release notes](https://github.com/tqdm/tqdm/releases)
- [Commits](https://github.com/tqdm/tqdm/compare/v4.66.1...v4.66.3)

---
updated-dependencies:
- dependency-name: tqdm
  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-05-06 16:27:58 +01:00
William
043b51d812 test: update test suite to Paper 1.20.6 2024-05-01 12:35:19 +01:00
William
fa5cea2aa3 refactor: adjust way advancement messages are cleared, close #285 2024-05-01 12:30:18 +01:00
William
e35dcf3aad feat: Minecraft 1.20.5/6 support (#295)
* feat: start 1.20.5 update testing

nbt-api seems to work great already :)

* feat: add DFU support for legacy upgrade

Adds an optional overload to `deserialize` to support passing the MC Version of the snapshot data

* refactor: `clone` ItemStack[] bukkit data arrays, close #294

Don't perform async operations on mutable player data
2024-05-01 12:08:42 +01:00
IbanEtc
68ec79add6 locales: add French (fr-fr) locales, courtesy of IbanEtchep (#293)
* french translation

* locales: fix line spacing in `fr-fr`

* locales: credit French (fr-fr)

---------

Co-authored-by: William <will27528@gmail.com>
2024-04-23 16:56:45 +01:00
dependabot[bot]
70235963ba deps: bump org.apache.commons:commons-text from 1.11.0 to 1.12.0 (#291)
Bumps org.apache.commons:commons-text from 1.11.0 to 1.12.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>
2024-04-23 16:50:24 +01:00
William
245fbec80c fix: wrong check in legacy stats loading 2024-04-19 14:39:26 +01:00
Nhan Le
4d1a465c03 fix: add Settings packet to allow list (#287)
fix player skin and chunk loading actions get cancelled
2024-04-16 10:57:34 +01:00
dependabot[bot]
07dc0b8c12 deps: bump commons-io:commons-io from 2.16.0 to 2.16.1 (#284)
Bumps commons-io:commons-io from 2.16.0 to 2.16.1.

---
updated-dependencies:
- dependency-name: commons-io:commons-io
  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-04-15 14:23:47 +01:00
William
525f15e65b locales: correct typo in error_invalid_data, fix #283 2024-04-14 16:44:26 +01:00
William
017d26673a refactor: adjust imports 2024-04-13 15:23:05 +01:00
jhqwqmc
087c787ec2 locales: update zh-cn.yml (#281)
* Update zh-cn.yml

* Update zh-cn.yml
2024-04-13 02:05:59 +01:00
William
7218390f65 feat: Add support for Folia (#280)
* feat: Add support for Folia

* feat: fix folia advancement stuff

* feat: fix double negation (whoops)
2024-04-12 17:48:41 +01:00
William
bd312c48ea refactor: Improve data validation, allow deletion of invalid snapshots (#279)
* feat: move validation to be on unpack

* refactor: add validation and handling for invalid data to UX

* fix: `runAfter` not firing on unpack failure

* locales: minor update to `data_list_item_invalid`
2024-04-12 16:56:45 +01:00
dependabot[bot]
e4cc792f54 build(deps): bump idna from 3.4 to 3.7 in /test (#278)
Bumps [idna](https://github.com/kjd/idna) from 3.4 to 3.7.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst)
- [Commits](https://github.com/kjd/idna/compare/v3.4...v3.7)

---
updated-dependencies:
- dependency-name: idna
  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-04-12 16:53:28 +01:00
William
7941745ed0 docs: update config file docs 2024-04-12 15:21:03 +01:00
William
21f125c48a fix: bad cast in applyStat 2024-04-12 15:08:56 +01:00
William
18b8b958fe docs: update data snapshot API
Fix a few references to max health and update data types table
2024-04-11 19:22:21 +01:00
William
35c23c7970 refactor: remove some unused code, cleanup 2024-04-11 19:16:27 +01:00
William
4bb38f67d3 refactor: use registry for statistics
also updates BukkitKeyedAdapter methods to use the registries and moves to just using the Json serializer for Bukkit locations
2024-04-11 19:10:40 +01:00
William
98cf42065b refactor: slightly simplify checkout logic 2024-04-11 15:15:11 +01:00
William
328d4476aa docs: update sync features docs/faq 2024-04-11 14:55:11 +01:00
William
8293d767da feat: add config to skip certain attributes 2024-04-10 20:24:27 +01:00
William
7b8fb92737 fix: ICV modifying offline inventories via API, close #275 2024-04-10 20:16:09 +01:00
William
0f1cc2d24f docs: Attributes now hold max health 2024-04-10 19:39:49 +01:00
William
676ba7a10a feat: Add attribute syncing (#276)
* refactor: add attribute syncing

* fix: don't sync unmodified attributes

* fix: register json serializer for Attributes

* fix: improve Attribute API methods

* docs: update Sync Features

* refactor: make attributes a set

Because they're unique (by UUID)
2024-04-10 19:38:37 +01:00
William
82dc765f66 fix: NotNull-annotate PacketEvent 2024-04-10 17:55:40 +01:00
William
16cfbc9410 fix: remove debug messages 2024-04-10 17:55:13 +01:00
William
2b4c7e6c3d fix: missing ALLOWED_PACKETS calling desync 2024-04-10 17:54:33 +01:00
William
a03d540938 fix: DateTimeException when running /userdata 2024-04-10 17:54:15 +01:00
William
6bcb3e7908 fix: add flight_status to getData 2024-04-10 17:12:14 +01:00
William
facbda65a8 fix: ProtocolLib startup warnings 2024-04-10 17:11:24 +01:00
William
2f5ddf6164 feat: add ProtocolLib support for deeper-level packet cancellation (#274)
* feat: add support for ProtocolLib packet-level state cancelling

* refactor: move commands to event listener, document ProtocolLib support

* docs: make Setup less claustrophobic

* fix: remove `@Getter` on `PlayerPacketAdapter`

* build: add missing license headers

* fix: inaccessible method on Paper

* test: add ProtocolLib to network spin test

* fix: whoops I targeted the wrong packets

* fix: bad command disabled check logic

* fix: final protocollib adjustments
2024-04-10 17:09:53 +01:00
William
4dfbc0e32b fix: more gracefully handle platform mismatches 2024-04-10 15:41:23 +01:00
William
07d0376dd6 docs: fix grammar in config comment 2024-04-10 15:32:33 +01:00
William
d23ea087c1 refactor: Redis -> 𝓡𝓮𝓭𝓲𝓼 2024-04-10 14:41:45 +01:00
William
ea77f2d782 docs: update docs on getting flight status 2024-04-10 14:40:28 +01:00
William
ef3dc7e602 fix: bad null annotations on legacy conversion 2024-04-10 14:38:07 +01:00
William
3fe6245ddf docs: update README to reflect new DB support 2024-04-10 14:36:11 +01:00
William
a35e83a424 feat: Move flight status into its own data type, use lombok for data class (#273)
* refactor: use lombok, separate flight, close #191

* refactor: suppress some warnings

* refactor: suppress unused `from` warnings

* refactor: correct bad null-annotations on Items

* refactor: fix null annotation on `getStack`

* refactor: override methods for getting flight status

* docs: add deprecation docs for flight in gamemode data
2024-04-10 14:34:19 +01:00
William
be5d1128de docs: add note about Paper stats API 2024-04-07 12:48:04 +01:00
William
8463e1bb7a fix: Remove legacy MineDown calls 2024-04-07 12:47:08 +01:00
William
5456b232f0 fix: don't escape double underscores in text 2024-04-07 12:45:31 +01:00
William
b0e585841c refactor: use system locale for date formatting 2024-04-07 12:44:58 +01:00
William
cd298af5ae deps: bump deps, update MineDown, close #270 2024-04-07 12:40:04 +01:00
William
e19477aada refactor: remove redundant toString on debug 2024-04-07 12:35:45 +01:00
William
7f75b9a917 refactor: explicitly cancel ArmorStandManipulateEvent 2024-04-07 12:35:22 +01:00
William
819421492b Merge remote-tracking branch 'origin/master' 2024-04-07 12:35:06 +01:00
SnowCutieOwO
8f13a3955c docs: fix errors in API (#268)
* Update API.md

There's a little mistake and I found it and fixed. :)

* Update API.md

Fixed the wrong index
2024-04-07 12:33:10 +01:00
dependabot[bot]
73de0ff392 deps: bump commons-io:commons-io from 2.15.1 to 2.16.0 (#266)
Bumps commons-io:commons-io from 2.15.1 to 2.16.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-04-02 14:57:55 +01:00
William
93edb0de4c refactor: slightly adjust how quit cursor item dropping gets handled 2024-03-29 23:50:51 +00:00
dependabot[bot]
bb5ae0b741 deps: bump space.arim.morepaperlib:morepaperlib from 0.4.3 to 0.4.4 (#263)
Bumps space.arim.morepaperlib:morepaperlib from 0.4.3 to 0.4.4.

---
updated-dependencies:
- dependency-name: space.arim.morepaperlib:morepaperlib
  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-03-27 12:55:31 +00:00
dependabot[bot]
ccd7601a0e deps: bump org.projectlombok:lombok from 1.18.30 to 1.18.32 (#264)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.30 to 1.18.32.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.30...v1.18.32)

---
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-03-27 12:54:47 +00:00
dependabot[bot]
50d15e9580 deps: bump de.tr7zw:item-nbt-api from 2.12.2 to 2.12.3 (#265)
Bumps de.tr7zw:item-nbt-api from 2.12.2 to 2.12.3.

---
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-03-27 12:54:37 +00:00
dependabot[bot]
aa1e8b8e95 deps: bump com.google.guava:guava from 33.0.0-jre to 33.1.0-jre (#260)
Bumps [com.google.guava:guava](https://github.com/google/guava) from 33.0.0-jre to 33.1.0-jre.
- [Release notes](https://github.com/google/guava/releases)
- [Commits](https://github.com/google/guava/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-19 17:42:56 +00:00
William
3ff01f7bb3 build: bump to 3.4.1 2024-03-16 19:57:37 +00:00
William
93ab25bf44 deps: target ConfigLib on Maven Central 2024-03-16 19:57:06 +00:00
Preva1l
4c0addfd67 feat: PostgreSQL, Mongo Atlas & Replica Support (#255)
* Started impl for mongo

* added docs

* refactor of the mongo code, made mongodb artifacts download at run time, tested and working

* complete all change requests

* remove mongo and bson from relocations as they arnt needed

* changed the config

* updated docs

* not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null

* added postgres support (closes https://github.com/WiIIiam278/HuskSync/issues/212)

* add support for mongodb atlas, added atlas and postrgres to docs, update the config example in docs, also updates mongodb driver bc apparently i was special and very very out of data

* Rework how mongo connections are handled, **breaks config for mongo only**, allows for MongoDB Atlas, normal MongoDb AND MongoDB replica sets via the parameters in advanced mongo settings, added try and catch on all mongo operations so that it actually throws instead of a cutsie little warning

* small doc change

* whoops forgot to instantiate MongoCollectionHelper, and added missing step from docs for atlas users

* why thats a tad embarrassing (grammar mistake)

* add cluster id to `/husksync status`, shows "MongoDB Atlas" in status if using mongodb atlas

---------

Co-authored-by: William <will27528@gmail.com>
2024-03-16 12:50:26 +00:00
SnowCutieOwO
b77cf2524d build: fix grgit failsafe being unreachable (#258)
* Update build.gradle

* Update build.gradle

* Update build.gradle
2024-03-13 09:59:48 +00:00
dependabot[bot]
501ea3f609 deps: bump org.json:json from 20240205 to 20240303 (#254)
Bumps [org.json:json](https://github.com/douglascrockford/JSON-java) from 20240205 to 20240303.
- [Release notes](https://github.com/douglascrockford/JSON-java/releases)
- [Changelog](https://github.com/stleary/JSON-java/blob/master/docs/RELEASES.md)
- [Commits](https://github.com/douglascrockford/JSON-java/commits)

---
updated-dependencies:
- dependency-name: org.json:json
  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-03-05 18:49:21 +00:00
William278
a93af95fd2 docs: update README 2024-03-02 15:58:18 +00:00
William278
39767c5cd0 build: bump to 3.4 2024-03-02 15:54:22 +00:00
William278
48f7037898 fix: update license headers 2024-03-02 15:52:54 +00:00
Preva1l
67dddf0cfa feat: Add support for MongoDB data storage (#250)
* Started impl for mongo

* added docs

* refactor of the mongo code, made mongodb artifacts download at run time, tested and working

* complete all change requests

* remove mongo and bson from relocations as they arnt needed

* changed the config

* updated docs

* not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null not null

---------

Co-authored-by: William <will27528@gmail.com>
2024-03-02 15:47:36 +00:00
William
eeb5e57c1e fix: shutdown not clearing cached data 2024-02-28 23:19:37 +00:00
dependabot[bot]
5a6ea2cffe deps: bump com.github.Exlll.ConfigLib:configlib-yaml (#251)
Bumps [com.github.Exlll.ConfigLib:configlib-yaml](https://github.com/Exlll/ConfigLib) from v4.4.0 to v4.5.0.
- [Release notes](https://github.com/Exlll/ConfigLib/releases)
- [Commits](https://github.com/Exlll/ConfigLib/compare/v4.4.0...v4.5.0)

---
updated-dependencies:
- dependency-name: com.github.Exlll.ConfigLib:configlib-yaml
  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-02-27 13:45:37 +00:00
dependabot[bot]
07ddd34f8e deps: bump net.kyori:adventure-api from 4.15.0 to 4.16.0 (#252)
Bumps [net.kyori:adventure-api](https://github.com/KyoriPowered/adventure) from 4.15.0 to 4.16.0.
- [Release notes](https://github.com/KyoriPowered/adventure/releases)
- [Commits](https://github.com/KyoriPowered/adventure/compare/v4.15.0...v4.16.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>
2024-02-27 13:45:18 +00:00
dependabot[bot]
a0b86c298f deps: bump org.ajoberstar.grgit from 5.2.1 to 5.2.2 (#247)
Bumps [org.ajoberstar.grgit](https://github.com/ajoberstar/grgit) from 5.2.1 to 5.2.2.
- [Release notes](https://github.com/ajoberstar/grgit/releases)
- [Commits](https://github.com/ajoberstar/grgit/compare/5.2.1...5.2.2)

---
updated-dependencies:
- dependency-name: org.ajoberstar.grgit
  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-02-19 13:41:49 +00:00
William
6fbef032bc locales: update zh-tw by lin_ak90 2024-02-17 17:37:05 +00:00
William
318aacd432 refactor: minor tidy up 2024-02-17 15:48:09 +00:00
Timon Michel
ba1b2ff62e fix: improve event cancellation logic for better plugin compat (#246) 2024-02-17 15:43:32 +00:00
William278
67ef4888da fix: death save updating player 2024-02-17 14:55:19 +00:00
William278
a5d3015c6e feat: allow customizable save / update causes 2024-02-13 16:23:33 +00:00
William278
131a364f53 fix: cache not cleared on /userdata delete, close #245 2024-02-13 14:38:19 +00:00
William
19636d9447 refactor: optimize imports 2024-02-12 17:51:54 +00:00
William
f803a0b57b refactor: revert keys change 2024-02-12 17:51:40 +00:00
William
28afffe95e refactor/redis: use scan instead of keys 2024-02-12 17:19:05 +00:00
dependabot[bot]
c7e100a78a deps: bump org.json:json from 20231013 to 20240205 (#244)
Bumps [org.json:json](https://github.com/douglascrockford/JSON-java) from 20231013 to 20240205.
- [Release notes](https://github.com/douglascrockford/JSON-java/releases)
- [Changelog](https://github.com/stleary/JSON-java/blob/master/docs/RELEASES.md)
- [Commits](https://github.com/douglascrockford/JSON-java/commits)

---
updated-dependencies:
- dependency-name: org.json:json
  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-02-12 10:02:57 +00:00
William
12e223618d refactor: data save event order processing, use new method in DataSyncer (#243)
* fix: fire DataSaveEvent before disconnect

* fix: revert rename `addSnapshot`

* docs: mention `addSnapshot` firing the API event

* refactor: use DataSyncer method for event saving, close #242

* fix: trailing semicolon
2024-02-11 15:37:03 +00:00
William278
f6773f4e68 build: bump to 3.3.2 2024-02-11 14:27:04 +00:00
William278
b9434a56e8 refactor: minor Bukkit platform refactors 2024-02-11 14:26:48 +00:00
William
325fac41bf deps: bump junit to 5.10.2 2024-02-05 15:38:44 +00:00
William
87377bffc1 docs: update FAQs 2024-02-05 10:38:00 +00:00
William
c6fb7fb10f fix: preserve order of saved items to keep, close #186 2024-02-02 23:01:41 +00:00
William
c2ae9bd20a build: bump to 3.3.1 2024-02-02 22:24:47 +00:00
William
e580c4f2bd fix: LOCKSTEP preventing offline inv updates, close #229 2024-02-02 22:24:27 +00:00
dependabot[bot]
dabd9bc57d ci: bump gradle/gradle-build-action from 2 to 3 (#235)
Bumps [gradle/gradle-build-action](https://github.com/gradle/gradle-build-action) from 2 to 3.
- [Release notes](https://github.com/gradle/gradle-build-action/releases)
- [Commits](https://github.com/gradle/gradle-build-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: gradle/gradle-build-action
  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-01-31 09:45:44 +00:00
William
fa7f6f0d6e fix: exception when reading server defaults 2024-01-26 14:55:09 +00:00
William
267cf1ff35 fix: wrong URL on startup exception 2024-01-26 13:57:52 +00:00
William
08944ffd35 refactor: update a few config comments 2024-01-26 13:48:46 +00:00
William
c75114b858 deps: bump ConfigLib to 4.3.0 2024-01-26 13:40:46 +00:00
William
350a8b864d fix: bad ConfigProvider logic 2024-01-26 00:00:18 +00:00
William278
df0bd7a7cb refactor: actually don't use lombok for API just yet 2024-01-25 15:46:07 +00:00
William278
9fc9e8caf4 refactor: use lombok in a few other places 2024-01-25 15:44:36 +00:00
William278
2e3db2fffa refactor: use Guava methods in various places 2024-01-25 15:42:30 +00:00
William
530b3ef24d refactor: Migrate from BoostedYaml to Exll's ConfigLib (#233)
* feat: start work on moving to Exll's configlib

* refactor: Fully migrate to Exlll's configlib

* refactor: Optimize imports
2024-01-25 15:37:04 +00:00
William278
a9bd4dd2f0 build: stop trying to be clever with gradle publishing
if 'i aint readin allat' was a build scripting language
2024-01-24 23:34:47 +00:00
William278
85706d97c5 refactor: move unregister to common API module 2024-01-24 23:32:35 +00:00
William278
f7e3104e6b build: remove unnecessary "plugin" module 2024-01-24 23:30:39 +00:00
William278
f56d7f6113 docs: Fix API platforms section typo 2024-01-24 23:26:14 +00:00
William278
685431a40d api: add cross-platform API support 2024-01-24 23:25:37 +00:00
William278
9da3ff5281 build: Start minimizing built jars 2024-01-24 23:11:59 +00:00
William278
24453d0e1a build: Require Java 17, Minecraft 1.17.1 2024-01-24 23:06:25 +00:00
William278
280e90e297 refactor: use guard clause in thread unlock logic 2024-01-24 23:00:41 +00:00
Rubén
31920d056d refactor: Reconnect to Redis when connection lost (#230)
* Add redis reconnection

* Add separated method to handle thread unlock

* Add reconnection time constant
2024-01-22 12:53:56 +00:00
William278
6641e11fd9 fix: high latency redis environments firing data updates twice 2024-01-20 17:30:22 +00:00
William278
66bbde0b5d command: update translator credits in AboutMenu 2024-01-19 16:33:56 +00:00
WinTone01
7dde6423e4 Update tr-tr.yml (#228) 2024-01-19 16:32:16 +00:00
William278
0eac12e3f8 locales: Add id-id, courtesy of Wirayuda5620 2024-01-18 19:17:56 +00:00
Wirayuda5620
5df58e4ef9 Update HuskSyncCommand.java AboutMenu
hehe 😋
2024-01-19 00:36:59 +07:00
Wirayuda5620
4a6583d8bd Indonesian translation for HuskSync 2024-01-19 00:34:52 +07:00
jhqwqmc
059ee6f660 locales: Update zh-cn.yml (#224)
Correction
2024-01-13 13:06:58 +00:00
William
414246f243 fix: Handle Bukkit objects that don't fully implement Keyed 2023-12-26 14:57:40 +00:00
William
a3e269c00b docs: document /husksync status 2023-12-26 14:42:28 +00:00
William
bf9f29ffe9 refactor: Improve display of /husksync status 2023-12-26 14:41:39 +00:00
William
29bd2e1319 feat: Add /husksync status report menu 2023-12-26 14:28:41 +00:00
William
2475a9b3c6 docs: Fix license headers 2023-12-26 12:49:07 +00:00
William
2a52cc9086 ci: bump github-wiki-action to v4 2023-12-26 12:42:59 +00:00
William
237abf9698 deps: bump adventure-platform to 4.3.2 2023-12-26 12:41:24 +00:00
William
adbc264532 Merge remote-tracking branch 'origin/master' 2023-12-26 12:41:20 +00:00
jhqwqmc
f9cfec7d03 Update zh-cn.yml (#220) 2023-12-26 12:40:27 +00:00
William
29805bfe04 docs: bump to 3.2.1 2023-12-26 12:39:42 +00:00
William
8d2e5a6a52 fix: Enum#valueOf throwing on legacy stat-map conversion 2023-12-26 12:39:24 +00:00
William
d4f61bd646 refactor: catch Throwable, not Exception 2023-12-26 12:38:07 +00:00
William
55173be04b docs: More on updated default sync mode 2023-12-21 19:11:56 +00:00
William
e7078c9542 docs: Document updated default sync mode 2023-12-21 19:04:22 +00:00
William
2aa33b2f2c fix: Improve accuracy of max health syncing #148 2023-12-21 18:30:40 +00:00
William
972fee1bc7 fix: Fix flight syncing sometimes failing, close #206 2023-12-21 17:34:01 +00:00
William
efe34977b5 ci: Use wiki-action@v3 2023-12-21 17:31:25 +00:00
William
02ed9687ee deps: Bump runtime dependencies 2023-12-21 17:31:22 +00:00
William
08889a1739 docs: Bump to 3.2 (Redis key protocol changes) 2023-12-21 17:01:56 +00:00
William
9cf6d1eab6 refactor: change default sync mode to LOCKSTEP 2023-12-21 17:01:34 +00:00
William
33c2eb2237 refactor: Use cloud for server for HuskHomes consistency 2023-12-21 15:57:58 +00:00
William
299586aa86 refactor: Rename DATA_UPDATE -> LATEST_SNAPSHOT 2023-12-21 15:53:56 +00:00
William
05c988f2c7 refactor: Extend DATA_UPDATE Redis cache time on LOCKSTEP mode 2023-12-21 15:50:35 +00:00
William
8e0ad76968 refactor: Improve getUserCheckedOut debug log 2023-12-21 15:06:17 +00:00
William
4db162e78f refactor: Even more minor debug logging tweaks 2023-12-21 15:02:36 +00:00
William
272bc1278a refactor: More minor debug logging tweaks 2023-12-21 15:01:46 +00:00
William
35fdcf7106 refactor: Further improve debug log messages 2023-12-21 14:59:15 +00:00
William
48e087a3d7 refactor: Improve debug log wording for getUserCheckedOut 2023-12-21 14:55:49 +00:00
William
ca000197e4 refactor: Further improvements to debug messages 2023-12-21 14:50:22 +00:00
William
a6bab88cee refactor: Add debug log for listenForRedis timeout 2023-12-21 14:30:14 +00:00
William
f0c64df439 refactor: Improve debug logging messages 2023-12-21 14:25:38 +00:00
William
ac5ab56717 fix: Don't wrap saveUserData in runAsync twice 2023-12-21 13:24:52 +00:00
William
c2025350ba fix: Optimize imports 2023-12-19 22:06:29 +00:00
William
4c2bb5c6df fix: Get correct platform Audience for OnlineUsers 2023-12-19 22:06:13 +00:00
William
fb069296e1 refactor: Use native adventure implementation on Paper 2023-12-19 22:03:24 +00:00
Roman Alexander
22eedc8522 feat: Add support for Redis Sentinels (#216)
* Add support for Redis Sentinels

* Add some comments
2023-12-19 19:27:03 +00:00
William278
664c8c3352 Bump to 3.1.3 2023-12-12 13:30:00 +00:00
William
e7e6f9cfa7 test: bump test suite to 1.20.4 2023-12-10 17:02:18 +00:00
William278
5ec0f1b098 Support MC 1.20.4, improve timestamp exceptions 2023-12-10 15:33:38 +00:00
William
8fad075357 Bump to 3.1.2 2023-12-10 00:54:29 +00:00
William
83e27cca83 locales: Add Korean (ko-kr) courtesy of cada3141 2023-12-10 00:53:21 +00:00
William
729230a646 ci: Update deps, tidy workflow files 2023-12-10 00:51:11 +00:00
Joo200
029407613f locales: add new localization to de-de (#215) 2023-12-09 23:32:24 +00:00
Daniil Nartsissov
3d6ff7c30b Save cause localization support (#214) 2023-12-03 13:40:05 +00:00
Daniil Nartsissov
5833ce955f locales: add ru-ru localization (#211) 2023-12-02 19:05:33 +00:00
dependabot[bot]
b3a5091828 Bump commons-io:commons-io from 2.15.0 to 2.15.1 (#209)
Bumps commons-io:commons-io from 2.15.0 to 2.15.1.

---
updated-dependencies:
- dependency-name: commons-io:commons-io
  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>
Co-authored-by: William <will27528@gmail.com>
2023-12-02 15:18:45 +00:00
William
693209ff00 docs: v3 instead of v2 in MPDB migrator page 2023-12-02 15:18:16 +00:00
dependabot[bot]
5d1bd7c3a9 Bump org.jetbrains:annotations from 24.0.1 to 24.1.0 (#208)
Bumps [org.jetbrains:annotations](https://github.com/JetBrains/java-annotations) from 24.0.1 to 24.1.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.0.1...24.1.0)

---
updated-dependencies:
- dependency-name: org.jetbrains:annotations
  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>
2023-11-24 11:05:40 +00:00
WinTone01
7b8c75dbeb Create tr-tr.yml (#207) 2023-11-12 12:39:15 +00:00
William
b7a30bd6e9 [ci skip] update readme 2023-11-10 10:20:25 +00:00
William
2daf5fedef docs: Add BuiltByBit to sidebar 2023-11-09 18:32:09 +00:00
William
5fd40915d0 [ci skip] docs: Add builtbybit to README 2023-11-09 18:30:17 +00:00
dependabot[bot]
c49700e9ec Bump org.junit.jupiter:junit-jupiter-params from 5.10.0 to 5.10.1 (#205)
Bumps [org.junit.jupiter:junit-jupiter-params](https://github.com/junit-team/junit5) from 5.10.0 to 5.10.1.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.10.0...r5.10.1)

---
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>
2023-11-07 14:13:19 +00:00
William
0f35331441 Bump runtime dependencies 2023-11-06 12:43:50 +00:00
dependabot[bot]
0153e14ce5 Bump org.junit.jupiter:junit-jupiter-engine from 5.10.0 to 5.10.1 (#202)
Bumps [org.junit.jupiter:junit-jupiter-engine](https://github.com/junit-team/junit5) from 5.10.0 to 5.10.1.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.10.0...r5.10.1)

---
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>
2023-11-06 12:40:49 +00:00
dependabot[bot]
419434bdca Bump org.junit.jupiter:junit-jupiter-api from 5.10.0 to 5.10.1 (#201)
Bumps [org.junit.jupiter:junit-jupiter-api](https://github.com/junit-team/junit5) from 5.10.0 to 5.10.1.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.10.0...r5.10.1)

---
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>
2023-11-06 12:39:18 +00:00
dependabot[bot]
f1be4d2d88 Bump de.tr7zw:item-nbt-api from 2.12.0 to 2.12.1 (#200)
Bumps de.tr7zw:item-nbt-api from 2.12.0 to 2.12.1.

---
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>
2023-11-06 12:39:03 +00:00
dependabot[bot]
c973dc5f05 Bump com.zaxxer:HikariCP from 5.0.1 to 5.1.0 (#199)
Bumps [com.zaxxer:HikariCP](https://github.com/brettwooldridge/HikariCP) from 5.0.1 to 5.1.0.
- [Changelog](https://github.com/brettwooldridge/HikariCP/blob/dev/CHANGES)
- [Commits](https://github.com/brettwooldridge/HikariCP/compare/HikariCP-5.0.1...HikariCP-5.1.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>
2023-11-06 12:38:17 +00:00
dependabot[bot]
b530941687 Bump dev.triumphteam:triumph-gui from 3.1.6 to 3.1.7 (#197) 2023-11-03 01:23:56 +00:00
Ceddix
c09fde4c36 Update German (de-de) locales, fix broken link in README (#196)
* updated the locales url

* updated German translation
2023-11-02 20:30:33 +00:00
dependabot[bot]
8d3beab145 Bump commons-io:commons-io from 2.14.0 to 2.15.0 (#193)
Bumps commons-io:commons-io from 2.14.0 to 2.15.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>
2023-11-02 14:48:18 +00:00
dependabot[bot]
cdf666bde6 Bump org.apache.commons:commons-text from 1.10.0 to 1.11.0 (#194)
Bumps org.apache.commons:commons-text from 1.10.0 to 1.11.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>
2023-11-02 14:46:53 +00:00
William
350528e394 docs: add note about persisting data via the DataSaveEvent 2023-11-02 14:46:32 +00:00
dependabot[bot]
a1d3e5fddc Bump org.ajoberstar.grgit from 5.2.0 to 5.2.1 (#192)
Bumps [org.ajoberstar.grgit](https://github.com/ajoberstar/grgit) from 5.2.0 to 5.2.1.
- [Release notes](https://github.com/ajoberstar/grgit/releases)
- [Commits](https://github.com/ajoberstar/grgit/compare/5.2.0...5.2.1)

---
updated-dependencies:
- dependency-name: org.ajoberstar.grgit
  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>
2023-10-25 01:57:35 +01:00
William
e096e58c45 Bump to 3.1.1, bump jedis to 5.0.1 2023-10-19 00:15:44 +01:00
William
75eafe57e2 [ci skip] Fix API badge on README 2023-10-18 19:04:10 +01:00
dependabot[bot]
0005392cd3 Bump urllib3 from 2.0.6 to 2.0.7 in /test (#188)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.6 to 2.0.7.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.0.6...2.0.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-17 22:06:35 +01:00
dependabot[bot]
93913ca4ef Bump org.json:json from 20230618 to 20231013 (#187)
Bumps [org.json:json](https://github.com/douglascrockford/JSON-java) from 20230618 to 20231013.
- [Release notes](https://github.com/douglascrockford/JSON-java/releases)
- [Changelog](https://github.com/stleary/JSON-java/blob/master/docs/RELEASES.md)
- [Commits](https://github.com/douglascrockford/JSON-java/commits)

---
updated-dependencies:
- dependency-name: org.json:json
  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>
2023-10-16 15:58:16 +01:00
William
aa09639e55 Fix persisted locked map banner rotation 2023-10-15 14:46:56 +01:00
William278
b205643fdd Fix duplicate cursor creation, close #185 2023-10-15 12:56:49 +01:00
Missing_Love
6fc827dedf Update zh-cn.yml (#184) 2023-10-14 01:14:32 +01:00
Missing_Love
b8aa1d9701 Update zh-cn.yml (#183)
Update Chinese Simplified Translation
2023-10-14 00:07:22 +01:00
William
2db3bb313f docs: Add v4.9.2 MPDB converter warning 2023-10-13 17:00:01 +01:00
William
4d23377a18 Ensure #setContents always sets correct size 2023-10-11 19:02:46 +01:00
William
51116cbdfb docs: Minor updates to links/legacy migration 2023-10-10 16:38:29 +01:00
dependabot[bot]
6831ce094d Bump dev.triumphteam:triumph-gui from 3.1.5 to 3.1.6 (#181) 2023-10-10 02:20:44 +01:00
William
289227e763 locales: Remove redundant comments in Japanese 2023-10-10 00:25:18 +01:00
William
3b8a9e4ed1 locales: Fix Dutch filename 2023-10-10 00:24:59 +01:00
William
7db3ed678f Paper plugin support, save player itemsToKeep rather than drops if not empty (#179)
* Paper plugin support, save itemsToKeep if present, close #172

* Fixup wrong packages, suppress a warning

* Update docs, add settings for death saving, reorganise config slightly

* Improve default server name lookup

* docs: Add note on Unsupported Versions

* docs: Minor Sync Modes tweaks
2023-10-10 00:10:06 +01:00
William
6d9e68a65b sync: Terminate syncer connection before redis 2023-10-08 23:02:27 +01:00
William
2c33f3b0b4 stats: Add exception handling for legacy materials 2023-10-08 23:01:34 +01:00
William
c002d86fc0 Persist extra-terrestrial locked maps to disk (#180)
* Persist extra-terrestrial maps to disk, close #157

* Fix issue writing map file caches

* Don't bother synchronously rendering maps
2023-10-08 12:10:26 +01:00
William
a384de8e42 Update docs and bump test suite to 1.20.1 2023-10-05 19:12:21 +01:00
William
cae17f6e68 Introduce new lockstep syncing system, modularize sync modes (#178)
* Start work on modular sync systems

* Add experimental lockstep sync system, close #69

* Refactor RedisMessageType enum

* Fixup lockstep syncing

* Bump to 3.1

* Update docs with details about the new Sync Modes

* Sync mode config key is `mode` instead of `type`

* Add server to data snapshot overview

* API: Add API for setting data syncers

* Fixup weird statistic matching logic
2023-10-05 18:05:02 +01:00
dependabot[bot]
03ca335293 Bump urllib3 from 2.0.4 to 2.0.6 in /test (#177) 2023-10-03 10:47:34 +01:00
Arno Keesman
c2b9e6c932 add Dutch translation (#176) 2023-10-02 20:58:28 +01:00
dependabot[bot]
518853c921 Bump commons-io:commons-io from 2.13.0 to 2.14.0 (#174)
Bumps commons-io:commons-io from 2.13.0 to 2.14.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>
Co-authored-by: William <will27528@gmail.com>
2023-10-02 15:01:18 +01:00
dependabot[bot]
fe9dda31bd Bump net.kyori:adventure-platform-bukkit from 4.3.0 to 4.3.1 (#175)
Bumps [net.kyori:adventure-platform-bukkit](https://github.com/KyoriPowered/adventure-platform) from 4.3.0 to 4.3.1.
- [Release notes](https://github.com/KyoriPowered/adventure-platform/releases)
- [Commits](https://github.com/KyoriPowered/adventure-platform/compare/v4.3.0...v4.3.1)

---
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>
2023-10-02 14:59:58 +01:00
Galen Huang
0fd29bca57 Fixed an error in stats map caused by modded block/item being null (#171) 2023-09-26 10:33:37 +01:00
Namiu/うにたろう
37a671dae9 Update ja-jp.yml (#170) 2023-09-26 10:33:24 +01:00
William
c406f40898 Bump to 3.0.2 2023-09-23 23:34:46 +01:00
William
7561762c25 Fix relocation of com.fatboyindustrial lib 2023-09-23 22:27:12 +01:00
William
d245245083 Fix #get call when appling locked map data, Fix #169 2023-09-23 18:45:06 +01:00
William
2b55e129b3 Slightly improve BukkitData.Items#setContents method 2023-09-23 15:15:10 +01:00
William
0caec74436 Improve stat map resilience for modded block types 2023-09-23 14:08:53 +01:00
William
55e443cd49 Improve error handling on data sync 2023-09-22 22:07:31 +01:00
William
b63e1bd283 Fixup adapting health when scaling 2023-09-22 21:48:39 +01:00
William
575122e6dd Tweak max health syncing calculation, add config option 2023-09-22 21:47:05 +01:00
William
856cbb9caa Allow conversion of v1-v3 data snapshots 2023-09-22 21:27:11 +01:00
William
7034a97d3a Fix wrong timestamp/UUID being used for legacy conversion (#167)
* Maintain legacy snapshot IDs when updating

* Also maintain timestamps during conversion

* Actually implement timestamp fix in LegacyConverter
2023-09-22 16:12:08 +01:00
William
635edb930f Fix wrong syntax message on /userdata restore, Close #166 2023-09-22 13:26:31 +01:00
dependabot[bot]
1d3e4b7a20 Bump de.tr7zw:item-nbt-api from 2.12.0-SNAPSHOT to 2.12.0 (#165)
Bumps de.tr7zw:item-nbt-api from 2.12.0-SNAPSHOT to 2.12.0.

---
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>
2023-09-22 09:45:40 +01:00
William278
6f9abb63cc docs: warn against joining futures 2023-09-21 20:27:55 +01:00
dependabot[bot]
563b9e146e Bump de.tr7zw:item-nbt-api from 2.12.0-RC1 to 2.12.0-SNAPSHOT (#164)
Bumps de.tr7zw:item-nbt-api from 2.12.0-RC1 to 2.12.0-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>
2023-09-21 08:30:08 +01:00
Villag3r_
7c9ac37eb7 Update it-it.yml (#163) 2023-09-20 17:11:56 +01:00
William278
5d3de4115f docs: Update links to Data Snapshot API 2023-09-20 14:14:52 +01:00
William
105f65c93a v3.0: New modular, more compatible data format, new API, better UX (#160)
* Start work on v3

* More work on task scheduling

* Add comment to notification display slot

* Synchronise branches

* Use new HuskHomes-style task system

* Bump to 2.3

* Remove HuskSyncInitializationException.java

* Optimise database for MariaDB

* Update libraries, move some around

* Tweak command registration

* Remove dummyhusksync

* Fixup core synchronisation logic to use new task system

* Implement new event dispatch subsystem

* Remove last remaining future calls

* Remove `Event#fire()`

* Refactor startup process

* New command subsystem, more initialization improvements, locale fixes

* Update docs, tweak command perms

* Reduce task number during data setting

* add todo

* Start work on data format / serialization refactor

* More work on Bukkit impl

* More serialization work

* Fixes to serialization, data preview system

* Start legacy conversion skeleton

* Handle setting empty inventories

* Start on-the-fly legacy conversion work

* Add advancement conversion

* Rewrite advancement get / apply logic

* Start work on locked map persistence

* More map persistence work

* More work on map serialization

* Move around persistence logic

* Add testing suite

* Fix item synchronisation

* Finalize more reliable locked map persistence

* Remove deprecated method call

* remove sync feature enum

* Fix held item slot syncing

* Make data types modular and API-extensible

* Remove some excessive debugging, minor refactor

* Fixup date formatting, improve menu UIs

* Finish up legacy data converting

* Null safety in item stack serializaiton

* Fix relocation of nbtapi, update dumping docs

* Add v1/MPDB Migrators back in

* Fix pinning/unpinning data not working

* Consumer instead of Function for editing data

* Show file size in DataSnapshotOverview

* Fix getIdentifier always returning empty

* Re-add items and inventory GUI commands

* Improve config file, fixup data restoration

* Add min time between backups (more useful backups!)

* More work on backups

* Fixup backup rotation frequency

* Remove stdout debug print in `#getEventPriority`

* Improve sync complete locale logic, fix synchronization spelling

* Remove `static` on exception

* Use dedicated thread for Redis, properly unsubscribe

* Refactor `player` package -> `user`

* `PlayerDataHolder` -> `UserDataHolder`

* Make `StatisticsMap` public, but `@ApiStatus.Internal`

* Suppress unused warnings on `Data`

* Add option to disable Plan hook

* Decompress legacy data before converting

* Decompress bytes in fromBytes

* Check permission node before serving TAB suggestions

* Actually convert legacy item stack data

* Fix syntax errors

* Minor method refactor in items command

* Fixup case-sensitive parsing in HuskSync command

* Start API work

* More work on API, fix potion effects

* Fix cross-server, config formatting for auto-pinned issue

* Fix confusion with UserData command, update docs images

* Update commands docs

* More docs updating

* Fix sync feature enabled/disabled checking logic

* Fix `#isCustom()`

* Enable persistent_data syncing by default

* docs: update Sync-Features config snippet

* docs: correct typo in Sync Features

* More API work

* bukkit: slightly optimized schedulers

* More API work, various refactorings

* docs: Start new API docs

* bump dependencies

* Add some basic unit tests

* docs: Correct typos

* More docs work, annotate DB methods as `@Blocking`

* Encapsulate `RedisMessage`, minor optimisations

* api: Simplify `#getCurrentData`

* api: Simplify `editCurrentData`, using `ThrowingConsumers` for better error handling

* docs: More Data Snapshot API documenting

* docs: add TOC to Data Snapshot API page

* bukkit: Make data types extend BukkitData

* Move where custom data is stored, finish up Custom Data API docs

* Optimise imports

* Fix `data_manager_advancements_preview_remaining` locale

* Fix advancement and playtime previews

* Fix potion effect deserialization

* Make snapshot_backup_frequency default to 4, more error handling/logging

* docs: Add ToC to Custom Data API

* docs: Minor legacy API tweaks

* Remove some unneeded catch logic

* Suppress a few warnings

* Fix Effect constructor being supplied in wrong order
2023-09-20 14:02:26 +01:00
William
9018ad02e1 settings: Fix wrong comment 2023-09-09 09:51:00 +03:00
rktfier
9db9a3e721 docs: Improve troubleshooting information for Redis/MySQL on Pterodactyl (#158) 2023-08-29 13:38:33 +01:00
William
9120c062de docs: correct mclo.gs link 2023-08-12 19:57:34 +01:00
William
bd83c8935d Tweak database connection confirmation messages 2023-07-28 21:38:10 +01:00
William
62095364ce 2.2.8: Explicitly specify MariaDB Driver class name 2023-07-28 21:25:23 +01:00
William
304df9984c [ci skip] Bump to 2.2.7 2023-07-28 19:44:09 +01:00
William
b73de81519 Add missing Maria schema 2023-07-28 19:41:42 +01:00
William
12e882fe22 v2.2.6: Crafting inventory safety, Maria v11 support (#153)
* Clear player inventory crafting slots on sync

* Bundle Maria driver for v11 support
2023-07-28 16:50:52 +01:00
William
4ed8b94d55 docs: explicitly state compatibility with custom item plugins 2023-07-28 13:58:12 +01:00
dependabot[bot]
854bf37186 Bump org.junit.jupiter:junit-jupiter-api from 5.9.3 to 5.10.0 (#149)
Bumps [org.junit.jupiter:junit-jupiter-api](https://github.com/junit-team/junit5) from 5.9.3 to 5.10.0.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.9.3...r5.10.0)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-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>
2023-07-24 19:38:08 +01:00
dependabot[bot]
8bab2a8123 Bump org.junit.jupiter:junit-jupiter-engine from 5.9.3 to 5.10.0 (#150)
Bumps [org.junit.jupiter:junit-jupiter-engine](https://github.com/junit-team/junit5) from 5.9.3 to 5.10.0.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.9.3...r5.10.0)

---
updated-dependencies:
- dependency-name: org.junit.jupiter:junit-jupiter-engine
  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>
2023-07-24 19:37:29 +01:00
Joo200
97ad608d56 Add mariadb protocol option type (#145)
Co-authored-by: William <will27528@gmail.com>
2023-07-01 13:54:30 +01:00
William
f7419f7277 [ci skip] Update README footer 2023-07-01 13:43:54 +01:00
William
f0497f61f0 [ci skip] Update README headings 2023-07-01 13:43:06 +01:00
William
f6aab54d4d license: Relicense under Apache-2.0 2023-07-01 13:39:48 +01:00
kFor
c306d700ce Option to blacklist all commands (#138)
* Option to blacklist all commands

* blacklist all commands by default

---------

Co-authored-by: William <will27528@gmail.com>
2023-06-22 11:25:17 +01:00
Rafael Romão
bbcb091daf Fix locked map data saving (#140) 2023-06-21 10:45:17 +01:00
William
eb9e2491e5 docs: Fix songoda links 2023-06-11 13:13:23 +01:00
William
0250ad80c8 docs: Use new repo 2023-06-11 13:08:23 +01:00
William
516b163e07 use AndJam 1.0.2 for now. 2023-06-09 10:22:10 +01:00
William
c123b15708 Bump AndJam to 1.0.3 2023-06-09 10:19:42 +01:00
William
29f8d8bf50 Bump dependencies 2023-06-09 10:12:21 +01:00
dependabot[bot]
4f65cf49ef Bump commons-io:commons-io from 2.11.0 to 2.12.0 (#134)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: William <will27528@gmail.com>
2023-05-28 10:28:38 +01:00
William
27f9e24dfb [ci skip] Fix missing closing perenthesis 2023-05-17 17:01:35 +01:00
William
2cbe39f158 [ci skip] Update songoda link to craftaro 2023-05-17 17:01:09 +01:00
William
5840f61571 Fix tests 2023-05-07 23:17:23 +01:00
William
6bd12dc000 Update DesertWell to 2.0.2, improve adventure API usage 2023-05-07 23:15:50 +01:00
William
1afbd3753a Merge remote-tracking branch 'origin/master'
# Conflicts:
#	.github/workflows/java_ci.yml
#	build.gradle
2023-05-07 23:02:05 +01:00
William
38a063420b Add license headers 2023-05-07 23:00:12 +01:00
William
0fae3484a1 Add workflow files, test reporting, maven publishing, docs, bump version 2023-05-07 22:59:19 +01:00
William
7bb4bff485 Make case conversion operations use the English locale 2023-05-07 22:50:37 +01:00
William
c5f5cd702e Update gradle-build-action 2023-05-01 14:58:10 +01:00
dependabot[bot]
3ced2cc0af Bump org.junit.jupiter:junit-jupiter-api from 5.9.2 to 5.9.3 (#128)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-01 14:56:48 +01:00
dependabot[bot]
73e1840574 Bump org.junit.jupiter:junit-jupiter-engine from 5.9.2 to 5.9.3 (#129)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: William <will27528@gmail.com>
2023-05-01 13:50:40 +01:00
dependabot[bot]
d1ca56af1f Bump dev.triumphteam:triumph-gui from 3.1.4 to 3.1.5 (#130)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-01 13:50:25 +01:00
dependabot[bot]
55211e30c5 Bump org.ajoberstar.grgit from 5.0.0 to 5.2.0 (#126)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: William <will27528@gmail.com>
2023-04-26 00:25:20 +01:00
evlad
2c1a38f2c9 Disable all projectiles while player is in sync (#127) 2023-04-26 00:21:04 +01:00
小蔡
0c7e052d44 Update zh-tw.yml (#124) 2023-04-20 22:20:24 +01:00
William
54cc11fce0 [ci skip] Update README.md 2023-04-17 11:21:40 +01:00
William
3645fa01ec [ci skip] Wrap README header in header tag 2023-04-17 11:11:38 +01:00
dependabot[bot]
e20a0da845 Bump net.kyori:adventure-api from 4.13.0 to 4.13.1 (#120)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-15 18:02:22 +01:00
William
7ed7d0a29e Add additional cast checking to PDC tag fetching, cast via complex type (#118) 2023-04-05 12:53:05 +01:00
William278
e7e7995e4e Bump dependencies 2023-04-05 12:03:13 +01:00
William278
af57cfcf70 Fix log method not logging throwables 2023-03-28 15:37:04 +01:00
William
f1ac9b5e04 [ci skip] Update README 2023-03-13 13:44:25 +00:00
William
a9b1070725 Encapsulate Settings, tweak keys, add empty dead player save option, close #73 2023-03-10 21:17:15 +00:00
William
5a000add98 Fix deadlock on busy servers with small thread pools, close #100 2023-03-10 21:00:15 +00:00
dependabot[bot]
aec2836d1e Bump com.github.plan-player-analytics:Plan from 5.5.2254 to 5.5.2272 (#108)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: William <will27528@gmail.com>
2023-03-10 20:56:14 +00:00
William
84e2ea3904 gradle: Update to Gradle 8.0.2, bump shadow to 8.1.0 2023-03-10 20:54:57 +00:00
dependabot[bot]
4f669170c2 Bump org.jetbrains:annotations from 24.0.0 to 24.0.1 (#107)
Bumps [org.jetbrains:annotations](https://github.com/JetBrains/java-annotations) from 24.0.0 to 24.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/commits)

---
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>
2023-03-05 12:45:59 +00:00
dependabot[bot]
8ea8c7b7ba Bump com.github.plan-player-analytics:Plan from 5.5.2208 to 5.5.2254 (#104)
Bumps [com.github.plan-player-analytics:Plan](https://github.com/plan-player-analytics/Plan) from 5.5.2208 to 5.5.2254.
- [Release notes](https://github.com/plan-player-analytics/Plan/releases)
- [Commits](https://github.com/plan-player-analytics/Plan/compare/5.5.2208...5.5.2254)

---
updated-dependencies:
- dependency-name: com.github.plan-player-analytics:Plan
  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>
Co-authored-by: William <will27528@gmail.com>
2023-02-28 02:50:03 +00:00
dependabot[bot]
acab4ae58a Bump net.william278:DesertWell from 1.1 to 1.1.1 (#102)
Bumps [net.william278:DesertWell](https://github.com/WiIIiam278/DesertWell) from 1.1 to 1.1.1.
- [Release notes](https://github.com/WiIIiam278/DesertWell/releases)
- [Commits](https://github.com/WiIIiam278/DesertWell/compare/1.1...1.1.1)

---
updated-dependencies:
- dependency-name: net.william278:DesertWell
  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>
2023-02-28 02:49:35 +00:00
Ceddix
ea822b0f4b fixed some issues I made (#101) 2023-02-23 02:26:52 +00:00
dependabot[bot]
24a9974ff7 Bump com.github.plan-player-analytics:Plan from 5.4.1690 to 5.5.2208 (#99)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: William <will27528@gmail.com>
2023-02-22 12:59:51 +00:00
dependabot[bot]
222a9871e0 Bump dev.dejvokep:boosted-yaml from 1.3 to 1.3.1 (#98)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: William <will27528@gmail.com>
2023-02-22 12:59:37 +00:00
dependabot[bot]
0ce9d2ce74 Bump dev.triumphteam:triumph-gui from 3.1.3 to 3.1.4 (#97)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-22 12:58:27 +00:00
William278
cde500a123 Bump to 2.2.4 2023-02-18 13:30:36 +00:00
William278
f15790030f Update dependencies 2023-02-18 13:29:12 +00:00
William278
ce3350c6fa Fix shutdown crash with PDC (rollback switch to using task) 2023-02-18 13:24:49 +00:00
William
4288742052 Update dependencies, close #85, close #86, close #87, close #88, close #89 2023-02-13 13:29:52 +00:00
William
8205b9c169 Create dependabot.yml and funding.yml 2023-02-13 13:20:27 +00:00
William
1d7f6a8d8b refactor: Use mappings class for PDC tag type handling 2023-02-12 19:14:46 +00:00
William
3425c97245 Bump to v2.2.3 2023-02-12 18:18:29 +00:00
evlad
2d1d8f1ab6 Sync lock: Cancel item frame interaction, add command blacklist (#84)
Co-authored-by: William <will27528@gmail.com>
2023-02-12 18:08:46 +00:00
William
f322d31b03 [ci skip] Correct typos in README 2023-02-12 15:43:26 +00:00
William278
368e665ac3 [ci skip] Update README 2023-01-27 11:46:31 +00:00
William278
922eb2f19a Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2023-01-27 11:45:50 +00:00
William278
23e0123004 Update README and banner 2023-01-27 11:45:04 +00:00
William
e98bac844a Hotfix bump to 2.2.2 - fix unchecked cast on trident lock check 2023-01-10 14:42:54 +00:00
William
d6d9a55f72 Fix unchecked cast on trident launch locking, fix #79 2023-01-10 14:42:28 +00:00
William
e3070a65ab Ensure player isn't locked before force-dropping items 2023-01-07 22:17:42 +00:00
William
a8b4696604 Minor style tweaks 2023-01-07 22:14:06 +00:00
William
7f5ca6206b Merge pull request #76 from ItsWagPvP/patch-1 2023-01-07 22:11:18 +00:00
William
ad885a9a15 Bump library versions, fix test dependencies 2023-01-07 22:11:05 +00:00
William
fe89e7b770 Fix tests 2023-01-07 21:31:14 +00:00
William
17ea62ed0b Merge pull request #78 from emmanuelvlad/master 2023-01-07 21:11:01 +00:00
evlad
94717637ba fix dupe with trident when player is locked 2023-01-07 22:06:47 +01:00
William
f6663f0c09 Refactor; consolidate Logger and ResourceReader, simplify certain method arguments 2023-01-05 19:06:04 +00:00
William
33588c2345 Bump to v2.2.1 2023-01-05 18:35:34 +00:00
William
c2c5a424fb Add checks against the user being an NPC 2023-01-05 18:35:10 +00:00
Gabriele Cabrini
ce41053e87 Fixed issue #74
Added the code that @alexdev03 suggested
2023-01-03 21:06:26 +01:00
William
5817de83e5 Fix locked map data not being applied in some cases 2023-01-03 12:38:17 +00:00
William
30dd48ce88 Add additional checks and error handling when setting health 2023-01-03 12:14:18 +00:00
William
cf7912a89e Bump to 2.2 2023-01-03 11:58:06 +00:00
William
9900b44858 Disable locked map syncing by default 2023-01-03 11:56:47 +00:00
William
9019181208 Merge remote-tracking branch 'origin/master' 2023-01-03 11:51:29 +00:00
William
99483387f1 Add additional error handling for player health and statistic updating 2023-01-03 11:51:25 +00:00
William
42177f2582 Merge pull request #75 from emmanuelvlad/expose-locked-players 2023-01-01 16:19:52 +00:00
William
e4e0743205 [ci skip] Update license year (2023) 2023-01-01 16:18:58 +00:00
evlad
105927a57f added dummy method 2022-12-31 04:41:20 +03:00
evlad
71706bf9ae expose locked players + add a method on OnlineUser 2022-12-31 04:30:45 +03:00
William
101e0c11d7 Fix unsynced players having data saved on world save / death 2022-12-27 19:31:14 +00:00
William
70323fb2e2 Merge remote-tracking branch 'origin/master' 2022-12-26 16:42:48 +00:00
William
9dc5577175 Clear the player's cursor when setting inventory contents 2022-12-26 16:06:22 +00:00
William
117d5edea2 [ci skip] Update badge 2022-12-19 16:04:35 +00:00
William
3f0f518037 [ci-skip] Clarify minimum Java version in README 2022-11-20 18:43:04 +00:00
William
2017ecc20f Minor refactoring / code improvements 2022-11-18 14:40:15 +00:00
William
ded89ad343 ACTION_BAR as default notification slot 2022-11-18 11:25:00 +00:00
William
c4b194f8d6 Fix unlocked maps getting wrongly locked 2022-11-17 02:46:40 +00:00
William
d682e6e6c6 Fix notifications through Toast AndJam library 2022-11-17 02:34:52 +00:00
William
6fef9c4eae Remove map itemstack debug logging mess 2022-11-16 22:40:23 +00:00
William278
16eee05065 Add notification slot configuration, support for toasts 2022-11-16 22:31:57 +00:00
William278
b664e2586d Bump gson to 2.10 2022-11-16 22:06:26 +00:00
William278
d594c9c257 Truncate long data save cause names, close #60 2022-11-16 14:44:32 +00:00
William278
532a65eca8 Ensure players remain locked on disconnect and shutdown, close #67 2022-11-16 12:06:01 +00:00
William
5af8ae0da5 Use canvas rendering approach, finish locked map synchronisation 2022-11-15 23:37:41 +00:00
William278
c0709f82bd Add the ability to synchronise/persist locked maps cross-server, close #14 2022-11-15 18:29:37 +00:00
William278
945b65e1bc Minor performance improvement to event cancelling, add checks against inventory clicks 2022-11-15 17:46:55 +00:00
William278
efcb36d345 Fix database username at wrong config path 2022-11-15 11:37:15 +00:00
William
30cd89c578 Remove unnecessary debug 2022-11-15 00:43:52 +00:00
William
bb3753b8e4 Fix typo 2022-11-15 00:42:34 +00:00
William
d5569ad3ed Fix event priorities in config, bump to 2.1.3 2022-11-15 00:36:45 +00:00
William
d8386fd2a2 Fix edit nodes not being respected 2022-11-14 18:25:44 +00:00
William
3bfea58f35 Make event priority configurable for three key events 2022-11-14 11:47:33 +00:00
William
51cf7beeb8 Remove redundant compiler warning suppressors 2022-11-14 11:04:41 +00:00
William
df247b41f4 Bump to v2.1.2 2022-11-06 22:32:46 +00:00
William
bac760165e Tweak logic for determining if a player is dead, fix issues with <1HP players being detected dead 2022-11-06 22:31:33 +00:00
William
dd39482ed1 Tweaked bukkit implementation of #isDead detection 2022-10-28 13:00:08 +01:00
William
c05f165278 Bump to v2.1.1 2022-10-26 15:21:06 +01:00
William
c888759d33 Merge remote-tracking branch 'origin/master' 2022-10-26 15:20:21 +01:00
William
089ea5b63a Fix unsafe joins on inventory and ender chest commands, better exception catching, Close #58 2022-10-26 15:20:08 +01:00
William
9020e9d906 Merge pull request #57 from iVillager/patch-1
Update it-it.yml
2022-10-19 19:41:27 +01:00
Villag3r_
7584ea0070 Update it-it.yml 2022-10-19 19:46:25 +02:00
William
9c243c2893 Merge pull request #56 from Ceddix/master
Updated German locales
2022-10-19 15:27:08 +01:00
Ceddix
8ba90fadc4 Updated german locales 2022-10-19 15:39:09 +02:00
William
480796fbee Remove config.yml 2022-10-13 20:30:19 +01:00
William
9b186ec97a [ci skip] Add data-dumping.png 2022-10-12 21:47:55 +01:00
William
d828631dea Merge branch 'master' into triumph-gui 2022-10-12 21:34:15 +01:00
William
5b8de7967b Fix it not being possible to creative-middle-click-clone item stacks from un-editable inventory snapshot views, close #44 2022-10-12 21:34:09 +01:00
William
4fddbc2b32 Automatically expand menus to accommodate custom inventory/echest sizes, close #45 2022-10-12 21:16:11 +01:00
William
43cd367ca3 TriumphGUI for menus, fix missing inv/echest view message, fix data saving despite no updates, close #42 2022-10-12 21:02:57 +01:00
William
19ca504bab Suggest delete and restore commands rather than run them 2022-10-12 16:37:56 +01:00
William
394b8ff1d1 Disable dump commands for operators by default 2022-10-12 15:43:39 +01:00
William
4577da3336 Restore users with at least one health point 2022-10-12 15:37:26 +01:00
William
c3b339b3dd Minor cleanup, tweak listener method names 2022-10-12 15:13:19 +01:00
William
2b91154ca2 Ensure players have their inventory contents saved on death 2022-10-12 01:45:46 +01:00
William
2351be31e3 Merge branch 'annotaml-config' into save-on-death 2022-10-11 21:15:50 +01:00
William
624543b93d More test fixes 2022-10-11 21:15:43 +01:00
William
c13f4b2a05 Merge branch 'annotaml-config' into save-on-death 2022-10-11 21:01:02 +01:00
William
00b8d335d8 Fix tests 2022-10-11 21:00:54 +01:00
William
89d8b79ae3 Add ability to save user data on player death 2022-10-11 20:58:19 +01:00
William
acd97a1cb0 Migrate config to Annotaml 2022-10-11 20:42:15 +01:00
William
8c0f7a295f [ci skip] tweak comment 2022-10-11 19:05:57 +01:00
William
7536bfaaf5 Add /userdata dump command, for file/web dumping of user data json 2022-10-07 22:52:58 +01:00
William
cbf5d9c24e Implement variable-sized user data; only save needed data 2022-10-07 17:02:26 +01:00
William
b9e474d946 Variable-size data format support: Deprecate getXData() methods in UserData in favour of optional returns 2022-10-07 16:04:56 +01:00
William
3d232f97fb Shade adventure instead of bundling at runtime 2022-10-07 14:31:08 +01:00
William
6d649d0889 Extra logging for DataSerialization exceptions 2022-10-07 14:20:48 +01:00
William
0754837820 Start work on data list pagination via PagineDown, update locales 2022-09-22 23:00:24 +01:00
William
8f44dbb296 Import cleanup 2022-09-22 15:00:38 +01:00
William
049cd8ecca Migrate plugin to MineDown/Kyori-adventure 2022-09-22 15:00:22 +01:00
William
2ed7705903 Merge remote-tracking branch 'origin/master' 2022-09-22 14:49:45 +01:00
William
6bc6749e38 Merge API module into bukkit for simplicity 2022-09-22 14:48:36 +01:00
William278
97a02b7a05 [ci skip] Tweak docs URL 2022-09-10 20:01:41 +03:00
William278
abc41a0aca [ci skip] Add screenshot to README 2022-09-10 20:01:00 +03:00
William278
31a14b2de7 [ci skip] Update README with new badges 2022-09-10 19:58:53 +03:00
William278
59a0002c16 Migrate to DesertWell for about menu, version checking 2022-09-10 19:52:34 +03:00
William278
61020e04d9 Fix typo in default command description 2022-09-10 19:40:53 +03:00
William278
ff1ace8342 Update version format 2022-09-10 19:27:26 +03:00
William278
847790c514 Remove api module, include it in with the bukkit module for simplicity 2022-09-10 19:25:07 +03:00
William
390a77b407 Update README.md (remove mariadb comment) 2022-08-21 13:30:37 +01:00
William
04ab9d14f8 Update rich-syntax-highlighting.png 2022-08-21 13:22:23 +01:00
William
3a32d481c4 Add rich-syntax-highlighting.png 2022-08-21 13:20:04 +01:00
William
8edbc029f8 Remove test commodore completions 2022-08-21 13:15:48 +01:00
William
258356e45d Bundle boosted-yaml, adjust shading and build scripts 2022-08-21 13:09:30 +01:00
William
e1628b6448 Download jedis at runtime 2022-08-21 12:51:58 +01:00
William
fa32e97564 Bump mysql-connector-java to 8.0.30 2022-08-21 12:33:47 +01:00
William
8080d57645 Update MPDB migrator messages 2022-08-21 12:22:33 +01:00
William
0a2f7b6cd4 Fix migration reporting for MPDB migrator too 2022-08-21 12:21:56 +01:00
William
26a2366876 Fix inaccurate migration progress tracking 2022-08-21 12:20:19 +01:00
William
2690ab3144 Make initialization exception more obvious, close #47 2022-08-21 11:54:51 +01:00
William
18b96944e9 Tweak migrator to ensure safe user setting on large data imports 2022-08-21 11:49:38 +01:00
William
3282f5739c Fix nested futures on #ensureUser causing ineffective #join synchronisation calls, close #40 2022-08-21 11:49:15 +01:00
William
50e66be0c0 Relocate DummySettings 2022-08-08 19:49:10 +01:00
William
593c88c8ba Fix tests, create DummySettings 2022-08-08 19:44:12 +01:00
William
2f700b2d93 Use Commodore for rich command completion registering 2022-08-08 19:32:09 +01:00
William
d1c95030f0 Added config option for syncing dead player inventories, cancel damage if locked 2022-08-04 17:48:16 +01:00
William
1ed2414241 Add Bulgarian (bg-bg) courtesy of Pukejoy_1 2022-08-04 13:31:28 +01:00
William
8847483ff8 Correct Persistent Data serialization 2022-07-21 14:23:27 +01:00
William
31552f85e4 Fix malformed events 2022-07-19 14:18:41 +01:00
William
125f142cf5 Fix tests 2022-07-19 11:28:26 +01:00
William
dc3882e47e Merge remote-tracking branch 'origin/master' 2022-07-19 11:27:00 +01:00
William
dafbcad10e Fix PersistentDataContainer synchronisation, bump to v2.0.2 2022-07-19 11:26:56 +01:00
William
d1085ca7bd Update README.md 2022-07-16 17:51:53 +01:00
265 changed files with 24021 additions and 6751 deletions

22
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
# Dependabot configuration file for GitHub
version: 2
updates:
# CI workflow action updates
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "ci"
# Gradle package updates
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "deps"
ignore:
- dependency-name: 'org.spigotmc:spigot-api'
- dependency-name: 'org.papermc:paper-api'

4
.github/funding.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
# Funding metadata for GitHub
github: WiIIiam278
custom: https://buymeacoff.ee/william278

80
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
name: CI Tests & Publish
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 21 📦'
uses: actions/setup-java@v4
with:
java-version: '21'
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@v5
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
- 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
paper-1.21.1
paper-1.21.4
fabric-1.20.1
fabric-1.21.1
fabric-1.21.4
distro-groups: |
paper
paper
paper
fabric
fabric
fabric
distro-descriptions: |
Paper 1.20.1
Paper 1.21.1
Paper 1.21.4
Fabric 1.20.1
Fabric 1.21.1
Fabric 1.21.4
files: |
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.20.1.jar
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.1.jar
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.4.jar
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.20.1.jar
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.1.jar
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.4.jar

View File

@@ -1,32 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
name: Java CI
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 16
uses: actions/setup-java@v3
with:
java-version: '16'
distribution: 'temurin'
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
with:
arguments: test

30
.github/workflows/pr_tests.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: PR Tests
on:
pull_request:
branches: [ 'master' ]
permissions:
contents: read
checks: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 'Checkout for CI 🛎'
uses: actions/checkout@v4
- name: 'Set up JDK 21 📦'
uses: actions/setup-java@v4
with:
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@v5
if: success() || failure() # Continue on failure
with:
report_paths: '**/build/test-results/test/TEST-*.xml'

69
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
name: Release Tests & Publish
on:
release:
types: [ 'published' ]
permissions:
contents: read
checks: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 'Checkout for CI 🛎️'
uses: actions/checkout@v4
- name: 'Set up JDK 21 📦'
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: 'Build with Gradle 🏗️'
uses: gradle/gradle-build-action@v3
with:
arguments: build test publish
env:
RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
RELEASES_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: '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.20.1
paper-1.21.1
paper-1.21.4
fabric-1.20.1
fabric-1.21.1
fabric-1.21.4
distro-groups: |
paper
paper
paper
fabric
fabric
fabric
distro-descriptions: |
Paper 1.20.1
Paper 1.21.1
Paper 1.21.4
Fabric 1.20.1
Fabric 1.21.1
Fabric 1.21.4
files: |
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.20.1.jar
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.1.jar
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.4.jar
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.20.1.jar
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.1.jar
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.4.jar

25
.github/workflows/update_docs.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Update Docs
on:
push:
branches: [ 'master' ]
paths:
- 'docs/**'
- 'workflows/**'
tags-ignore:
- '*'
permissions:
contents: write
jobs:
update-docs:
name: 'Update Docs'
runs-on: ubuntu-latest
steps:
- name: 'Checkout for CI 🛎️'
uses: actions/checkout@v4
- name: 'Push Docs to Github Wiki 📄️'
uses: Andrew-Chen-Wang/github-wiki-action@v4
with:
path: 'docs'

4
.gitignore vendored
View File

@@ -118,3 +118,7 @@ run/
!gradle-wrapper.jar
/build-output-final/
/target/
# Don't include generated test suite files
/test/servers/
/test/HuskSync

View File

@@ -1,19 +0,0 @@
#!/bin/bash
JV=$(java -version 2>&1 >/dev/null | head -1)
echo "$JV" | sed -E 's/^.*version "([^".]*)\.[^"]*".*$/\1/'
if [ "$JV" != 16 ]; then
case "$1" in
install)
echo "installing sdkman..."
curl -s "https://get.sdkman.io" | bash
source ~/.sdkman/bin/sdkman-init.sh
sdk install java 16.0.1-open
;;
use)
echo "must source ~/.sdkman/bin/sdkman-init.sh"
exit 1
;;
esac
fi

16
HEADER Normal file
View File

@@ -0,0 +1,16 @@
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.

217
LICENSE
View File

@@ -1,21 +1,202 @@
Copyright © William278 2022. All rights reserved
LICENSE
This source code is provided as reference to licensed individuals that have purchased the HuskSync
plugin once from any of the official sources it is provided. The availability of this code does
not grant you the rights to modify, re-distribute, compile or redistribute this source code or
"plugin" outside this intended purpose. This license does not cover libraries developed by third
parties that are utilised in the plugin.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
CONTRIBUTOR AGREEMENT
By contributing code to this repository, contributors agree that they forfeit their contributions
to the copyright holder and only the copyright holder.
In exchange for contributing, the copyright holder may give, at their discretion, permission to use
the plugin in commercial contexts
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
DEFINITIONS
"plugin"; the jar file compiled from this source code
"source code"; the java source code and gradle configurations provided in this repository, however
excludes libraries
"copyright holder"; William278
"contributor(s)"; person(s) who submit (contribute) code through a pull request to this repository
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to saveCause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must saveCause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

125
README.md
View File

@@ -1,61 +1,108 @@
# [![HuskSync Banner](images/banner-graphic.png)](https://github.com/WiIIiam278/HuskSync)
![Github Actions](https://github.com/WiIIiam278/HuskSync/workflows/Java%20CI/badge.svg)
[![Discord](https://img.shields.io/discord/818135932103557162?color=7289da&logo=discord)](https://discord.gg/tVYhJfyDWG)
<!--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>
<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" />
</a>
<a href="https://discord.gg/tVYhJfyDWG">
<img src="https://img.shields.io/discord/818135932103557162.svg?label=&logo=discord&logoColor=fff&color=7389D8&labelColor=6A7EC2" />
</a>
<br/>
<b>
<a href="https://www.spigotmc.org/resources/husksync.97144/">Spigot</a>
</b>
<b>
<a href="https://william278.net/docs/husksync/setup">Setup</a>
</b>
<b>
<a href="https://william278.net/docs/husksync/">Docs</a>
</b>
<b>
<a href="https://github.com/WiIIiam278/HuskSync/issues">Issues</a>
</b>
</p>
<br/>
[Documentation, Guides & API](https://william278.net/docs/husksync/Home) · [Resource Page](https://www.spigotmc.org/resources/husksync.97144/) · [Bug Reports](https://github.com/WiIIiam278/HuskSync/issues)
**HuskSync** is a modern, cross-server player data synchronisation system that enables the comprehensive synchronisation of your user's data across multiple proxied servers. It does this by making use of Redis and MySQL to optimally cache data while players change servers.
**HuskSync** is a modern, cross-server player data synchronization system that enables the comprehensive synchronization of your user's data across multiple proxied servers. It does this by making use of Redis and a MySQL/Mongo/PostgreSQL to optimally cache data while players change servers.
## Features
- Synchronise inventories, ender chests, advancements, statistics, experience points, health, max health, hunger, saturation, potion effects, persistent data container tags, game mode, location and more across multiple proxied servers.
- Create and manage "snapshot" backups of user data and roll back users to previous states on-the-fly. (`/userdata`)
- Preview, list, delete, restore & pin user data snapshots in-game with an intuitive menu.
- Examine the contents of player's inventories and ender chests on-the-fly. (`/inventory`, `/enderchest`)
- Hooks with your [Player Analytics](https://github.com/plan-player-analytics/Plan) web panel to provide an overview of user data.
- Supports segregating synchronisation across multiple distinct clusters on one network.
**⭐ Seamless synchronization** &mdash; Utilises optimised Redis caching when players change server to sync player data super quickly for a seamless experience.
## Requirements
* A MySQL Database (v8.0+)
* A Redis Database (v5.0+)
* Any number of proxied Spigot servers (Minecraft v1.16.5+)
**⭐ Complete player synchronization** &mdash; Sync inventories, Ender Chests, health, hunger, effects, advancements, statistics, locked maps & [more](https://william278.net/docs/husksync/sync-features)—no data left behind!
**⭐ Backup, restore & rotate** &mdash; Something gone wrong? Restore players back to a previous data state. Rotate and manage data snapshots in-game!
**⭐ Import existing data** &mdash; Import your MySQLPlayerDataBridge data—or from your existing world data! No server reset needed!
**⭐ Works great with Plan** &mdash; Stay in touch with your community through HuskSync analytics on your Plan web panel.
**⭐ Extensible API & open-source** &mdash; Need more? Extend the plugin with the Developer API. Or, submit a pull request through our code bounty system!
**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
1. Place the plugin jar file in the `/plugins/` directory of each Spigot 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.
3. Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`) and fill in both the MySQL and Redis database credentials.
4. Start every server again and synchronistaion will begin.
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).
## Building
To build HuskSync, simply run the following in the root of the repository:
```
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.
3. Navigate to the HuskSync config file on each server and fill in both your database and Redis server credentials.
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 21). Builds will be output in `/target`:
```bash
./gradlew clean build
```
## License
HuskSync is a premium resource. This source code is provided as reference only for those who have purchased the resource from an official source.
HuskSync uses `essential-multi-version` (Fabric) and `preprocessor` (Bukkit) to target multiple versions of Minecraft in one codebase - [check here](https://github.com/WiIIiam278/PreProcessor?tab=readme-ov-file#code-example) for a preprocessor comment logic reference.
### License
HuskSync is licensed under the Apache 2.0 license.
- [License](https://github.com/WiIIiam278/HuskSync/blob/master/LICENSE)
## Contributing
A code bounty program is in place for HuskSync, where developers making significant code contributions to HuskSync may be entitled to a license at my discretion to use HuskSync in commercial contexts without having to purchase the resource. Please read the information for contributors in the LICENSE file before submitting a pull request.
Contributions to the project are welcome&mdash;feel free to open a pull request with new features, improvements and/or fixes!
## Translation
### Support
Due to its complexity, official binaries and customer support for HuskSync is provided through a paid model. This means that support is only available to users who have purchased a license to the plugin from Spigot, Polymart, or BuiltByBit and have provided proof of purchase. Please join our Discord server if you have done so and need help!
### Translations
Translations of the plugin locales are welcome to help make the plugin more accessible. Please submit a pull request with your translations as a `.yml` file.
- [Locales Directory](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/locales)
- [English Locales](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/locales/en-gb.yml)
## bStats
This plugin uses bStats to provide me with metrics about its usage:
- [bStats Metrics](https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140)
You can turn metric collection off by navigating to `~/plugins/bStats/config.yml` and editing the config to disable plugin metrics.
## Links
- [Documentation, Guides & API](https://william278.net/docs/husksync/Home)
- [Resource Page](https://www.spigotmc.org/resources/husksync.97144/)
- [Bug Reports](https://github.com/WiIIiam278/HuskSync/issues)
- [Discord Support](https://discord.gg/tVYhJfyDWG) (Proof of purchase required)
- [Docs](https://william278.net/docs/husksync/) &mdash; Read the plugin documentation!
- [Spigot](https://www.spigotmc.org/resources/husksync.97144/) &mdash; View the Spigot resource page (Also: [Polymart](https://polymart.org/resource/husksync.1634), [Craftaro](https://craftaro.com/marketplace/product/husksync.758), [BuiltByBit](https://builtbybit.com/resources/husksync.34956/))
- [Issues](https://github.com/WiIIiam278/HuskSync/issues) &mdash; File a bug report or feature request
- [Discord](https://discord.gg/tVYhJfyDWG) &mdash; Get help, ask questions (Purchase required)
- [bStats](https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140) &mdash; View plugin metrics
---
&copy; [William278](https://william278.net/), 2022. All rights reserved.
&copy; [William278](https://william278.net/), 2025. Licensed under the Apache-2.0 License.

View File

@@ -1,29 +0,0 @@
dependencies {
implementation project(path: ':bukkit')
compileOnly project(path: ':common')
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
compileOnly 'org.jetbrains:annotations:23.0.0'
}
shadowJar {
relocate 'org.apache', 'net.william278.husksync.libraries'
relocate 'dev.dejvokep', 'net.william278.husksync.libraries'
relocate 'de.themoep', 'net.william278.husksync.libraries'
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
relocate 'org.intellij', 'net.william278.husksync.libraries'
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
relocate 'com.google', 'net.william278.husksync.libraries'
relocate 'redis.clients', 'net.william278.husksync.libraries'
relocate 'org.json', 'net.william278.husksync.libraries.json'
relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby'
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
}
java {
withSourcesJar()
withJavadocJar()
}

View File

@@ -1,203 +0,0 @@
package net.william278.husksync.api;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.data.*;
import net.william278.husksync.player.BukkitPlayer;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
/**
* The HuskSync API implementation for the Bukkit platform, providing methods to access and modify player {@link UserData} held by {@link User}s.
* </p>
* Retrieve an instance of the API class via {@link #getInstance()}.
*/
@SuppressWarnings("unused")
public class HuskSyncAPI extends BaseHuskSyncAPI {
/**
* <b>(Internal use only)</b> - Instance of the API class
*/
private static final HuskSyncAPI INSTANCE = new HuskSyncAPI();
/**
* <b>(Internal use only)</b> - Constructor, instantiating the API
*/
private HuskSyncAPI() {
super(BukkitHuskSync.getInstance());
}
/**
* Entrypoint to the HuskSync API - returns an instance of the API
*
* @return instance of the HuskSync API
*/
public static @NotNull HuskSyncAPI getInstance() {
return INSTANCE;
}
/**
* Returns a {@link User} instance for the given bukkit {@link Player}.
*
* @param player the bukkit player to get the {@link User} instance for
* @return the {@link User} instance for the given bukkit {@link Player}
* @since 2.0
*/
@NotNull
public OnlineUser getUser(@NotNull Player player) {
return BukkitPlayer.adapt(player);
}
/**
* Set the inventory in the database of the given {@link User} to the given {@link ItemStack} contents
*
* @param user the {@link User} to set the inventory of
* @param inventoryContents the {@link ItemStack} contents to set the inventory to
* @return future returning void when complete
* @since 2.0
*/
public CompletableFuture<Void> setInventoryData(@NotNull User user, @NotNull ItemStack[] inventoryContents) {
return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData ->
userData.ifPresent(data -> serializeItemStackArray(inventoryContents)
.thenAccept(serializedInventory -> {
data.getInventoryData().serializedItems = serializedInventory;
setUserData(user, data).join();
}))));
}
/**
* Set the inventory in the database of the given {@link User} to the given {@link BukkitInventoryMap} contents
*
* @param user the {@link User} to set the inventory of
* @param inventoryMap the {@link BukkitInventoryMap} contents to set the inventory to
* @return future returning void when complete
* @since 2.0
*/
public CompletableFuture<Void> setInventoryData(@NotNull User user, @NotNull BukkitInventoryMap inventoryMap) {
return setInventoryData(user, inventoryMap.getContents());
}
/**
* Set the Ender Chest in the database of the given {@link User} to the given {@link ItemStack} contents
*
* @param user the {@link User} to set the Ender Chest of
* @param enderChestContents the {@link ItemStack} contents to set the Ender Chest to
* @return future returning void when complete
* @since 2.0
*/
public CompletableFuture<Void> setEnderChestData(@NotNull User user, @NotNull ItemStack[] enderChestContents) {
return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData ->
userData.ifPresent(data -> serializeItemStackArray(enderChestContents)
.thenAccept(serializedInventory -> {
data.getEnderChestData().serializedItems = serializedInventory;
setUserData(user, data).join();
}))));
}
/**
* Returns a {@link BukkitInventoryMap} for the given {@link User}, containing their current inventory item data
*
* @param user the {@link User} to get the {@link BukkitInventoryMap} for
* @return future returning the {@link BukkitInventoryMap} for the given {@link User} if they exist,
* otherwise an empty {@link Optional}
* @since 2.0
*/
public CompletableFuture<Optional<BukkitInventoryMap>> getPlayerInventory(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> getUserData(user).join()
.map(userData -> deserializeInventory(userData
.getInventoryData().serializedItems).join()));
}
/**
* Returns the {@link ItemStack}s array contents of the given {@link User}'s Ender Chest data
*
* @param user the {@link User} to get the Ender Chest contents of
* @return future returning the {@link ItemStack} array of Ender Chest items for the user if they exist,
* otherwise an empty {@link Optional}
* @since 2.0
*/
public CompletableFuture<Optional<ItemStack[]>> getPlayerEnderChest(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> getUserData(user).join()
.map(userData -> deserializeItemStackArray(userData
.getEnderChestData().serializedItems).join()));
}
/**
* Deserialize a Base-64 encoded inventory array string into a {@link ItemStack} array.
*
* @param serializedItemStackArray The Base-64 encoded inventory array string.
* @return The deserialized {@link ItemStack} array.
* @throws DataSerializationException If an error occurs during deserialization.
* @since 2.0
*/
public CompletableFuture<ItemStack[]> deserializeItemStackArray(@NotNull String serializedItemStackArray)
throws DataSerializationException {
return CompletableFuture.supplyAsync(() -> BukkitSerializer
.deserializeItemStackArray(serializedItemStackArray).join());
}
/**
* Deserialize a serialized {@link ItemStack} array of player inventory contents into a {@link BukkitInventoryMap}
*
* @param serializedInventory The serialized {@link ItemStack} array of player inventory contents.
* @return A {@link BukkitInventoryMap} of the deserialized {@link ItemStack} contents array
* @throws DataSerializationException If an error occurs during deserialization.
* @since 2.0
*/
public CompletableFuture<BukkitInventoryMap> deserializeInventory(@NotNull String serializedInventory)
throws DataSerializationException {
return CompletableFuture.supplyAsync(() -> BukkitSerializer
.deserializeInventory(serializedInventory).join());
}
/**
* Serialize an {@link ItemStack} array into a Base-64 encoded string.
*
* @param itemStacks The {@link ItemStack} array to serialize.
* @return The serialized Base-64 encoded string.
* @throws DataSerializationException If an error occurs during serialization.
* @see #deserializeItemStackArray(String)
* @see ItemData
* @since 2.0
*/
public CompletableFuture<String> serializeItemStackArray(@NotNull ItemStack[] itemStacks)
throws DataSerializationException {
return CompletableFuture.supplyAsync(() -> BukkitSerializer.serializeItemStackArray(itemStacks).join());
}
/**
* Deserialize a Base-64 encoded potion effect array string into a {@link PotionEffect} array.
*
* @param serializedPotionEffectArray The Base-64 encoded potion effect array string.
* @return The deserialized {@link PotionEffect} array.
* @throws DataSerializationException If an error occurs during deserialization.
* @since 2.0
*/
public CompletableFuture<PotionEffect[]> deserializePotionEffectArray(@NotNull String serializedPotionEffectArray)
throws DataSerializationException {
return CompletableFuture.supplyAsync(() -> BukkitSerializer
.deserializePotionEffectArray(serializedPotionEffectArray).join());
}
/**
* Serialize a {@link PotionEffect} array into a Base-64 encoded string.
*
* @param potionEffects The {@link PotionEffect} array to serialize.
* @return The serialized Base-64 encoded string.
* @throws DataSerializationException If an error occurs during serialization.
* @see #deserializePotionEffectArray(String)
* @see PotionEffectData
* @since 2.0
*/
public CompletableFuture<String> serializePotionEffectArray(@NotNull PotionEffect[] potionEffects)
throws DataSerializationException {
return CompletableFuture.supplyAsync(() -> BukkitSerializer.serializePotionEffectArray(potionEffects).join());
}
}

View File

@@ -1,102 +1,233 @@
import org.apache.tools.ant.filters.ReplaceTokens
plugins {
id 'com.github.johnrengelman.shadow' version '7.1.2'
id 'org.ajoberstar.grgit' version '5.0.0'
id 'java'
id 'com.gradleup.shadow' version '8.3.6'
id 'org.cadixdev.licenser' version '0.6.1' apply false
id 'fabric-loom' version "$fabric_loom_version" apply false
id 'gg.essential.multi-version.root' apply false
id 'org.ajoberstar.grgit' version '5.3.0'
id 'maven-publish'
id 'java'
}
group 'net.william278'
version "$ext.plugin_version+${versionMetadata()}"
version "$ext.plugin_version${versionMetadata()}"
description "$ext.plugin_description"
defaultTasks 'licenseFormat', 'build'
ext {
set 'version', version.toString()
set 'description', description.toString()
set 'jedis_version', jedis_version.toString()
set 'mysql_driver_version', mysql_driver_version.toString()
set 'mariadb_driver_version', mariadb_driver_version.toString()
set 'postgres_driver_version', postgres_driver_version.toString()
set 'mongodb_driver_version', mongodb_driver_version.toString()
set 'snappy_version', snappy_version.toString()
}
import org.apache.tools.ant.filters.ReplaceTokens
publishing {
repositories {
if (System.getenv("RELEASES_MAVEN_USERNAME") != null) {
maven {
name = "william278-releases"
url = "https://repo.william278.net/releases"
credentials {
username = System.getenv("RELEASES_MAVEN_USERNAME")
password = System.getenv("RELEASES_MAVEN_PASSWORD")
}
authentication {
basic(BasicAuthentication)
}
}
}
if (System.getenv("SNAPSHOTS_MAVEN_USERNAME") != null) {
maven {
name = "william278-snapshots"
url = "https://repo.william278.net/snapshots"
credentials {
username = System.getenv("SNAPSHOTS_MAVEN_USERNAME")
password = System.getenv("SNAPSHOTS_MAVEN_PASSWORD")
}
authentication {
basic(BasicAuthentication)
}
}
}
}
}
allprojects {
apply plugin: 'com.github.johnrengelman.shadow'
// Ignore parent projects (no jars)
if (project.name == 'fabric' || project.name == 'bukkit') {
return
}
apply plugin: 'com.gradleup.shadow'
apply plugin: 'org.cadixdev.licenser'
apply plugin: 'java'
compileJava.options.encoding = 'UTF-8'
compileJava.options.release.set 17
javadoc.options.encoding = 'UTF-8'
javadoc.options.addStringOption('Xdoclint:none', '-quiet')
compileJava.options.release.set 16
repositories {
mavenLocal()
mavenCentral()
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.codemc.io/repository/maven-public/' }
maven { url 'https://repo.minebench.de/' }
maven { url 'https://repo.alessiodp.com/releases/' }
maven { url 'https://jitpack.io' }
maven { url 'https://mvn-repo.arim.space/lesser-gpl3/' }
maven { url 'https://libraries.minecraft.net/' }
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.8.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'
testCompileOnly 'org.jetbrains:annotations:26.0.2'
}
license {
header = rootProject.file('HEADER')
include '**/*.java'
newLine = true
}
test {
useJUnitPlatform()
}
processResources {
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
tokens: rootProject.ext.properties
def tokenMap = rootProject.ext.properties
tokenMap.merge("grgit",'',(s, s2) -> s)
filesMatching(['**/*.json', '**/*.yml']) {
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
tokens: tokenMap
}
}
}
subprojects {
// Ignore parent projects (no jars)
if (['fabric', 'bukkit'].contains(project.name)) {
return
}
// Project naming
version rootProject.version
archivesBaseName = "${rootProject.name}-${project.name.capitalize()}"
def name = "$rootProject.name"
if (rootProject != project.parent) {
name += "-${project.parent.name.capitalize()}"
} else {
name += "-${project.name.capitalize()}"
}
archivesBaseName = name
if (['bukkit', 'api', 'plugin'].contains(project.name)) {
shadowJar {
destinationDirectory.set(file("$rootDir/target"))
archiveClassifier.set('')
// Version-specific configuration
if (['fabric', 'bukkit'].contains(project.parent?.name)) {
compileJava.options.release.set (project.name == '1.20.1' ? 17 : 21) // 1.20.1 requires Java 17
version += "+mc.${project.name}"
if (project.parent?.name?.equals('fabric')) {
apply plugin: 'fabric-loom'
}
}
// API publishing
if ('api'.contains(project.name)) {
java {
withSourcesJar()
withJavadocJar()
}
sourcesJar {
destinationDirectory.set(file("$rootDir/target"))
}
javadocJar {
destinationDirectory.set(file("$rootDir/target"))
}
shadowJar.dependsOn(sourcesJar, javadocJar)
jar {
from '../LICENSE'
}
publishing {
shadowJar {
destinationDirectory.set(file("$rootDir/target"))
archiveClassifier.set('')
}
// API publishing
if (project.name == 'common' || ['fabric', 'bukkit'].contains(project.parent?.name)) {
java {
withSourcesJar()
withJavadocJar()
}
sourcesJar {
destinationDirectory.set(file("$rootDir/target"))
}
javadocJar {
destinationDirectory.set(file("$rootDir/target"))
}
shadowJar.dependsOn(sourcesJar, javadocJar)
publishing {
if (['common'].contains(project.name)) {
publications {
mavenJava(MavenPublication) {
groupId = 'net.william278'
artifactId = 'husksync'
mavenJavaCommon(MavenPublication) {
groupId = 'net.william278.husksync'
artifactId = 'husksync-common'
version = "$rootProject.version"
artifact shadowJar
artifact javadocJar
artifact sourcesJar
artifact javadocJar
}
}
}
if (project.parent?.name?.equals('bukkit')) {
publications {
"mavenJavaBukkit_${project.name.replace('.', '_')}"(MavenPublication) {
groupId = 'net.william278.husksync'
artifactId = 'husksync-bukkit'
version = "$rootProject.version+$project.name"
artifact shadowJar
artifact sourcesJar
artifact javadocJar
}
}
}
if (project.parent?.name?.equals('fabric')) {
publications {
"mavenJavaFabric_${project.name.replace('.', '_')}"(MavenPublication) {
groupId = 'net.william278.husksync'
artifactId = 'husksync-fabric'
version = "$rootProject.version+$project.name"
artifact remapJar
artifact sourcesJar
artifact javadocJar
}
}
}
}
jar.dependsOn(shadowJar)
clean.delete "$rootDir/target"
}
jar.dependsOn shadowJar
clean.delete "$rootDir/target"
}
logger.lifecycle("Building HuskSync ${version} by William278")
@SuppressWarnings('GrMethodMayBeStatic')
def versionMetadata() {
// Require grgit
if (grgit == null) {
return System.getenv("GITHUB_RUN_NUMBER") ? 'build.' + System.getenv("GITHUB_RUN_NUMBER") : 'unknown'
return '-unknown'
}
return 'rev.' + grgit.head().abbreviatedId + (grgit.status().clean ? '' : '-indev')
}
// If unclean, return the last commit hash with -indev
if (!grgit.status().clean) {
return '-' + grgit.head().abbreviatedId + '-indev'
}
// Otherwise if this matches a tag, return nothing
def tag = grgit.tag.list().find { it.commit.id == grgit.head().id }
if (tag != null) {
return ''
}
return '-' + grgit.head().abbreviatedId
}

View File

@@ -0,0 +1,3 @@
minecraft_version_numeric=12001
minecraft_api_version=1.20
paper_api_version=1.20.1-R0.1-SNAPSHOT

View File

@@ -0,0 +1,3 @@
minecraft_version_numeric=12101
minecraft_api_version=1.21
paper_api_version=1.21.1-R0.1-SNAPSHOT

View File

@@ -0,0 +1,3 @@
minecraft_version_numeric=12104
minecraft_api_version=1.21
paper_api_version=1.21.4-R0.1-SNAPSHOT

View File

@@ -1,29 +1,97 @@
plugins {
id 'java'
id 'net.william278.preprocessor' version '1.0'
id 'xyz.jpenilla.run-paper' version '2.3.1'
id 'maven-publish'
}
dependencies {
implementation project(path: ':common')
implementation 'org.bstats:bstats-bukkit:3.0.0'
implementation 'net.william278.uniform:uniform-bukkit:1.3.1'
implementation 'net.william278.uniform:uniform-paper:1.3.1'
implementation 'net.william278.toilet:toilet-bukkit:1.0.12'
implementation 'net.william278:mpdbdataconverter:1.0.1'
implementation 'net.william278:hsldataconverter:1.0'
implementation 'net.william278:mapdataapi:2.0'
implementation 'org.bstats:bstats-bukkit:3.1.0'
implementation 'net.kyori:adventure-platform-bukkit:4.3.4'
implementation 'dev.triumphteam:triumph-gui:3.1.11'
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
implementation 'de.tr7zw:item-nbt-api:2.14.2-SNAPSHOT'
compileOnly 'redis.clients:jedis:4.2.3'
compileOnly 'commons-io:commons-io:2.11.0'
compileOnly 'de.themoep:minedown:1.7.1-SNAPSHOT'
compileOnly 'dev.dejvokep:boosted-yaml:1.2'
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
compileOnly 'com.zaxxer:HikariCP:5.0.1'
compileOnly "io.papermc.paper:paper-api:${paper_api_version}"
compileOnly 'com.github.retrooper:packetevents-spigot:2.7.0'
compileOnly 'com.github.dmulloy2:ProtocolLib:5.3.0'
compileOnly 'org.projectlombok:lombok:1.18.36'
compileOnly 'commons-io:commons-io:2.18.0'
compileOnly 'org.json:json:20250107'
compileOnly 'net.william278:minedown:1.8.2'
compileOnly 'de.exlll:configlib-yaml:4.5.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.36'
}
processResources {
filesMatching(['**/*.json', '**/*.yml']) {
expand([
version: version,
paper_api_version: paper_api_version,
minecraft_version: project.name,
minecraft_api_version: minecraft_api_version
])
}
}
sourceSets.main {
java.srcDirs '../src/main/java'
resources.srcDirs '../src/main/resources'
}
javadoc.setSource('./build/generated/preprocessed/main/java')
preprocess {
vars.put('MC', minecraft_version_numeric)
}
shadowJar {
relocate 'org.apache', 'net.william278.husksync.libraries'
dependencies {
exclude(dependency('com.mojang:brigadier'))
}
relocate 'org.apache.commons.io', 'net.william278.husksync.libraries.commons.io'
relocate 'org.apache.commons.text', 'net.william278.husksync.libraries.commons.text'
relocate 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3'
relocate 'com.google.gson', 'net.william278.husksync.libraries.gson'
relocate 'com.fatboyindustrial', 'net.william278.husksync.libraries'
relocate 'de.themoep', 'net.william278.husksync.libraries'
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
relocate 'org.intellij', 'net.william278.husksync.libraries'
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
relocate 'com.google', 'net.william278.husksync.libraries'
relocate 'redis.clients', 'net.william278.husksync.libraries'
relocate 'org.json', 'net.william278.husksync.libraries.json'
relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby'
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
relocate 'de.exlll', 'net.william278.husksync.libraries'
relocate 'net.william278.uniform', 'net.william278.husksync.libraries.uniform'
relocate 'net.william278.toilet', 'net.william278.husksync.libraries.toilet'
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.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
relocate 'org.json', 'net.william278.husksync.libraries.json'
relocate 'net.querz', 'net.william278.husksync.libraries.nbtparser'
relocate 'net.roxeez', 'net.william278.husksync.libraries'
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib'
relocate 'de.tr7zw.changeme.nbtapi', 'net.william278.husksync.libraries.nbtapi'
minimize()
}
tasks {
runServer {
minecraftVersion(project.name)
}
}

5
bukkit/gradle.properties Normal file
View File

@@ -0,0 +1,5 @@
org.gradle.daemon=false
org.gradle.parallel=true
org.gradle.configureoncommand=true
org.gradle.parallel.threads=4
org.gradle.jvmargs=-Xmx8G

0
bukkit/root.gradle Normal file
View File

View File

@@ -1,295 +1,414 @@
/*
* 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;
import dev.dejvokep.boostedyaml.YamlDocument;
import dev.dejvokep.boostedyaml.dvs.versioning.BasicVersioning;
import dev.dejvokep.boostedyaml.settings.dumper.DumperSettings;
import dev.dejvokep.boostedyaml.settings.general.GeneralSettings;
import dev.dejvokep.boostedyaml.settings.loader.LoaderSettings;
import dev.dejvokep.boostedyaml.settings.updater.UpdaterSettings;
import net.william278.husksync.command.BukkitCommand;
import net.william278.husksync.command.BukkitCommandType;
import net.william278.husksync.command.Permission;
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;
import lombok.Setter;
import net.kyori.adventure.platform.AudienceProvider;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import net.william278.desertwell.util.Version;
import net.william278.husksync.adapter.DataAdapter;
import net.william278.husksync.adapter.GsonAdapter;
import net.william278.husksync.adapter.SnappyGsonAdapter;
import net.william278.husksync.api.BukkitHuskSyncAPI;
import net.william278.husksync.command.PluginCommand;
import net.william278.husksync.config.Locales;
import net.william278.husksync.config.Server;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.CompressedDataAdapter;
import net.william278.husksync.data.DataAdapter;
import net.william278.husksync.data.JsonDataAdapter;
import net.william278.husksync.data.*;
import net.william278.husksync.database.Database;
import net.william278.husksync.database.MongoDbDatabase;
import net.william278.husksync.database.MySqlDatabase;
import net.william278.husksync.editor.DataEditor;
import net.william278.husksync.event.BukkitEventCannon;
import net.william278.husksync.event.EventCannon;
import net.william278.husksync.database.PostgresDatabase;
import net.william278.husksync.event.BukkitEventDispatcher;
import net.william278.husksync.hook.PlanHook;
import net.william278.husksync.listener.BukkitEventListener;
import net.william278.husksync.listener.EventListener;
import net.william278.husksync.listener.LockedHandler;
import net.william278.husksync.migrator.LegacyMigrator;
import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.migrator.MpdbMigrator;
import net.william278.husksync.player.BukkitPlayer;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.util.*;
import net.william278.husksync.sync.DataSyncer;
import net.william278.husksync.user.BukkitUser;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.util.BukkitLegacyConverter;
import net.william278.husksync.maps.BukkitMapHandler;
import net.william278.husksync.util.BukkitTask;
import net.william278.husksync.util.LegacyConverter;
import net.william278.toilet.BukkitToilet;
import net.william278.toilet.Toilet;
import net.william278.uniform.Uniform;
import net.william278.uniform.bukkit.BukkitUniform;
import org.bstats.bukkit.Metrics;
import org.bukkit.Bukkit;
import org.bukkit.command.PluginCommand;
import org.bukkit.entity.Player;
import org.bukkit.permissions.PermissionDefault;
import org.bukkit.map.MapView;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import space.arim.morepaperlib.MorePaperLib;
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
import space.arim.morepaperlib.scheduling.AttachedScheduler;
import space.arim.morepaperlib.scheduling.GracefulScheduling;
import space.arim.morepaperlib.scheduling.RegionalScheduler;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.stream.Collectors;
public class BukkitHuskSync extends JavaPlugin implements HuskSync {
@Getter
@NoArgsConstructor
@SuppressWarnings("unchecked")
public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier,
BukkitEventDispatcher, BukkitMapHandler {
/**
* Metrics ID for <a href="https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140">HuskSync on Bukkit</a>.
*/
private static final int METRICS_ID = 13140;
private static final String PLATFORM_TYPE_ID = "bukkit";
private final TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap(
SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR
);
private final Map<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap();
private final Map<Integer, MapView> mapViews = Maps.newConcurrentMap();
private final List<Migrator> availableMigrators = Lists.newArrayList();
private final Set<UUID> lockedPlayers = Sets.newConcurrentHashSet();
private boolean disabling;
private Gson gson;
private AudienceProvider audiences;
private Toilet toilet;
private MorePaperLib paperLib;
private Database database;
private RedisManager redisManager;
private Logger logger;
private ResourceReader resourceReader;
private EventListener eventListener;
private BukkitEventListener eventListener;
private DataAdapter dataAdapter;
private DataEditor dataEditor;
private EventCannon eventCannon;
private DataSyncer dataSyncer;
private LegacyConverter legacyConverter;
private AsynchronousScheduler asyncScheduler;
private RegionalScheduler regionalScheduler;
@Setter
private Settings settings;
@Setter
private Locales locales;
private List<Migrator> availableMigrators;
private static BukkitHuskSync instance;
/**
* (<b>Internal use only)</b> Returns the instance of the implementing Bukkit plugin
*
* @return the instance of the Bukkit plugin
*/
public static BukkitHuskSync getInstance() {
return instance;
}
@Setter
@Getter(AccessLevel.NONE)
private Server serverName;
@Override
public void onLoad() {
instance = this;
// Initial plugin setup
this.disabling = false;
this.gson = createGson();
this.paperLib = new MorePaperLib(this);
// Load settings and locales
initialize("plugin config & locale files", (plugin) -> {
loadSettings();
loadLocales();
loadServer();
validateConfigFiles();
});
this.eventListener = createEventListener();
eventListener.onLoad();
}
@Override
public void onEnable() {
// Initialize HuskSync
final AtomicBoolean initialized = new AtomicBoolean(true);
try {
// Set the logging adapter and resource reader
this.logger = new BukkitLogger(this.getLogger());
this.resourceReader = new BukkitResourceReader(this);
this.audiences = BukkitAudiences.create(this);
this.toilet = BukkitToilet.create(getDumpOptions());
// Load settings and locales
getLoggingAdapter().log(Level.INFO, "Loading plugin configuration settings & locales...");
initialized.set(reload().join());
if (initialized.get()) {
logger.showDebugLogs(settings.getBooleanValue(Settings.ConfigOption.DEBUG_LOGGING));
getLoggingAdapter().log(Level.INFO, "Successfully loaded plugin configuration settings & locales");
// Check compatibility
checkCompatibility();
// Register commands
initialize("commands", (plugin) -> getUniform().register(PluginCommand.Type.create(this)));
// Prepare data adapter
initialize("data adapter", (plugin) -> {
if (settings.getSynchronization().isCompressData()) {
dataAdapter = new SnappyGsonAdapter(this);
} else {
throw new HuskSyncInitializationException("Failed to load plugin configuration settings and/or locales");
dataAdapter = new GsonAdapter(this);
}
});
// Prepare data adapter
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_COMPRESS_DATA)) {
dataAdapter = new CompressedDataAdapter();
} else {
dataAdapter = new JsonDataAdapter();
}
// Prepare serializers
initialize("data serializers", (plugin) -> {
registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this));
registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this));
registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(this));
registerSerializer(Identifier.STATISTICS, new Serializer.Json<>(this, BukkitData.Statistics.class));
registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this));
registerSerializer(Identifier.GAME_MODE, new Serializer.Json<>(this, BukkitData.GameMode.class));
registerSerializer(Identifier.FLIGHT_STATUS, new Serializer.Json<>(this, BukkitData.FlightStatus.class));
registerSerializer(Identifier.ATTRIBUTES, new Serializer.Json<>(this, BukkitData.Attributes.class));
registerSerializer(Identifier.HEALTH, new Serializer.Json<>(this, BukkitData.Health.class));
registerSerializer(Identifier.HUNGER, new Serializer.Json<>(this, BukkitData.Hunger.class));
registerSerializer(Identifier.EXPERIENCE, new Serializer.Json<>(this, BukkitData.Experience.class));
registerSerializer(Identifier.LOCATION, new Serializer.Json<>(this, BukkitData.Location.class));
validateDependencies();
});
// Prepare event cannon
eventCannon = new BukkitEventCannon();
// Prepare data editor
dataEditor = new DataEditor(locales);
// Prepare migrators
availableMigrators = new ArrayList<>();
// Setup available migrators
initialize("data migrators/converters", (plugin) -> {
availableMigrators.add(new LegacyMigrator(this));
final Plugin mySqlPlayerDataBridge = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
if (mySqlPlayerDataBridge != null) {
availableMigrators.add(new MpdbMigrator(this, mySqlPlayerDataBridge));
if (isDependencyLoaded("MySqlPlayerDataBridge")) {
availableMigrators.add(new MpdbMigrator(this));
}
legacyConverter = new BukkitLegacyConverter(this);
});
// Prepare database connection
this.database = new MySqlDatabase(settings, resourceReader, logger, dataAdapter, eventCannon);
getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the database...");
initialized.set(this.database.initialize());
if (initialized.get()) {
getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the database");
} else {
throw new HuskSyncInitializationException("Failed to establish a connection to the database. " +
"Please check the supplied database credentials in the config file");
}
// Initialize the database
initialize(getSettings().getDatabase().getType().getDisplayName() + " database connection", (plugin) -> {
this.database = switch (settings.getDatabase().getType()) {
case MYSQL, MARIADB -> new MySqlDatabase(this);
case POSTGRES -> new PostgresDatabase(this);
case MONGO -> new MongoDbDatabase(this);
};
this.database.initialize();
});
// Prepare redis connection
// Prepare redis connection
initialize("Redis server connection", (plugin) -> {
this.redisManager = new RedisManager(this);
getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the Redis server...");
initialized.set(this.redisManager.initialize().join());
if (initialized.get()) {
getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the Redis server");
} else {
throw new HuskSyncInitializationException("Failed to establish a connection to the Redis server. " +
"Please check the supplied Redis credentials in the config file");
}
this.redisManager.initialize();
});
// Register events
getLoggingAdapter().log(Level.INFO, "Registering events...");
this.eventListener = new BukkitEventListener(this);
getLoggingAdapter().log(Level.INFO, "Successfully registered events listener");
// Prepare data syncer
initialize("data syncer", (plugin) -> {
dataSyncer = getSettings().getSynchronization().getMode().create(this);
dataSyncer.initialize();
});
// Register permissions
getLoggingAdapter().log(Level.INFO, "Registering permissions & commands...");
Arrays.stream(Permission.values()).forEach(permission -> getServer().getPluginManager()
.addPermission(new org.bukkit.permissions.Permission(permission.node, switch (permission.defaultAccess) {
case EVERYONE -> PermissionDefault.TRUE;
case NOBODY -> PermissionDefault.FALSE;
case OPERATORS -> PermissionDefault.OP;
})));
// Register events
initialize("events", (plugin) -> eventListener.onEnable());
// Register commands
for (final BukkitCommandType bukkitCommandType : BukkitCommandType.values()) {
final PluginCommand pluginCommand = getCommand(bukkitCommandType.commandBase.command);
if (pluginCommand != null) {
new BukkitCommand(bukkitCommandType.commandBase, this).register(pluginCommand);
}
// Register plugin hooks
initialize("hooks", (plugin) -> {
if (isDependencyLoaded("Plan") && getSettings().isEnablePlanHook()) {
new PlanHook(this).hookIntoPlan();
}
getLoggingAdapter().log(Level.INFO, "Successfully registered permissions & commands");
});
// Hook into plan
if (Bukkit.getPluginManager().getPlugin("Plan") != null) {
getLoggingAdapter().log(Level.INFO, "Enabling Plan integration...");
new PlanHook(database, logger).hookIntoPlan();
getLoggingAdapter().log(Level.INFO, "Plan integration enabled!");
}
// Register API
initialize("api", (plugin) -> BukkitHuskSyncAPI.register(this));
// Hook into bStats metrics
try {
new Metrics(this, METRICS_ID);
} catch (final Exception e) {
getLoggingAdapter().log(Level.WARNING, "Skipped bStats metrics initialization due to an exception.");
}
// Check for updates
if (settings.getBooleanValue(Settings.ConfigOption.CHECK_FOR_UPDATES)) {
getLoggingAdapter().log(Level.INFO, "Checking for updates...");
CompletableFuture.runAsync(() -> new UpdateChecker(getPluginVersion(), getLoggingAdapter()).logToConsole());
}
} catch (HuskSyncInitializationException exception) {
getLoggingAdapter().log(Level.SEVERE, exception.getMessage());
initialized.set(false);
} catch (Exception exception) {
getLoggingAdapter().log(Level.SEVERE, "An unhandled exception occurred initializing HuskSync!", exception);
initialized.set(false);
} finally {
// Validate initialization
if (initialized.get()) {
getLoggingAdapter().log(Level.INFO, "Successfully enabled HuskSync v" + getPluginVersion());
} else {
getLoggingAdapter().log(Level.SEVERE, "Failed to initialize HuskSync. The plugin will now be disabled");
getServer().getPluginManager().disablePlugin(this);
}
}
// Hook into bStats and check for updates
initialize("metrics", (plugin) -> this.registerMetrics(METRICS_ID));
this.checkForUpdates();
}
@Override
public void onDisable() {
// Handle shutdown
this.disabling = true;
// Close the event listener / data syncer
if (this.dataSyncer != null) {
this.dataSyncer.terminate();
}
if (this.eventListener != null) {
this.eventListener.handlePluginDisable();
}
getLoggingAdapter().log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion());
// Unregister API and cancel tasks
BukkitHuskSyncAPI.unregister();
this.cancelTasks();
// Complete shutdown
log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion());
}
@NotNull
protected BukkitEventListener createEventListener() {
return new BukkitEventListener(this);
}
@Override
public @NotNull Set<OnlineUser> getOnlineUsers() {
return Bukkit.getOnlinePlayers().stream().map(BukkitPlayer::adapt).collect(Collectors.toSet());
@NotNull
public Set<OnlineUser> getOnlineUsers() {
return getServer().getOnlinePlayers().stream()
.map(player -> BukkitUser.adapt(player, this))
.collect(Collectors.toSet());
}
@Override
public @NotNull Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
final Player player = Bukkit.getPlayer(uuid);
@NotNull
public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
final Player player = getServer().getPlayer(uuid);
if (player == null) {
return Optional.empty();
}
return Optional.of(BukkitPlayer.adapt(player));
return Optional.of(BukkitUser.adapt(player, this));
}
@Override
public @NotNull Database getDatabase() {
return database;
public void setDataSyncer(@NotNull DataSyncer dataSyncer) {
log(Level.INFO, String.format("Switching data syncer to %s", dataSyncer.getClass().getSimpleName()));
this.dataSyncer = dataSyncer;
}
@Override
public @NotNull RedisManager getRedisManager() {
return redisManager;
}
@Override
public @NotNull DataAdapter getDataAdapter() {
return dataAdapter;
}
@Override
public @NotNull DataEditor getDataEditor() {
return dataEditor;
}
@Override
public @NotNull EventCannon getEventCannon() {
return eventCannon;
@NotNull
public Uniform getUniform() {
return BukkitUniform.getInstance(this);
}
@NotNull
@Override
public List<Migrator> getAvailableMigrators() {
return availableMigrators;
public Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
return playerCustomDataStore.compute(
user.getUuid(),
(uuid, data) -> data == null ? Maps.newHashMap() : data
);
}
@Override
public @NotNull Settings getSettings() {
return settings;
@NotNull
public String getServerName() {
return serverName == null ? "server" : serverName.getName();
}
@Override
public @NotNull Locales getLocales() {
return locales;
public boolean isDependencyLoaded(@NotNull String name) {
final Plugin plugin = getServer().getPluginManager().getPlugin(name);
return plugin != null;
}
// Register bStats metrics
public void registerMetrics(int metricsId) {
if (!getPluginVersion().getMetadata().isBlank()) {
return;
}
try {
new Metrics(this, metricsId);
} catch (Throwable e) {
log(Level.WARNING, "Failed to register bStats metrics (%s)".formatted(e.getMessage()));
}
}
@Override
public @NotNull Logger getLoggingAdapter() {
return logger;
public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable) {
if (throwable.length > 0) {
getLogger().log(level, message, throwable[0]);
} else {
getLogger().log(level, message);
}
}
@NotNull
@Override
public Version getPluginVersion() {
return Version.fromString(getDescription().getVersion(), "-");
}
@NotNull
@Override
public Version getMinecraftVersion() {
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
public @NotNull Version getPluginVersion() {
return Version.pluginVersion(getDescription().getVersion());
@NotNull
public String getServerVersion() {
return String.format("%s/%s", getServer().getName(), getServer().getVersion());
}
@Override
public @NotNull Version getMinecraftVersion() {
return Version.minecraftVersion(Bukkit.getBukkitVersion());
public Optional<LegacyConverter> getLegacyConverter() {
return Optional.of(legacyConverter);
}
@Override
public CompletableFuture<Boolean> reload() {
return CompletableFuture.supplyAsync(() -> {
try {
this.settings = Settings.load(YamlDocument.create(new File(getDataFolder(), "config.yml"), Objects.requireNonNull(resourceReader.getResource("config.yml")), GeneralSettings.builder().setUseDefaults(false).build(), LoaderSettings.builder().setAutoUpdate(true).build(), DumperSettings.builder().setEncoding(DumperSettings.Encoding.UNICODE).build(), UpdaterSettings.builder().setVersioning(new BasicVersioning("config_version")).build()));
this.locales = Locales.load(YamlDocument.create(new File(getDataFolder(), "messages-" + settings.getStringValue(Settings.ConfigOption.LANGUAGE) + ".yml"), Objects.requireNonNull(resourceReader.getResource("locales/" + settings.getStringValue(Settings.ConfigOption.LANGUAGE) + ".yml"))));
return true;
} catch (IOException | NullPointerException e) {
getLoggingAdapter().log(Level.SEVERE, "Failed to load data from the config", e);
return false;
}
});
@NotNull
public LockedHandler getLockedHandler() {
return eventListener.getLockedHandler();
}
@NotNull
public GracefulScheduling getScheduler() {
return paperLib.scheduling();
}
@NotNull
public AsynchronousScheduler getAsyncScheduler() {
return asyncScheduler == null
? asyncScheduler = getScheduler().asyncScheduler() : asyncScheduler;
}
@NotNull
public RegionalScheduler getSyncScheduler() {
return regionalScheduler == null
? regionalScheduler = getScheduler().globalRegionalScheduler() : regionalScheduler;
}
@NotNull
public AttachedScheduler getUserSyncScheduler(@NotNull UserDataHolder user) {
return getScheduler().entitySpecificScheduler(((BukkitUser) user).getPlayer());
}
@Override
@NotNull
public Path getConfigDirectory() {
return getDataFolder().toPath();
}
@Override
@NotNull
public BukkitHuskSync getPlugin() {
return this;
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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;
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;
import net.william278.uniform.paper.PaperUniform;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
@SuppressWarnings({"unused"})
public class PaperHuskSync extends BukkitHuskSync {
@NotNull
@Override
protected BukkitEventListener createEventListener() {
return new PaperEventListener(this);
}
@NotNull
@Override
public Audience getAudience(@NotNull UUID user) {
final Player player = getServer().getPlayer(user);
return player == null || !player.isOnline() ? Audience.empty() : player;
}
@NotNull
@Override
public Version getMinecraftVersion() {
return Version.fromString(getServer().getMinecraftVersion());
}
@Override
@NotNull
public Uniform getUniform() {
return PaperUniform.getInstance(this);
}
}

View File

@@ -0,0 +1,82 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync;
import de.exlll.configlib.Configuration;
import de.exlll.configlib.YamlConfigurations;
import io.papermc.paper.plugin.loader.PluginClasspathBuilder;
import io.papermc.paper.plugin.loader.PluginLoader;
import io.papermc.paper.plugin.loader.library.impl.MavenLibraryResolver;
import lombok.NoArgsConstructor;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.repository.RemoteRepository;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.InputStream;
import java.util.List;
import java.util.Objects;
@NoArgsConstructor
@SuppressWarnings("UnstableApiUsage")
public class PaperHuskSyncLoader implements PluginLoader {
@Override
public void classloader(@NotNull PluginClasspathBuilder classpathBuilder) {
final MavenLibraryResolver resolver = new MavenLibraryResolver();
resolveLibraries(classpathBuilder).stream()
.map(DefaultArtifact::new)
.forEach(artifact -> resolver.addDependency(new Dependency(artifact, null)));
resolver.addRepository(new RemoteRepository.Builder(
"maven", "default", "https://repo.maven.apache.org/maven2/"
).build());
classpathBuilder.addLibrary(resolver);
}
@NotNull
private static List<String> resolveLibraries(@NotNull PluginClasspathBuilder classpathBuilder) {
try (InputStream input = getLibraryListFile()) {
return YamlConfigurations.read(
Objects.requireNonNull(input, "Failed to read libraries file"),
PaperLibraries.class
).libraries;
} catch (Throwable e) {
classpathBuilder.getContext().getLogger().error("Failed to resolve libraries", e);
}
return List.of();
}
@Nullable
private static InputStream getLibraryListFile() {
return PaperHuskSyncLoader.class.getClassLoader().getResourceAsStream("paper-libraries.yml");
}
@Configuration
@NoArgsConstructor
public static class PaperLibraries {
private List<String> libraries;
}
}

View File

@@ -0,0 +1,264 @@
/*
* 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.api;
import net.william278.desertwell.util.ThrowingConsumer;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.data.BukkitData;
import net.william278.husksync.data.DataHolder;
import net.william278.husksync.user.BukkitUser;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
/**
* The HuskSync API implementation for the Bukkit platform
* </p>
* Retrieve an instance of the API class via {@link #getInstance()}.
*/
@SuppressWarnings("unused")
public class BukkitHuskSyncAPI extends HuskSyncAPI {
/**
* <b>(Internal use only)</b> - Constructor, instantiating the API.
*/
@ApiStatus.Internal
private BukkitHuskSyncAPI(@NotNull BukkitHuskSync plugin) {
super(plugin);
}
/**
* Entrypoint to the HuskSync API on the bukkit platform - returns an instance of the API
*
* @return instance of the HuskSync API
* @since 3.0
*/
@NotNull
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.");
}
if (instance == null) {
throw new NotRegisteredException();
}
return (BukkitHuskSyncAPI) instance;
}
/**
* <b>(Internal use only)</b> - Register the API for this platform.
*
* @param plugin the plugin instance
* @since 3.0
*/
@ApiStatus.Internal
public static void register(@NotNull BukkitHuskSync plugin) {
instance = new BukkitHuskSyncAPI(plugin);
}
/**
* Returns a {@link OnlineUser} instance for the given bukkit {@link Player}.
*
* @param player the bukkit player to get the {@link OnlineUser} instance for
* @return the {@link OnlineUser} instance for the given bukkit {@link Player}
* @since 2.0
*/
@NotNull
public BukkitUser getUser(@NotNull Player player) {
return BukkitUser.adapt(player, plugin);
}
/**
* Get the current {@link BukkitData.Items.Inventory} of the given {@link User}
*
* @param user the user to get the inventory of
* @return the {@link BukkitData.Items.Inventory} of the given {@link User}
* @since 3.0
*/
public CompletableFuture<Optional<BukkitData.Items.Inventory>> getCurrentInventory(@NotNull User user) {
return getCurrentData(user).thenApply(data -> data.flatMap(DataHolder::getInventory)
.map(BukkitData.Items.Inventory.class::cast));
}
/**
* Get the current {@link BukkitData.Items.Inventory} of the given {@link Player}
*
* @param user the user to get the inventory of
* @return the {@link BukkitData.Items.Inventory} of the given {@link Player}
* @since 3.0
*/
public CompletableFuture<Optional<ItemStack[]>> getCurrentInventoryContents(@NotNull User user) {
return getCurrentInventory(user)
.thenApply(inventory -> inventory.map(BukkitData.Items.Inventory::getContents));
}
/**
* Set the current {@link BukkitData.Items.Inventory} of the given {@link User}
*
* @param user the user to set the inventory of
* @param contents the contents to set the inventory to
* @since 3.0
*/
public void setCurrentInventory(@NotNull User user, @NotNull BukkitData.Items.Inventory contents) {
editCurrentData(user, dataHolder -> dataHolder.setInventory(contents));
}
/**
* Set the current {@link BukkitData.Items.Inventory} of the given {@link User}
*
* @param user the user to set the inventory of
* @param contents the contents to set the inventory to
* @since 3.0
*/
public void setCurrentInventoryContents(@NotNull User user, @NotNull ItemStack[] contents) {
editCurrentData(
user,
dataHolder -> dataHolder.getInventory().ifPresent(
inv -> inv.setContents(adaptItems(contents))
)
);
}
/**
* Edit the current {@link BukkitData.Items.Inventory} of the given {@link User}
*
* @param user the user to edit the inventory of
* @param editor the editor to apply to the inventory
* @since 3.0
*/
public void editCurrentInventory(@NotNull User user, ThrowingConsumer<BukkitData.Items.Inventory> editor) {
editCurrentData(user, dataHolder -> dataHolder.getInventory()
.map(BukkitData.Items.Inventory.class::cast)
.ifPresent(editor));
}
/**
* Edit the current {@link BukkitData.Items.Inventory} of the given {@link User}
*
* @param user the user to edit the inventory of
* @param editor the editor to apply to the inventory
* @since 3.0
*/
public void editCurrentInventoryContents(@NotNull User user, ThrowingConsumer<ItemStack[]> editor) {
editCurrentData(user, dataHolder -> dataHolder.getInventory()
.map(BukkitData.Items.Inventory.class::cast)
.ifPresent(inventory -> editor.accept(inventory.getContents())));
}
/**
* Get the current {@link BukkitData.Items.EnderChest} of the given {@link User}
*
* @param user the user to get the ender chest of
* @return the {@link BukkitData.Items.EnderChest} of the given {@link User}, or {@link Optional#empty()} if the
* user data could not be found
* @since 3.0
*/
public CompletableFuture<Optional<BukkitData.Items.EnderChest>> getCurrentEnderChest(@NotNull User user) {
return getCurrentData(user).thenApply(data -> data.flatMap(DataHolder::getEnderChest)
.map(BukkitData.Items.EnderChest.class::cast));
}
/**
* Get the current {@link BukkitData.Items.EnderChest} of the given {@link Player}
*
* @param user the user to get the ender chest of
* @return the {@link BukkitData.Items.EnderChest} of the given {@link Player}, or {@link Optional#empty()} if the
* user data could not be found
* @since 3.0
*/
public CompletableFuture<Optional<ItemStack[]>> getCurrentEnderChestContents(@NotNull User user) {
return getCurrentEnderChest(user)
.thenApply(enderChest -> enderChest.map(BukkitData.Items.EnderChest::getContents));
}
/**
* Set the current {@link BukkitData.Items.EnderChest} of the given {@link User}
*
* @param user the user to set the ender chest of
* @param contents the contents to set the ender chest to
* @since 3.0
*/
public void setCurrentEnderChest(@NotNull User user, @NotNull BukkitData.Items.EnderChest contents) {
editCurrentData(user, dataHolder -> dataHolder.setEnderChest(contents));
}
/**
* Set the current {@link BukkitData.Items.EnderChest} of the given {@link User}
*
* @param user the user to set the ender chest of
* @param contents the contents to set the ender chest to
* @since 3.0
*/
public void setCurrentEnderChestContents(@NotNull User user, @NotNull ItemStack[] contents) {
editCurrentData(
user,
dataHolder -> dataHolder.getEnderChest().ifPresent(
enderChest -> enderChest.setContents(adaptItems(contents))
)
);
}
/**
* Edit the current {@link BukkitData.Items.EnderChest} of the given {@link User}
*
* @param user the user to edit the ender chest of
* @param editor the editor to apply to the ender chest
* @since 3.0
*/
public void editCurrentEnderChest(@NotNull User user, Consumer<BukkitData.Items.EnderChest> editor) {
editCurrentData(user, dataHolder -> dataHolder.getEnderChest()
.map(BukkitData.Items.EnderChest.class::cast)
.ifPresent(editor));
}
/**
* Edit the current {@link BukkitData.Items.EnderChest} of the given {@link User}
*
* @param user the user to edit the ender chest of
* @param editor the editor to apply to the ender chest
* @since 3.0
*/
public void editCurrentEnderChestContents(@NotNull User user, Consumer<ItemStack[]> editor) {
editCurrentData(user, dataHolder -> dataHolder.getEnderChest()
.map(BukkitData.Items.EnderChest.class::cast)
.ifPresent(enderChest -> editor.accept(enderChest.getContents())));
}
/**
* Adapts an array of {@link ItemStack} to a {@link BukkitData.Items} instance
*
* @param contents the contents to adapt
* @return the adapted {@link BukkitData.Items} instance
* @since 3.0
*/
@NotNull
public BukkitData.Items adaptItems(@NotNull ItemStack[] contents) {
return BukkitData.Items.ItemArray.adapt(contents);
}
}

View File

@@ -1,71 +0,0 @@
package net.william278.husksync.command;
import net.william278.husksync.HuskSync;
import net.william278.husksync.player.BukkitPlayer;
import org.bukkit.command.*;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
/**
* Bukkit executor that implements and executes {@link CommandBase}s
*/
public class BukkitCommand implements CommandExecutor, TabExecutor {
/**
* The {@link CommandBase} that will be executed
*/
private final CommandBase command;
/**
* The implementing plugin
*/
private final HuskSync plugin;
public BukkitCommand(@NotNull CommandBase command, @NotNull HuskSync implementor) {
this.command = command;
this.plugin = implementor;
}
/**
* Registers a {@link PluginCommand} to this implementation
*
* @param pluginCommand {@link PluginCommand} to register
*/
public void register(@NotNull PluginCommand pluginCommand) {
pluginCommand.setExecutor(this);
pluginCommand.setTabCompleter(this);
pluginCommand.setPermission(command.permission);
pluginCommand.setDescription(command.getDescription());
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (sender instanceof Player player) {
this.command.onExecute(BukkitPlayer.adapt(player), args);
} else {
if (this.command instanceof ConsoleExecutable consoleExecutable) {
consoleExecutable.onConsoleExecute(args);
} else {
plugin.getLocales().getLocale("error_in_game_command_only").
ifPresent(locale -> sender.spigot().sendMessage(locale.toComponent()));
}
}
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] args) {
if (this.command instanceof TabCompletable tabCompletable) {
return tabCompletable.onTabComplete(args);
}
return Collections.emptyList();
}
}

View File

@@ -1,21 +0,0 @@
package net.william278.husksync.command;
import net.william278.husksync.BukkitHuskSync;
import org.jetbrains.annotations.NotNull;
/**
* Commands available on the Bukkit HuskSync implementation
*/
public enum BukkitCommandType {
HUSKSYNC_COMMAND(new HuskSyncCommand(BukkitHuskSync.getInstance())),
USERDATA_COMMAND(new UserDataCommand(BukkitHuskSync.getInstance())),
INVENTORY_COMMAND(new InventoryCommand(BukkitHuskSync.getInstance())),
ENDER_CHEST_COMMAND(new EnderChestCommand(BukkitHuskSync.getInstance()));
public final CommandBase commandBase;
BukkitCommandType(@NotNull CommandBase commandBase) {
this.commandBase = commandBase;
}
}

View File

@@ -0,0 +1,876 @@
/*
* 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.data;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gson.annotations.SerializedName;
import de.tr7zw.changeme.nbtapi.NBTCompound;
import de.tr7zw.changeme.nbtapi.NBTPersistentDataContainer;
import lombok.*;
import net.william278.desertwell.util.ThrowingConsumer;
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.*;
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;
//#if MC==12001
//$$ import org.bukkit.inventory.EquipmentSlot;
//#else
import org.bukkit.inventory.EquipmentSlotGroup;
//#endif
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.potion.PotionEffect;
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;
import java.util.stream.Collectors;
import static net.william278.husksync.util.BukkitKeyedAdapter.*;
public abstract class BukkitData implements Data {
@Override
public final void apply(@NotNull UserDataHolder dataHolder, @NotNull HuskSync plugin) throws IllegalStateException {
this.apply((BukkitUser) dataHolder, (BukkitHuskSync) plugin);
}
public abstract void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException;
@Getter
public static abstract class Items extends BukkitData implements Data.Items {
private final @Nullable ItemStack @NotNull [] contents;
private Items(@Nullable ItemStack @NotNull [] contents) {
this.contents = Arrays.stream(contents.clone())
.map(i -> i == null || i.getType() == Material.AIR ? null : i)
.toArray(ItemStack[]::new);
}
@Nullable
@Override
public Stack @NotNull [] getStack() {
return Arrays.stream(contents)
.map(stack -> stack != null ? new Stack(
stack.getType().getKey().toString(),
stack.getAmount(),
stack.hasItemMeta() ? (Objects.requireNonNull(
stack.getItemMeta()).hasDisplayName() ? stack.getItemMeta().getDisplayName() : null)
: null,
stack.hasItemMeta() ? (Objects.requireNonNull(
stack.getItemMeta()).hasLore() ? stack.getItemMeta().getLore() : null)
: null,
stack.hasItemMeta() && Objects.requireNonNull(stack.getItemMeta()).hasEnchants() ?
stack.getItemMeta().getEnchants().keySet().stream()
.map(enchantment -> enchantment.getKey().getKey())
.toList()
: List.of()
) : null)
.toArray(Stack[]::new);
}
@Override
public void clear() {
Arrays.fill(contents, null);
}
@Override
public void setContents(@NotNull Data.Items contents) {
this.setContents(((BukkitData.Items) contents).getContents());
}
public void setContents(@Nullable ItemStack @NotNull [] contents) {
// Ensure the array is the correct length for the inventory
if (contents.length != this.contents.length) {
contents = Arrays.copyOf(contents, this.contents.length);
}
System.arraycopy(contents, 0, this.contents, 0, this.contents.length);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof BukkitData.Items items) {
return Arrays.equals(contents, items.getContents());
}
return false;
}
@Setter
@Getter
public static class Inventory extends BukkitData.Items implements Data.Items.Inventory {
@Range(from = 0, to = 8)
private int heldItemSlot;
private Inventory(@Nullable ItemStack @NotNull [] contents, int heldItemSlot) {
super(contents);
this.heldItemSlot = heldItemSlot;
}
@NotNull
public static BukkitData.Items.Inventory from(@Nullable ItemStack @NotNull [] contents, int heldItemSlot) {
return new BukkitData.Items.Inventory(contents, heldItemSlot);
}
@NotNull
public static BukkitData.Items.Inventory empty() {
return new BukkitData.Items.Inventory(new ItemStack[INVENTORY_SLOT_COUNT], 0);
}
@Override
public int getSlotCount() {
return INVENTORY_SLOT_COUNT;
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
final Player player = user.getPlayer();
this.clearInventoryCraftingSlots(player);
player.setItemOnCursor(null);
player.getInventory().setContents(plugin.setMapViews(getContents()));
player.getInventory().setHeldItemSlot(heldItemSlot);
//noinspection UnstableApiUsage
player.updateInventory();
}
private void clearInventoryCraftingSlots(@NotNull Player player) {
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);
}
}
}
}
public static class EnderChest extends BukkitData.Items implements Data.Items.EnderChest {
private EnderChest(@Nullable ItemStack @NotNull [] contents) {
super(contents);
}
@NotNull
public static BukkitData.Items.EnderChest adapt(@Nullable ItemStack @NotNull [] contents) {
return new BukkitData.Items.EnderChest(contents);
}
@NotNull
public static BukkitData.Items.EnderChest adapt(@NotNull Collection<ItemStack> items) {
return adapt(items.toArray(ItemStack[]::new));
}
@NotNull
public static BukkitData.Items.EnderChest empty() {
return new BukkitData.Items.EnderChest(new ItemStack[ENDER_CHEST_SLOT_COUNT]);
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
user.getPlayer().getEnderChest().setContents(plugin.setMapViews(getContents()));
}
}
public static class ItemArray extends BukkitData.Items implements Data.Items {
private ItemArray(@Nullable ItemStack @NotNull [] contents) {
super(contents);
}
@NotNull
public static ItemArray adapt(@NotNull Collection<ItemStack> drops) {
return new ItemArray(drops.toArray(ItemStack[]::new));
}
@NotNull
public static ItemArray adapt(@Nullable ItemStack @NotNull [] drops) {
return new ItemArray(drops);
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
throw new UnsupportedOperationException("A generic item array cannot be applied to a player");
}
}
}
@Getter
@Setter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class PotionEffects extends BukkitData implements Data.PotionEffects {
private final Collection<PotionEffect> effects;
@NotNull
public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> sei) {
return new BukkitData.PotionEffects(Lists.newArrayList(sei.stream().filter(e -> !e.isAmbient()).toList()));
}
@NotNull
public static BukkitData.PotionEffects adapt(@NotNull Collection<Effect> effects) {
return from(effects.stream()
.map(effect -> {
final PotionEffectType type = matchEffectType(effect.type());
return type != null ? new PotionEffect(
type,
effect.duration(),
effect.amplifier(),
effect.isAmbient(),
effect.showParticles(),
effect.hasIcon()
) : null;
})
.filter(Objects::nonNull)
.toList());
}
@NotNull
@SuppressWarnings("unused")
public static BukkitData.PotionEffects empty() {
return new BukkitData.PotionEffects(Lists.newArrayList());
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
final Player player = user.getPlayer();
for (PotionEffect effect : player.getActivePotionEffects()) {
player.removePotionEffect(effect.getType());
}
for (PotionEffect effect : this.getEffects()) {
player.addPotionEffect(effect);
}
}
@NotNull
@Override
@Unmodifiable
public List<Effect> getActiveEffects() {
return effects.stream()
.map(potionEffect -> new Effect(
potionEffect.getType().getKey().toString(),
potionEffect.getAmplifier(),
potionEffect.getDuration(),
potionEffect.isAmbient(),
potionEffect.hasParticles(),
potionEffect.hasIcon()
))
.toList();
}
}
@Getter
@Setter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Advancements extends BukkitData implements Data.Advancements {
private List<Advancement> completed;
// Iterate through the server advancement set and add all advancements to the list
@NotNull
public static BukkitData.Advancements adapt(@NotNull Player player) {
final List<Advancement> advancements = Lists.newArrayList();
forEachAdvancement(advancement -> {
final AdvancementProgress advancementProgress = player.getAdvancementProgress(advancement);
final Map<String, Date> awardedCriteria = Maps.newHashMap();
advancementProgress.getAwardedCriteria().forEach(criteriaKey -> awardedCriteria.put(criteriaKey,
advancementProgress.getDateAwarded(criteriaKey)));
// Only save the advancement if criteria has been completed
if (!awardedCriteria.isEmpty()) {
advancements.add(Advancement.adapt(advancement.getKey().toString(), awardedCriteria));
}
});
return new BukkitData.Advancements(advancements);
}
@NotNull
public static BukkitData.Advancements from(@NotNull List<Advancement> advancements) {
return new BukkitData.Advancements(advancements);
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
plugin.runAsync(() -> forEachAdvancement(advancement -> {
final Player player = user.getPlayer();
final AdvancementProgress progress = player.getAdvancementProgress(advancement);
final Optional<Advancement> record = completed.stream()
.filter(r -> r.getKey().equals(advancement.getKey().toString()))
.findFirst();
if (record.isEmpty()) {
this.setAdvancement(plugin, advancement, player, user, List.of(), progress.getAwardedCriteria());
return;
}
final Map<String, Date> criteria = record.get().getCompletedCriteria();
this.setAdvancement(
plugin, advancement, player, user,
criteria.keySet().stream().filter(key -> !progress.getAwardedCriteria().contains(key)).toList(),
progress.getAwardedCriteria().stream().filter(key -> !criteria.containsKey(key)).toList()
);
}));
}
private void setAdvancement(@NotNull HuskSync plugin,
@NotNull org.bukkit.advancement.Advancement advancement,
@NotNull Player player,
@NotNull BukkitUser user,
@NotNull Collection<String> toAward,
@NotNull Collection<String> toRevoke) {
plugin.runSync(() -> {
// Track player exp level & progress
final int expLevel = player.getLevel();
final float expProgress = player.getExp();
// Award and revoke advancement criteria
final AdvancementProgress progress = player.getAdvancementProgress(advancement);
toAward.forEach(progress::awardCriteria);
toRevoke.forEach(progress::revokeCriteria);
// Set player experience and level (prevent advancement awards applying twice), reset game rule
if (!toAward.isEmpty()
&& (player.getLevel() != expLevel || player.getExp() != expProgress)) {
player.setLevel(expLevel);
player.setExp(expProgress);
}
}, user);
}
// Performs a consuming function for every advancement registered on the server
private static void forEachAdvancement(@NotNull ThrowingConsumer<org.bukkit.advancement.Advancement> consumer) {
final StringJoiner joiner = new StringJoiner(", ");
Bukkit.getServer().advancementIterator().forEachRemaining(a -> joiner.add(a.toString()));
Bukkit.getLogger().log(Level.INFO, "Advancements: %s".formatted(joiner.toString()));
Bukkit.getServer().advancementIterator().forEachRemaining(consumer);
}
}
@Getter
@Setter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Location extends BukkitData implements Data.Location, Adaptable {
@SerializedName("x")
private double x;
@SerializedName("y")
private double y;
@SerializedName("z")
private double z;
@SerializedName("yaw")
private float yaw;
@SerializedName("pitch")
private float pitch;
@SerializedName("world")
private World world;
@NotNull
public static BukkitData.Location from(double x, double y, double z,
float yaw, float pitch, @NotNull World world) {
return new BukkitData.Location(x, y, z, yaw, pitch, world);
}
@NotNull
public static BukkitData.Location adapt(@NotNull org.bukkit.Location location) {
return from(
location.getX(),
location.getY(),
location.getZ(),
location.getYaw(),
location.getPitch(),
new World(
Objects.requireNonNull(location.getWorld(), "World is null").getName(),
location.getWorld().getUID(),
location.getWorld().getEnvironment().name()
)
);
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
try {
final org.bukkit.Location location = new org.bukkit.Location(
Bukkit.getWorld(world.name()), x, y, z, yaw, pitch
);
user.getPlayer().teleport(location);
} catch (Throwable e) {
throw new IllegalStateException("Failed to apply location", e);
}
}
}
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Statistics extends BukkitData implements Data.Statistics, Adaptable {
@SerializedName("generic")
private Map<String, Integer> genericStatistics;
@SerializedName("blocks")
private Map<String, Map<String, Integer>> blockStatistics;
@SerializedName("items")
private Map<String, Map<String, Integer>> itemStatistics;
@SerializedName("entities")
private Map<String, Map<String, Integer>> entityStatistics;
@NotNull
public static BukkitData.Statistics adapt(@NotNull Player player) {
final Map<String, Integer> generic = Maps.newHashMap();
final Map<String, Map<String, Integer>> blocks = Maps.newHashMap(),
items = Maps.newHashMap(), entities = Maps.newHashMap();
Registry.STATISTIC.forEach(id -> {
switch (id.getType()) {
case UNTYPED -> addStatistic(player, id, generic);
// 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);
}
@NotNull
public static BukkitData.Statistics from(@NotNull Map<String, Integer> generic,
@NotNull Map<String, Map<String, Integer>> blocks,
@NotNull Map<String, Map<String, Integer>> items,
@NotNull Map<String, Map<String, Integer>> entities) {
return new BukkitData.Statistics(generic, blocks, items, entities);
}
private static void addStatistic(@NotNull Player p, @NotNull Statistic id, @NotNull Map<String, Integer> map) {
final int stat = p.getStatistic(id);
if (stat != 0) {
map.put(id.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 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 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);
if (stat == null) {
return;
}
try {
switch (type) {
case UNTYPED -> player.setStatistic(stat, value);
case BLOCK, ITEM -> player.setStatistic(stat, Objects.requireNonNull(matchMaterial(key[0])), value);
case ENTITY -> player.setStatistic(stat, Objects.requireNonNull(matchEntityType(key[0])), value);
}
} catch (Throwable a) {
plugin.log(Level.WARNING, "Failed to apply statistic " + id, a);
}
}
}
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class PersistentData extends BukkitData implements Data.PersistentData {
private final NBTCompound persistentData;
@NotNull
public static BukkitData.PersistentData adapt(@NotNull PersistentDataContainer persistentData) {
return new BukkitData.PersistentData(new NBTPersistentDataContainer(persistentData));
}
@NotNull
public static BukkitData.PersistentData from(@NotNull NBTCompound compound) {
return new BukkitData.PersistentData(compound);
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
final NBTPersistentDataContainer container = new NBTPersistentDataContainer(
user.getPlayer().getPersistentDataContainer()
);
container.clearNBT();
container.mergeCompound(persistentData);
}
}
@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;
@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 (settings.isIgnoredAttribute(id.getKey().toString()) || instance == null) {
return; // We don't sync attributes not marked as to be synced
}
attributes.add(adapt(instance, settings));
});
return new BukkitData.Attributes(attributes);
}
public Optional<Attribute> getAttribute(@NotNull org.bukkit.attribute.Attribute id) {
return attributes.stream().filter(attribute -> attribute.name().equals(id.getKey().toString())).findFirst();
}
@SuppressWarnings("unused")
public Optional<Attribute> getAttribute(@NotNull String key) {
final org.bukkit.attribute.Attribute attribute = matchAttribute(key);
if (attribute == null) {
return Optional.empty();
}
return getAttribute(attribute);
}
@NotNull
private static Attribute adapt(@NotNull AttributeInstance instance, @NotNull AttributeSettings settings) {
return new Attribute(
instance.getAttribute().getKey().toString(),
instance.getBaseValue(),
instance.getModifiers().stream()
.filter(modifier -> !settings.isIgnoredModifier(modifier.getName()))
//#if MC==12001
//$$ .filter(modifier -> modifier.getSlot() == null)
//#else
.filter(modifier -> modifier.getSlotGroup() != EquipmentSlotGroup.ANY)
//#endif
.map(BukkitData.Attributes::adapt).collect(Collectors.toSet())
);
}
@NotNull
private static Modifier adapt(@NotNull AttributeModifier modifier) {
//#if MC==12001
//$$ return new Modifier(
//$$ modifier.getUniqueId(),
//$$ modifier.getName(),
//$$ modifier.getAmount(),
//$$ modifier.getOperation().ordinal(),
//$$ modifier.getSlot() != null ? modifier.getSlot().ordinal() : -1
//$$ );
//#else
return new Modifier(
modifier.getKey().toString(),
modifier.getAmount(),
modifier.getOperation().ordinal(),
modifier.getSlotGroup().toString()
);
//#endif
}
private static void applyAttribute(@Nullable AttributeInstance instance, @Nullable Attribute attribute) {
if (instance == null) {
return;
}
instance.getModifiers().forEach(instance::removeModifier);
instance.setBaseValue(attribute == null ? instance.getValue() : attribute.baseValue());
if (attribute != 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) {
//#if MC==12001
//$$ return new AttributeModifier(
//$$ modifier.uuid(),
//$$ modifier.name(),
//$$ modifier.amount(),
//$$ AttributeModifier.Operation.values()[modifier.operation()],
//$$ modifier.equipmentSlot() != -1 ? EquipmentSlot.values()[modifier.equipmentSlot()] : null
//$$ );
//#else
return new AttributeModifier(
Objects.requireNonNull(NamespacedKey.fromString(modifier.name())),
modifier.amount(),
AttributeModifier.Operation.values()[modifier.operation()],
Optional.ofNullable(EquipmentSlotGroup.getByName(modifier.slotGroup())).orElse(EquipmentSlotGroup.ANY)
);
//#endif
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
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
@Setter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Health extends BukkitData implements Data.Health, Adaptable {
@SerializedName("health")
private double health;
@SerializedName("health_scale")
private double healthScale;
@SerializedName("is_health_scaled")
private boolean isHealthScaled;
@NotNull
public static BukkitData.Health from(double health, double scale, boolean isScaled) {
return new BukkitData.Health(health, scale, isScaled);
}
/**
* @deprecated Use {@link #from(double, double, boolean)} instead
*/
@NotNull
@Deprecated(since = "3.5.4")
public static BukkitData.Health from(double health, double scale) {
return from(health, scale, false);
}
/**
* @deprecated Use {@link #from(double, double, boolean)} instead
*/
@NotNull
@Deprecated(forRemoval = true, since = "3.5")
public static BukkitData.Health from(double health, @SuppressWarnings("unused") double max, double scale) {
return from(health, scale, false);
}
@NotNull
public static BukkitData.Health adapt(@NotNull Player player) {
return from(
player.getHealth(),
player.getHealthScale(),
player.isHealthScaled()
);
}
@Override
@SuppressWarnings("deprecation")
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
final Player player = user.getPlayer();
// Set health
try {
player.setHealth(Math.min(health, player.getMaxHealth()));
} catch (Throwable e) {
plugin.log(Level.WARNING, "Error setting %s's health to %s".formatted(player.getName(), health), e);
}
// Set health scale
double scale = healthScale <= 0 ? player.getMaxHealth() : healthScale;
try {
player.setHealthScale(scale);
player.setHealthScaled(isHealthScaled);
} catch (Throwable e) {
plugin.log(Level.WARNING, "Error setting %s's health scale to %s".formatted(player.getName(), scale), e);
}
}
}
@Getter
@Setter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Hunger extends BukkitData implements Data.Hunger, Adaptable {
@SerializedName("food_level")
private int foodLevel;
@SerializedName("saturation")
private float saturation;
@SerializedName("exhaustion")
private float exhaustion;
@NotNull
public static BukkitData.Hunger adapt(@NotNull Player player) {
return from(player.getFoodLevel(), player.getSaturation(), player.getExhaustion());
}
@NotNull
public static BukkitData.Hunger from(int foodLevel, float saturation, float exhaustion) {
return new BukkitData.Hunger(foodLevel, saturation, exhaustion);
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
final Player player = user.getPlayer();
player.setFoodLevel(foodLevel);
player.setSaturation(saturation);
player.setExhaustion(exhaustion);
}
}
@Getter
@Setter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Experience extends BukkitData implements Data.Experience, Adaptable {
@SerializedName("total_experience")
private int totalExperience;
@SerializedName("exp_level")
private int expLevel;
@SerializedName("exp_progress")
private float expProgress;
@NotNull
public static BukkitData.Experience from(int totalExperience, int expLevel, float expProgress) {
return new BukkitData.Experience(totalExperience, expLevel, expProgress);
}
@NotNull
public static BukkitData.Experience adapt(@NotNull Player player) {
return from(player.getTotalExperience(), player.getLevel(), player.getExp());
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
final Player player = user.getPlayer();
player.setTotalExperience(totalExperience);
player.setLevel(expLevel);
player.setExp(expProgress);
}
}
@Getter
@Setter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class GameMode extends BukkitData implements Data.GameMode, Adaptable {
@SerializedName("game_mode")
private String gameMode;
@NotNull
public static BukkitData.GameMode from(@NotNull String gameMode) {
return new BukkitData.GameMode(gameMode);
}
@NotNull
@Deprecated(forRemoval = true, since = "3.5")
@SuppressWarnings("unused")
public static BukkitData.GameMode from(@NotNull String gameMode, boolean allowFlight, boolean isFlying) {
return new BukkitData.GameMode(gameMode);
}
@NotNull
public static BukkitData.GameMode adapt(@NotNull Player player) {
return from(player.getGameMode().name());
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
user.getPlayer().setGameMode(org.bukkit.GameMode.valueOf(gameMode));
}
}
@Getter
@Setter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class FlightStatus extends BukkitData implements Data.FlightStatus, Adaptable {
@SerializedName("allow_flight")
private boolean allowFlight;
@SerializedName("is_flying")
private boolean flying;
@NotNull
public static BukkitData.FlightStatus from(boolean allowFlight, boolean flying) {
return new BukkitData.FlightStatus(allowFlight, allowFlight && flying);
}
@NotNull
public static BukkitData.FlightStatus adapt(@NotNull Player player) {
return from(player.getAllowFlight(), player.isFlying());
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
final Player player = user.getPlayer();
player.setAllowFlight(allowFlight);
player.setFlying(allowFlight && flying);
}
}
}

View File

@@ -1,126 +0,0 @@
package net.william278.husksync.data;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
/**
* A mapped player inventory, providing methods to easily access a player's inventory.
*/
@SuppressWarnings("unused")
public class BukkitInventoryMap {
private ItemStack[] contents;
/**
* Creates a new mapped inventory from the given contents.
*
* @param contents the contents of the inventory
*/
protected BukkitInventoryMap(ItemStack[] contents) {
this.contents = contents;
}
/**
* Gets the contents of the inventory.
*
* @return the contents of the inventory
*/
public ItemStack[] getContents() {
return contents;
}
/**
* Set the contents of the inventory.
*
* @param contents the contents of the inventory
*/
public void setContents(ItemStack[] contents) {
this.contents = contents;
}
/**
* Gets the size of the inventory.
*
* @return the size of the inventory
*/
public int getSize() {
return contents.length;
}
/**
* Gets the item at the given index.
*
* @param index the index of the item to get
* @return the item at the given index
*/
public Optional<ItemStack> getItemAt(int index) {
if (contents.length >= index) {
if (contents[index] == null) {
return Optional.empty();
}
return Optional.of(contents[index]);
}
return Optional.empty();
}
/**
* Sets the item at the given index.
*
* @param itemStack the item to set at the given index
* @param index the index of the item to set
* @throws IllegalArgumentException if the index is out of bounds
*/
public void setItemAt(@NotNull ItemStack itemStack, int index) throws IllegalArgumentException {
contents[index] = itemStack;
}
/**
* Returns the main inventory contents.
*
* @return the main inventory contents
*/
public ItemStack[] getInventory() {
final ItemStack[] inventory = new ItemStack[36];
System.arraycopy(contents, 0, inventory, 0, Math.min(contents.length, inventory.length));
return inventory;
}
public ItemStack[] getHotbar() {
final ItemStack[] armor = new ItemStack[9];
for (int i = 0; i <= 9; i++) {
armor[i] = getItemAt(i).orElse(null);
}
return armor;
}
public Optional<ItemStack> getOffHand() {
return getItemAt(40);
}
public Optional<ItemStack> getHelmet() {
return getItemAt(39);
}
public Optional<ItemStack> getChestplate() {
return getItemAt(38);
}
public Optional<ItemStack> getLeggings() {
return getItemAt(37);
}
public Optional<ItemStack> getBoots() {
return getItemAt(36);
}
public ItemStack[] getArmor() {
final ItemStack[] armor = new ItemStack[4];
for (int i = 36; i < 40; i++) {
armor[i - 36] = getItemAt(i).orElse(null);
}
return armor;
}
}

View File

@@ -1,218 +1,250 @@
/*
* 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.data;
import com.google.gson.reflect.TypeToken;
import de.tr7zw.changeme.nbtapi.NBT;
import de.tr7zw.changeme.nbtapi.NBTCompound;
import de.tr7zw.changeme.nbtapi.NBTContainer;
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBTCompoundList;
import de.tr7zw.changeme.nbtapi.utils.DataFixerUtil;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
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.api.HuskSyncAPI;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.List;
import static net.william278.husksync.data.BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
import static net.william278.husksync.data.Data.Items.Inventory.HELD_ITEM_SLOT_TAG;
import static net.william278.husksync.data.Data.Items.Inventory.ITEMS_TAG;
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class BukkitSerializer {
/**
* Returns a serialized array of {@link ItemStack}s
*
* @param inventoryContents The contents of the inventory
* @return The serialized inventory contents
*/
public static CompletableFuture<String> serializeItemStackArray(@NotNull ItemStack[] inventoryContents)
throws DataSerializationException {
return CompletableFuture.supplyAsync(() -> {
// Return an empty string if there is no inventory item data to serialize
if (inventoryContents.length == 0) {
return "";
protected final HuskSync plugin;
@SuppressWarnings("unused")
public BukkitSerializer(@NotNull HuskSyncAPI api) {
this.plugin = api.getPlugin();
}
@ApiStatus.Internal
@NotNull
public HuskSync getPlugin() {
return plugin;
}
public static class Inventory extends BukkitSerializer implements Serializer<BukkitData.Items.Inventory>,
ItemDeserializer {
public Inventory(@NotNull HuskSync plugin) {
super(plugin);
}
@Override
public BukkitData.Items.Inventory deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
throws DeserializationException {
final ReadWriteNBT root = NBT.parseNBT(serialized);
final ReadWriteNBT items = root.hasTag(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null;
return BukkitData.Items.Inventory.from(
items != null ? getItems(items, dataMcVersion) : new ItemStack[INVENTORY_SLOT_COUNT],
root.hasTag(HELD_ITEM_SLOT_TAG) ? root.getInteger(HELD_ITEM_SLOT_TAG) : 0
);
}
@Override
public BukkitData.Items.Inventory deserialize(@NotNull String serialized) {
return deserialize(serialized, plugin.getMinecraftVersion());
}
@NotNull
@Override
public String serialize(@NotNull BukkitData.Items.Inventory data) throws SerializationException {
final ReadWriteNBT root = NBT.createNBTObject();
root.setItemStackArray(ITEMS_TAG, data.getContents());
root.setInteger(HELD_ITEM_SLOT_TAG, data.getHeldItemSlot());
return root.toString();
}
}
public static class EnderChest extends BukkitSerializer implements Serializer<BukkitData.Items.EnderChest>,
ItemDeserializer {
public EnderChest(@NotNull HuskSync plugin) {
super(plugin);
}
@Override
public BukkitData.Items.EnderChest deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
throws DeserializationException {
final ItemStack[] items = getItems(NBT.parseNBT(serialized), dataMcVersion);
return items == null ? BukkitData.Items.EnderChest.empty() : BukkitData.Items.EnderChest.adapt(items);
}
@Override
public BukkitData.Items.EnderChest deserialize(@NotNull String serialized) {
return deserialize(serialized, plugin.getMinecraftVersion());
}
@NotNull
@Override
public String serialize(@NotNull BukkitData.Items.EnderChest data) throws SerializationException {
return NBT.itemStackArrayToNBT(data.getContents()).toString();
}
}
// Utility interface for deserializing and upgrading item stacks from legacy versions
private interface ItemDeserializer {
@Nullable
default ItemStack[] getItems(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion) {
if (mcVersion.compareTo(getPlugin().getMinecraftVersion()) < 0) {
return upgradeItemStacks((NBTCompound) tag, mcVersion);
}
return NBT.itemStackArrayFromNBT(tag);
}
// Create an output stream that will be encoded into base 64
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
// Define the length of the inventory array to serialize
bukkitOutputStream.writeInt(inventoryContents.length);
// Write each serialize each ItemStack to the output stream
for (ItemStack inventoryItem : inventoryContents) {
bukkitOutputStream.writeObject(serializeItemStack(inventoryItem));
@NotNull
private ItemStack @NotNull [] upgradeItemStacks(@NotNull NBTCompound itemsNbt, @NotNull Version mcVersion) {
final ReadWriteNBTCompoundList items = itemsNbt.getCompoundList("items");
final ItemStack[] itemStacks = new ItemStack[itemsNbt.getInteger("size")];
for (int i = 0; i < items.size(); i++) {
if (items.get(i) == null) {
itemStacks[i] = new ItemStack(Material.AIR);
continue;
}
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
} catch (IOException e) {
throw new DataSerializationException("Failed to serialize item stack data", e);
}
});
}
/**
* Returns a {@link BukkitInventoryMap} from a serialized array of ItemStacks representing the contents of a player's inventory.
*
* @param serializedPlayerInventory The serialized {@link ItemStack} inventory array
* @return The deserialized ItemStacks, mapped for convenience as a {@link BukkitInventoryMap}
* @throws DataSerializationException If the serialized item stack array could not be deserialized
*/
public static CompletableFuture<BukkitInventoryMap> deserializeInventory(@NotNull String serializedPlayerInventory)
throws DataSerializationException {
return CompletableFuture.supplyAsync(() -> new BukkitInventoryMap(deserializeItemStackArray(serializedPlayerInventory).join()));
}
/**
* Returns an array of ItemStacks from serialized inventory data.
*
* @param serializeItemStackArray The serialized {@link ItemStack} array
* @return The deserialized array of {@link ItemStack}s
* @throws DataSerializationException If the serialized item stack array could not be deserialized
* @implNote Empty slots will be represented by {@code null}
*/
public static CompletableFuture<ItemStack[]> deserializeItemStackArray(@NotNull String serializeItemStackArray)
throws DataSerializationException {
return CompletableFuture.supplyAsync(() -> {
// Return empty array if there is no inventory data (set the player as having an empty inventory)
if (serializeItemStackArray.isEmpty()) {
return new ItemStack[0];
}
// Create a byte input stream to read the serialized data
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(serializeItemStackArray))) {
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
// Read the length of the Bukkit input stream and set the length of the array to this value
ItemStack[] inventoryContents = new ItemStack[bukkitInputStream.readInt()];
// Set the ItemStacks in the array from deserialized ItemStack data
int slotIndex = 0;
for (ItemStack ignored : inventoryContents) {
inventoryContents[slotIndex] = deserializeItemStack(bukkitInputStream.readObject());
slotIndex++;
}
// Return the finished, serialized inventory contents
return inventoryContents;
try {
itemStacks[i] = NBT.itemStackFromNBT(upgradeItemData(items.get(i), mcVersion));
} catch (Throwable e) {
itemStacks[i] = new ItemStack(Material.AIR);
}
} catch (IOException | ClassNotFoundException e) {
throw new DataSerializationException("Failed to deserialize item stack data", e);
}
});
return itemStacks;
}
@NotNull
private ReadWriteNBT upgradeItemData(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion)
throws NoSuchFieldException, IllegalAccessException {
return DataFixerUtil.fixUpItemData(
tag,
getPlugin().getDataVersion(mcVersion),
DataFixerUtil.getCurrentVersion()
);
}
@NotNull
HuskSync getPlugin();
}
public static class PotionEffects extends BukkitSerializer implements Serializer<BukkitData.PotionEffects> {
private static final TypeToken<List<Data.PotionEffects.Effect>> TYPE = new TypeToken<>() {
};
public PotionEffects(@NotNull HuskSync plugin) {
super(plugin);
}
@Override
public BukkitData.PotionEffects deserialize(@NotNull String serialized) throws DeserializationException {
return BukkitData.PotionEffects.adapt(
plugin.getGson().fromJson(serialized, TYPE.getType())
);
}
@NotNull
@Override
public String serialize(@NotNull BukkitData.PotionEffects element) throws SerializationException {
return plugin.getGson().toJson(element.getActiveEffects());
}
}
public static class Advancements extends BukkitSerializer implements Serializer<BukkitData.Advancements> {
private static final TypeToken<List<Data.Advancements.Advancement>> TYPE = new TypeToken<>() {
};
public Advancements(@NotNull HuskSync plugin) {
super(plugin);
}
@Override
public BukkitData.Advancements deserialize(@NotNull String serialized) throws DeserializationException {
return BukkitData.Advancements.from(
plugin.getGson().fromJson(serialized, TYPE.getType())
);
}
@NotNull
@Override
public String serialize(@NotNull BukkitData.Advancements element) throws SerializationException {
return plugin.getGson().toJson(element.getCompleted());
}
}
public static class PersistentData extends BukkitSerializer implements Serializer<BukkitData.PersistentData> {
public PersistentData(@NotNull HuskSync plugin) {
super(plugin);
}
@Override
public BukkitData.PersistentData deserialize(@NotNull String serialized) throws DeserializationException {
return BukkitData.PersistentData.from((NBTContainer) NBT.parseNBT(serialized));
}
@NotNull
@Override
public String serialize(@NotNull BukkitData.PersistentData element) throws SerializationException {
return element.getPersistentData().toString();
}
}
/**
* Returns the serialized version of an {@link ItemStack} as a string to object Map
*
* @param item The {@link ItemStack} to serialize
* @return The serialized {@link ItemStack}
* @deprecated Use {@link Serializer.Json} in the common module instead
*/
@Nullable
private static Map<String, Object> serializeItemStack(@Nullable ItemStack item) {
return item != null ? item.serialize() : null;
@Deprecated(since = "2.6")
public class Json<T extends Data & Adaptable> extends Serializer.Json<T> {
public Json(@NotNull HuskSync plugin, @NotNull Class<T> type) {
super(plugin, type);
}
@NotNull
public BukkitHuskSync getPlugin() {
return (BukkitHuskSync) plugin;
}
}
/**
* Returns the deserialized {@link ItemStack} from the Object read from the {@link BukkitObjectInputStream}
*
* @param serializedItemStack The serialized item stack; a String-Object map
* @return The deserialized {@link ItemStack}
*/
@SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
@Nullable
private static ItemStack deserializeItemStack(@Nullable Object serializedItemStack) {
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
}
/**
* Returns a serialized array of {@link PotionEffect}s
*
* @param potionEffects The potion effect array
* @return The serialized potion effects
*/
public static CompletableFuture<String> serializePotionEffectArray(@NotNull PotionEffect[] potionEffects) throws DataSerializationException {
return CompletableFuture.supplyAsync(() -> {
// Return an empty string if there are no effects to serialize
if (potionEffects.length == 0) {
return "";
}
// Create an output stream that will be encoded into base 64
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
// Define the length of the potion effect array to serialize
bukkitOutputStream.writeInt(potionEffects.length);
// Write each serialize each PotionEffect to the output stream
for (PotionEffect potionEffect : potionEffects) {
bukkitOutputStream.writeObject(serializePotionEffect(potionEffect));
}
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
} catch (IOException e) {
throw new DataSerializationException("Failed to serialize potion effect data", e);
}
});
}
/**
* Returns an array of ItemStacks from serialized potion effect data
*
* @param potionEffectData The serialized {@link PotionEffect} array
* @return The {@link PotionEffect}s
*/
public static CompletableFuture<PotionEffect[]> deserializePotionEffectArray(@NotNull String potionEffectData) throws DataSerializationException {
return CompletableFuture.supplyAsync(() -> {
// Return empty array if there is no potion effect data (don't apply any effects to the player)
if (potionEffectData.isEmpty()) {
return new PotionEffect[0];
}
// Create a byte input stream to read the serialized data
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(potionEffectData))) {
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
// Read the length of the Bukkit input stream and set the length of the array to this value
PotionEffect[] potionEffects = new PotionEffect[bukkitInputStream.readInt()];
// Set the potion effects in the array from deserialized PotionEffect data
int potionIndex = 0;
for (PotionEffect ignored : potionEffects) {
potionEffects[potionIndex] = deserializePotionEffect(bukkitInputStream.readObject());
potionIndex++;
}
// Return the finished, serialized potion effect array
return potionEffects;
}
} catch (IOException | ClassNotFoundException e) {
throw new DataSerializationException("Failed to deserialize potion effects", e);
}
});
}
/**
* Returns the serialized version of an {@link ItemStack} as a string to object Map
*
* @param potionEffect The {@link ItemStack} to serialize
* @return The serialized {@link ItemStack}
*/
@Nullable
private static Map<String, Object> serializePotionEffect(@Nullable PotionEffect potionEffect) {
return potionEffect != null ? potionEffect.serialize() : null;
}
/**
* Returns the deserialized {@link PotionEffect} from the Object read from the {@link BukkitObjectInputStream}
*
* @param serializedPotionEffect The serialized potion effect; a String-Object map
* @return The deserialized {@link PotionEffect}
*/
@SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
@Nullable
private static PotionEffect deserializePotionEffect(@Nullable Object serializedPotionEffect) {
return serializedPotionEffect != null ? new PotionEffect((Map<String, Object>) serializedPotionEffect) : null;
}
}

View File

@@ -0,0 +1,171 @@
/*
* 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.data;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.maps.BukkitMapHandler;
import org.bukkit.entity.Player;
import org.bukkit.inventory.PlayerInventory;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public interface BukkitUserDataHolder extends UserDataHolder {
@Override
default Optional<? extends Data> getData(@NotNull Identifier id) {
if (!id.isCustom()) {
return switch (id.getKeyValue()) {
case "inventory" -> getInventory();
case "ender_chest" -> getEnderChest();
case "potion_effects" -> getPotionEffects();
case "advancements" -> getAdvancements();
case "location" -> getLocation();
case "statistics" -> getStatistics();
case "health" -> getHealth();
case "hunger" -> getHunger();
case "attributes" -> getAttributes();
case "experience" -> getExperience();
case "game_mode" -> getGameMode();
case "flight_status" -> getFlightStatus();
case "persistent_data" -> getPersistentData();
default -> throw new IllegalStateException(String.format("Unexpected data type: %s", id));
};
}
return Optional.ofNullable(getCustomDataStore().get(id));
}
@Override
default void setData(@NotNull Identifier id, @NotNull Data data) {
if (id.isCustom()) {
getCustomDataStore().put(id, data);
}
UserDataHolder.super.setData(id, data);
}
@NotNull
@Override
default Optional<Data.Items.Inventory> getInventory() {
if ((isDead() && !getPlugin().getSettings().getSynchronization().getSaveOnDeath()
.isSyncDeadPlayersChangingServer())) {
return Optional.of(BukkitData.Items.Inventory.empty());
}
final PlayerInventory inventory = getPlayer().getInventory();
return Optional.of(BukkitData.Items.Inventory.from(
getMapPersister().persistLockedMaps(inventory.getContents(), getPlayer()),
inventory.getHeldItemSlot()
));
}
@NotNull
@Override
default Optional<Data.Items.EnderChest> getEnderChest() {
return Optional.of(BukkitData.Items.EnderChest.adapt(
getMapPersister().persistLockedMaps(getPlayer().getEnderChest().getContents(), getPlayer())
));
}
@NotNull
@Override
default Optional<Data.PotionEffects> getPotionEffects() {
return Optional.of(BukkitData.PotionEffects.from(getPlayer().getActivePotionEffects()));
}
@NotNull
@Override
default Optional<Data.Advancements> getAdvancements() {
return Optional.of(BukkitData.Advancements.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.Location> getLocation() {
return Optional.of(BukkitData.Location.adapt(getPlayer().getLocation()));
}
@NotNull
@Override
default Optional<Data.Statistics> getStatistics() {
return Optional.of(BukkitData.Statistics.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.Health> getHealth() {
return Optional.of(BukkitData.Health.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.Hunger> getHunger() {
return Optional.of(BukkitData.Hunger.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.Attributes> getAttributes() {
return Optional.of(BukkitData.Attributes.adapt(getPlayer(), getPlugin()));
}
@NotNull
@Override
default Optional<Data.Experience> getExperience() {
return Optional.of(BukkitData.Experience.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.GameMode> getGameMode() {
return Optional.of(BukkitData.GameMode.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.FlightStatus> getFlightStatus() {
return Optional.of(BukkitData.FlightStatus.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.PersistentData> getPersistentData() {
return Optional.of(BukkitData.PersistentData.adapt(getPlayer().getPersistentDataContainer()));
}
boolean isDead();
@NotNull
Player getPlayer();
/**
* @deprecated Use {@link #getPlayer()} instead
*/
@Deprecated(since = "3.6")
@NotNull
default Player getBukkitPlayer() {
return getPlayer();
}
@NotNull
default BukkitMapHandler getMapPersister() {
return (BukkitHuskSync) getPlugin();
}
}

View File

@@ -1,24 +1,43 @@
/*
* 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.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.UserData;
import net.william278.husksync.player.User;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
@SuppressWarnings("unused")
public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
private boolean cancelled = false;
private UserData userData;
private final HuskSync plugin;
private final DataSnapshot.Packed snapshot;
private final User user;
private final DataSaveCause saveCause;
private boolean cancelled = false;
protected BukkitDataSaveEvent(@NotNull User user, @NotNull UserData userData,
@NotNull DataSaveCause saveCause) {
protected BukkitDataSaveEvent(@NotNull User user, @NotNull DataSnapshot.Packed snapshot, @NotNull HuskSync plugin) {
this.user = user;
this.userData = userData;
this.saveCause = saveCause;
this.snapshot = snapshot;
this.plugin = plugin;
}
@Override
@@ -38,18 +57,15 @@ public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, C
}
@Override
public @NotNull UserData getUserData() {
return userData;
@NotNull
public DataSnapshot.Packed getData() {
return snapshot;
}
@NotNull
@Override
public void setUserData(@NotNull UserData userData) {
this.userData = userData;
}
@Override
public @NotNull DataSaveCause getSaveCause() {
return saveCause;
public HuskSync getPlugin() {
return plugin;
}
@NotNull
@@ -57,4 +73,8 @@ public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, C
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View File

@@ -1,29 +1,44 @@
/*
* 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.william278.husksync.BukkitHuskSync;
import org.bukkit.Bukkit;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
@SuppressWarnings("unused")
public abstract class BukkitEvent extends Event implements net.william278.husksync.event.Event {
private static final HandlerList HANDLER_LIST = new HandlerList();
protected BukkitEvent() {
}
@NotNull
@Override
public CompletableFuture<net.william278.husksync.event.Event> fire() {
final CompletableFuture<net.william278.husksync.event.Event> eventFireFuture = new CompletableFuture<>();
// Don't fire events while the server is shutting down
if (!BukkitHuskSync.getInstance().isEnabled()) {
eventFireFuture.complete(this);
} else {
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
Bukkit.getServer().getPluginManager().callEvent(this);
eventFireFuture.complete(this);
});
}
return eventFireFuture;
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View File

@@ -1,33 +0,0 @@
package net.william278.husksync.event;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.UserData;
import net.william278.husksync.player.BukkitPlayer;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
public class BukkitEventCannon extends EventCannon {
public BukkitEventCannon() {
}
@Override
public CompletableFuture<Event> firePreSyncEvent(@NotNull OnlineUser user, @NotNull UserData userData) {
return new BukkitPreSyncEvent(((BukkitPlayer) user).getPlayer(), userData).fire();
}
@Override
public CompletableFuture<Event> fireDataSaveEvent(@NotNull User user, @NotNull UserData userData,
@NotNull DataSaveCause saveCause) {
return new BukkitDataSaveEvent(user, userData, saveCause).fire();
}
@Override
public void fireSyncCompleteEvent(@NotNull OnlineUser user) {
new BukkitSyncCompleteEvent(((BukkitPlayer) user).getPlayer()).fire();
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
public interface BukkitEventDispatcher extends EventDispatcher {
@Override
default <T extends Event> boolean fireIsCancelled(@NotNull T event) {
Bukkit.getPluginManager().callEvent((org.bukkit.event.Event) event);
return event instanceof Cancellable cancellable && cancellable.isCancelled();
}
@NotNull
@Override
default PreSyncEvent getPreSyncEvent(@NotNull OnlineUser user, @NotNull DataSnapshot.Packed data) {
return new BukkitPreSyncEvent(user, data, getPlugin());
}
@NotNull
@Override
default DataSaveEvent getDataSaveEvent(@NotNull User user, @NotNull DataSnapshot.Packed data) {
return new BukkitDataSaveEvent(user, data, getPlugin());
}
@NotNull
@Override
default SyncCompleteEvent getSyncCompleteEvent(@NotNull OnlineUser user) {
return new BukkitSyncCompleteEvent(user, getPlugin());
}
}

View File

@@ -1,35 +1,54 @@
/*
* 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.william278.husksync.BukkitHuskSync;
import net.william278.husksync.player.BukkitPlayer;
import net.william278.husksync.player.OnlineUser;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import net.william278.husksync.user.OnlineUser;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
@SuppressWarnings("unused")
public abstract class BukkitPlayerEvent extends BukkitEvent implements PlayerEvent {
protected final Player player;
private static final HandlerList HANDLER_LIST = new HandlerList();
protected BukkitPlayerEvent(@NotNull Player player) {
protected final OnlineUser player;
protected BukkitPlayerEvent(@NotNull OnlineUser player) {
this.player = player;
}
@Override
@NotNull
public OnlineUser getUser() {
return BukkitPlayer.adapt(player);
return player;
}
@NotNull
@Override
public CompletableFuture<Event> fire() {
final CompletableFuture<Event> eventFireFuture = new CompletableFuture<>();
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
Bukkit.getServer().getPluginManager().callEvent(this);
eventFireFuture.complete(this);
});
return eventFireFuture;
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View File

@@ -1,19 +1,42 @@
/*
* 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.william278.husksync.data.UserData;
import org.bukkit.entity.Player;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.OnlineUser;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
@SuppressWarnings("unused")
public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEvent, Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
private final HuskSync plugin;
private final DataSnapshot.Packed data;
private boolean cancelled = false;
private UserData userData;
protected BukkitPreSyncEvent(@NotNull Player player, @NotNull UserData userData) {
protected BukkitPreSyncEvent(@NotNull OnlineUser player, @NotNull DataSnapshot.Packed data, @NotNull HuskSync plugin) {
super(player);
this.userData = userData;
this.data = data;
this.plugin = plugin;
}
@Override
@@ -27,13 +50,15 @@ public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEven
}
@Override
public @NotNull UserData getUserData() {
return userData;
@NotNull
public DataSnapshot.Packed getData() {
return data;
}
@NotNull
@Override
public void setUserData(@NotNull UserData userData) {
this.userData = userData;
public HuskSync getPlugin() {
return plugin;
}
@NotNull
@@ -41,4 +66,8 @@ public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEven
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View File

@@ -1,13 +1,34 @@
/*
* 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 org.bukkit.entity.Player;
import net.william278.husksync.HuskSync;
import net.william278.husksync.user.OnlineUser;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
@SuppressWarnings("unused")
public class BukkitSyncCompleteEvent extends BukkitPlayerEvent implements SyncCompleteEvent {
private static final HandlerList HANDLER_LIST = new HandlerList();
protected BukkitSyncCompleteEvent(@NotNull Player player) {
protected BukkitSyncCompleteEvent(@NotNull OnlineUser player, @NotNull HuskSync plugin) {
super(player);
}
@@ -16,4 +37,8 @@ public class BukkitSyncCompleteEvent extends BukkitPlayerEvent implements SyncCo
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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.listener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.jetbrains.annotations.NotNull;
public interface BukkitDeathEventListener extends Listener {
boolean handleEvent(@NotNull EventListener.ListenerType type, @NotNull EventListener.Priority priority);
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
default void onPlayerDeathHighest(@NotNull PlayerDeathEvent event) {
if (handleEvent(EventListener.ListenerType.DEATH_LISTENER, EventListener.Priority.HIGHEST)) {
handlePlayerDeath(event);
}
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
default void onPlayerDeath(@NotNull PlayerDeathEvent event) {
if (handleEvent(EventListener.ListenerType.DEATH_LISTENER, EventListener.Priority.NORMAL)) {
handlePlayerDeath(event);
}
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
default void onPlayerDeathLowest(@NotNull PlayerDeathEvent event) {
if (handleEvent(EventListener.ListenerType.DEATH_LISTENER, EventListener.Priority.LOWEST)) {
handlePlayerDeath(event);
}
}
void handlePlayerDeath(@NotNull PlayerDeathEvent player);
}

View File

@@ -1,131 +1,159 @@
/*
* 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.listener;
import lombok.Getter;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.data.BukkitSerializer;
import net.william278.husksync.data.DataSerializationException;
import net.william278.husksync.data.ItemData;
import net.william278.husksync.editor.ItemEditorMenuType;
import net.william278.husksync.player.BukkitPlayer;
import net.william278.husksync.player.OnlineUser;
import org.bukkit.Bukkit;
import net.william278.husksync.data.BukkitData;
import net.william278.husksync.user.BukkitUser;
import net.william278.husksync.user.OnlineUser;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityPickupItemEvent;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.server.MapInitializeEvent;
import org.bukkit.event.world.WorldSaveEvent;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.stream.Collectors;
public class BukkitEventListener extends EventListener implements Listener {
@Getter
public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
BukkitDeathEventListener, Listener {
public BukkitEventListener(@NotNull BukkitHuskSync huskSync) {
super(huskSync);
Bukkit.getServer().getPluginManager().registerEvents(this, huskSync);
protected LockedHandler lockedHandler;
public BukkitEventListener(@NotNull BukkitHuskSync plugin) {
super(plugin);
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerJoin(@NotNull PlayerJoinEvent event) {
super.handlePlayerJoin(BukkitPlayer.adapt(event.getPlayer()));
public void onLoad() {
this.lockedHandler = createLockedHandler((BukkitHuskSync) plugin);
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerQuit(@NotNull PlayerQuitEvent event) {
super.handlePlayerQuit(BukkitPlayer.adapt(event.getPlayer()));
public void onEnable() {
getPlugin().getServer().getPluginManager().registerEvents(this, getPlugin());
lockedHandler.onEnable();
}
public void handlePluginDisable() {
super.handlePluginDisable();
lockedHandler.onDisable();
}
@NotNull
private LockedHandler createLockedHandler(@NotNull BukkitHuskSync plugin) {
if (!getPlugin().getSettings().isCancelPackets()) {
return new BukkitLockedEventListener(plugin);
}
if (getPlugin().isDependencyLoaded("PacketEvents")) {
return new BukkitPacketEventsLockedPacketListener(plugin);
} else if (getPlugin().isDependencyLoaded("ProtocolLib")) {
return new BukkitProtocolLibLockedPacketListener(plugin);
}
return new BukkitLockedEventListener(plugin);
}
@Override
public boolean handleEvent(@NotNull ListenerType type, @NotNull Priority priority) {
return plugin.getSettings().getSynchronization().getEventPriority(type).equals(priority);
}
@Override
public void handlePlayerQuit(@NotNull BukkitUser bukkitUser) {
final Player player = bukkitUser.getPlayer();
final ItemStack itemOnCursor = player.getItemOnCursor();
if (!bukkitUser.isLocked() && !itemOnCursor.getType().isAir()) {
player.setItemOnCursor(null);
player.getWorld().dropItem(player.getLocation(), itemOnCursor);
plugin.debug("Dropped " + itemOnCursor + " for " + player.getName() + " on quit");
}
super.handlePlayerQuit(bukkitUser);
}
@Override
public void handlePlayerJoin(@NotNull BukkitUser bukkitUser) {
super.handlePlayerJoin(bukkitUser);
}
@Override
public void handlePlayerDeath(@NotNull PlayerDeathEvent event) {
final OnlineUser user = BukkitUser.adapt(event.getEntity(), plugin);
// If the player is locked or the plugin disabling, clear their drops
if (lockedHandler.cancelPlayerEvent(user.getUuid())) {
event.getDrops().clear();
return;
}
// Handle saving player data snapshots on death
if (!plugin.getSettings().getSynchronization().getSaveOnDeath().isEnabled()) {
return;
}
// Truncate the dropped items list to the inventory size and save the player's inventory
final int maxInventorySize = BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
if (event.getDrops().size() > maxInventorySize) {
event.getDrops().subList(maxInventorySize, event.getDrops().size()).clear();
}
super.saveOnPlayerDeath(user, BukkitData.Items.ItemArray.adapt(event.getDrops()));
}
@EventHandler(ignoreCancelled = true)
public void onWorldSave(@NotNull WorldSaveEvent event) {
CompletableFuture.runAsync(() -> super.handleAsyncWorldSave(event.getWorld().getPlayers().stream()
.map(BukkitPlayer::adapt).collect(Collectors.toList())));
if (!plugin.getSettings().getSynchronization().isSaveOnWorldSave()) {
return;
}
// Handle saving player data snapshots when the world saves
plugin.runAsync(() -> super.saveOnWorldSave(event.getWorld().getPlayers()
.stream().map(player -> BukkitUser.adapt(player, plugin))
.collect(Collectors.toList())));
}
@EventHandler(ignoreCancelled = true)
public void onInventoryClose(@NotNull InventoryCloseEvent event) {
CompletableFuture.runAsync(() -> {
if (event.getPlayer() instanceof Player player) {
final OnlineUser user = BukkitPlayer.adapt(player);
plugin.getDataEditor().getEditingInventoryData(user).ifPresent(menu -> {
try {
BukkitSerializer.serializeItemStackArray(Arrays.copyOf(event.getInventory().getContents(),
menu.itemEditorMenuType == ItemEditorMenuType.INVENTORY_VIEWER
? player.getInventory().getSize()
: player.getEnderChest().getSize())).thenAccept(
serializedInventory -> super.handleMenuClose(user, new ItemData(serializedInventory)));
} catch (DataSerializationException e) {
plugin.getLoggingAdapter().log(Level.SEVERE,
"Failed to serialize inventory data during menu close", e);
}
});
}
});
}
/*
* Events to cancel if the player has not been set yet
*/
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onDropItem(@NotNull PlayerDropItemEvent event) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer())));
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPickupItem(@NotNull EntityPickupItemEvent event) {
if (event.getEntity() instanceof Player player) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(player)));
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerInteract(@NotNull PlayerInteractEvent event) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer())));
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onBlockPlace(@NotNull BlockPlaceEvent event) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer())));
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onBlockBreak(@NotNull BlockBreakEvent event) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer())));
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onInventoryClick(@NotNull InventoryClickEvent event) {
if (event.getWhoClicked() instanceof Player player) {
event.setCancelled(cancelInventoryClick(BukkitPlayer.adapt(player)));
public void onMapInitialize(@NotNull MapInitializeEvent event) {
if (plugin.getSettings().getSynchronization().isPersistLockedMaps() && event.getMap().isLocked()) {
getPlugin().runAsync(() -> ((BukkitHuskSync) plugin).renderPersistedMap(event.getMap()));
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
if (event.getPlayer() instanceof Player player) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(player)));
// We handle commands here to allow specific command handling on ProtocolLib servers
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
public void onCommandProcessed(@NotNull PlayerCommandPreprocessEvent event) {
if (!lockedHandler.isCommandDisabled(event.getMessage().substring(1).split(" ")[0])) {
return;
}
if (lockedHandler.cancelPlayerEvent(event.getPlayer().getUniqueId())) {
event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true)
public void onPlayerDeath(PlayerDeathEvent event) {
if (cancelPlayerEvent(BukkitPlayer.adapt(event.getEntity()))) {
event.getDrops().clear();
}
@NotNull
@Override
public BukkitHuskSync getPlugin() {
return (BukkitHuskSync) plugin;
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.listener;
import net.william278.husksync.HuskSync;
import net.william278.husksync.user.BukkitUser;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.jetbrains.annotations.NotNull;
public interface BukkitJoinEventListener extends Listener {
boolean handleEvent(@NotNull EventListener.ListenerType type, @NotNull EventListener.Priority priority);
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
default void onPlayerJoinHighest(@NotNull PlayerJoinEvent event) {
if (handleEvent(EventListener.ListenerType.JOIN_LISTENER, EventListener.Priority.HIGHEST)) {
handlePlayerJoin(BukkitUser.adapt(event.getPlayer(), getPlugin()));
}
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
default void onPlayerJoin(@NotNull PlayerJoinEvent event) {
if (handleEvent(EventListener.ListenerType.JOIN_LISTENER, EventListener.Priority.NORMAL)) {
handlePlayerJoin(BukkitUser.adapt(event.getPlayer(), getPlugin()));
}
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
default void onPlayerJoinLowest(@NotNull PlayerJoinEvent event) {
if (handleEvent(EventListener.ListenerType.JOIN_LISTENER, EventListener.Priority.LOWEST)) {
handlePlayerJoin(BukkitUser.adapt(event.getPlayer(), getPlugin()));
}
}
void handlePlayerJoin(@NotNull BukkitUser player);
@NotNull
HuskSync getPlugin();
}

View File

@@ -0,0 +1,131 @@
/*
* 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.listener;
import lombok.Getter;
import net.william278.husksync.BukkitHuskSync;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.event.Cancellable;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityPickupItemEvent;
import org.bukkit.event.entity.ProjectileLaunchEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
import org.bukkit.event.player.PlayerArmorStandManipulateEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.UUID;
@Getter
public class BukkitLockedEventListener implements LockedHandler, Listener {
protected final BukkitHuskSync plugin;
protected BukkitLockedEventListener(@NotNull BukkitHuskSync plugin) {
this.plugin = plugin;
}
@Override
public void onEnable() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onProjectileLaunch(@NotNull ProjectileLaunchEvent event) {
final Projectile projectile = event.getEntity();
if (projectile.getShooter() instanceof Player player) {
cancelPlayerEvent(player.getUniqueId(), event);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onDropItem(@NotNull PlayerDropItemEvent event) {
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPickupItem(@NotNull EntityPickupItemEvent event) {
if (event.getEntity() instanceof Player player) {
cancelPlayerEvent(player.getUniqueId(), event);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerInteract(@NotNull PlayerInteractEvent event) {
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerInteractEntity(@NotNull PlayerInteractEntityEvent event) {
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerInteractArmorStand(@NotNull PlayerArmorStandManipulateEvent event) {
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onBlockPlace(@NotNull BlockPlaceEvent event) {
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onBlockBreak(@NotNull BlockBreakEvent event) {
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
if (event.getPlayer() instanceof Player player) {
cancelPlayerEvent(player.getUniqueId(), event);
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onInventoryClick(@NotNull InventoryClickEvent event) {
cancelPlayerEvent(event.getWhoClicked().getUniqueId(), event);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
if (event.getEntity() instanceof Player player) {
cancelPlayerEvent(player.getUniqueId(), event);
}
}
private void cancelPlayerEvent(@NotNull UUID uuid, @NotNull Cancellable event) {
if (cancelPlayerEvent(uuid)) {
event.setCancelled(true);
plugin.debug("Cancelled event " + event.getClass().getSimpleName() + " from " + Objects.requireNonNull(plugin.getServer().getPlayer(uuid)).getName());
}
}
}

View File

@@ -0,0 +1,117 @@
/*
* 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.listener;
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;
import net.william278.husksync.BukkitHuskSync;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
import java.util.logging.Level;
public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
protected BukkitPacketEventsLockedPacketListener(@NotNull BukkitHuskSync plugin) {
super(plugin);
}
@Override
@SuppressWarnings("UnstableApiUsage")
public void onLoad() {
super.onLoad();
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(getPlugin()));
PacketEvents.getAPI().getSettings().reEncodeByDefault(false).checkForUpdates(false);
PacketEvents.getAPI().load();
}
@Override
public void onEnable() {
super.onEnable();
PacketEvents.getAPI().getEventManager().registerListener(new PlayerPacketAdapter(this));
PacketEvents.getAPI().init();
plugin.log(Level.INFO, "Using PacketEvents to cancel packets for locked players");
}
private static class PlayerPacketAdapter extends PacketListenerAbstract {
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
PacketType.Play.Client.CLIENT_SETTINGS // Video setting packets
);
private static final Set<PacketType.Play.Client> CANCEL_PACKETS = getPacketsToListenFor();
private final BukkitPacketEventsLockedPacketListener listener;
public PlayerPacketAdapter(@NotNull BukkitPacketEventsLockedPacketListener listener) {
super(PacketListenerPriority.HIGH);
this.listener = listener;
}
@Override
public void onPacketReceive(PacketReceiveEvent event) {
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)) {
return;
}
if (listener.cancelPlayerEvent(event.getUser().getUUID())) {
event.setCancelled(true);
}
}
// Returns the set of ALL Server packets, excluding the set of allowed packets
@NotNull
private static Set<PacketType.Play.Client> getPacketsToListenFor() {
return Sets.difference(
Sets.newHashSet(PacketType.Play.Client.values()),
ALLOWED_PACKETS
);
}
}
}

View File

@@ -0,0 +1,93 @@
/*
* 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.listener;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.events.ListenerPriority;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketEvent;
import com.google.common.collect.Sets;
import net.william278.husksync.BukkitHuskSync;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static com.comphenix.protocol.PacketType.Play.Client;
public class BukkitProtocolLibLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
protected BukkitProtocolLibLockedPacketListener(@NotNull BukkitHuskSync plugin) {
super(plugin);
}
@Override
public void onEnable() {
super.onEnable();
ProtocolLibrary.getProtocolManager().addPacketListener(new PlayerPacketAdapter(this));
plugin.log(Level.INFO, "Using ProtocolLib to cancel packets for locked players");
}
private static class PlayerPacketAdapter extends PacketAdapter {
// 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
Client.POSITION, Client.POSITION_LOOK, Client.LOOK, // Movement packets
Client.HELD_ITEM_SLOT, Client.ARM_ANIMATION, Client.TELEPORT_ACCEPT, // Animation packets
Client.SETTINGS // Video setting packets
);
private final BukkitProtocolLibLockedPacketListener listener;
public PlayerPacketAdapter(@NotNull BukkitProtocolLibLockedPacketListener listener) {
super(listener.getPlugin(), ListenerPriority.HIGHEST, getPacketsToListenFor());
this.listener = listener;
}
@Override
public void onPacketReceiving(@NotNull PacketEvent event) {
if (listener.cancelPlayerEvent(event.getPlayer().getUniqueId()) && !event.isReadOnly()) {
event.setCancelled(true);
}
}
@Override
public void onPacketSending(@NotNull PacketEvent event) {
if (listener.cancelPlayerEvent(event.getPlayer().getUniqueId()) && !event.isReadOnly()) {
event.setCancelled(true);
}
}
// Returns the set of ALL Server packets, excluding the set of allowed packets
@NotNull
private static Set<PacketType> getPacketsToListenFor() {
return Sets.difference(
Client.getInstance().values().stream().filter(PacketType::isSupported).collect(Collectors.toSet()),
ALLOWED_PACKETS
);
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.listener;
import net.william278.husksync.HuskSync;
import net.william278.husksync.user.BukkitUser;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import org.jetbrains.annotations.NotNull;
public interface BukkitQuitEventListener extends Listener {
boolean handleEvent(@NotNull EventListener.ListenerType type, @NotNull EventListener.Priority priority);
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
default void onPlayerQuitHighest(@NotNull PlayerQuitEvent event) {
if (handleEvent(EventListener.ListenerType.QUIT_LISTENER, EventListener.Priority.HIGHEST)) {
handlePlayerQuit(BukkitUser.adapt(event.getPlayer(), getPlugin()));
}
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
default void onPlayerQuit(@NotNull PlayerQuitEvent event) {
if (handleEvent(EventListener.ListenerType.QUIT_LISTENER, EventListener.Priority.NORMAL)) {
handlePlayerQuit(BukkitUser.adapt(event.getPlayer(), getPlugin()));
}
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
default void onPlayerQuitLowest(@NotNull PlayerQuitEvent event) {
if (handleEvent(EventListener.ListenerType.QUIT_LISTENER, EventListener.Priority.LOWEST)) {
handlePlayerQuit(BukkitUser.adapt(event.getPlayer(), getPlugin()));
}
}
void handlePlayerQuit(@NotNull BukkitUser player);
@NotNull
HuskSync getPlugin();
}

View File

@@ -0,0 +1,105 @@
/*
* 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.listener;
import com.google.common.collect.Lists;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.data.BukkitData;
import net.william278.husksync.user.BukkitUser;
import net.william278.husksync.user.OnlineUser;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerAdvancementDoneEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.jetbrains.annotations.NotNull;
import java.util.Iterator;
import java.util.List;
import static net.william278.husksync.config.Settings.SynchronizationSettings.SaveOnDeathSettings;
public class PaperEventListener extends BukkitEventListener {
public PaperEventListener(@NotNull BukkitHuskSync plugin) {
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
final OnlineUser user = BukkitUser.adapt(event.getEntity(), plugin);
if (lockedHandler.cancelPlayerEvent(user.getUuid())) {
event.getDrops().clear();
event.getItemsToKeep().clear();
return;
}
// Handle saving player data snapshots on death
final SaveOnDeathSettings settings = plugin.getSettings().getSynchronization().getSaveOnDeath();
if (!settings.isEnabled()) {
return;
}
// Paper - support saving the player's items to keep if enabled
final int maxInventorySize = BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
final List<ItemStack> itemsToSave = switch (settings.getItemsToSave()) {
case DROPS -> event.getDrops();
case ITEMS_TO_KEEP -> preserveOrder(event.getEntity().getInventory(), event.getItemsToKeep());
};
if (itemsToSave.size() > maxInventorySize) {
itemsToSave.subList(maxInventorySize, itemsToSave.size()).clear();
}
super.saveOnPlayerDeath(user, BukkitData.Items.ItemArray.adapt(itemsToSave));
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerAdvancementDone(@NotNull PlayerAdvancementDoneEvent event) {
if (lockedHandler.cancelPlayerEvent(event.getPlayer().getUniqueId())) {
event.message(null);
}
}
@NotNull
private List<ItemStack> preserveOrder(@NotNull PlayerInventory inventory, @NotNull List<ItemStack> toKeep) {
final List<ItemStack> preserved = Lists.newArrayList();
final List<ItemStack> items = Lists.newArrayList(inventory.getContents());
for (ItemStack item : toKeep) {
final Iterator<ItemStack> iterator = items.iterator();
while (iterator.hasNext()) {
final ItemStack originalItem = iterator.next();
if (originalItem != null && originalItem.equals(item)) {
preserved.add(originalItem);
iterator.remove();
break;
}
}
}
return preserved;
}
}

View File

@@ -0,0 +1,552 @@
/*
* 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.maps;
import com.google.common.collect.Lists;
import de.tr7zw.changeme.nbtapi.NBT;
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
import de.tr7zw.changeme.nbtapi.iface.ReadableNBT;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.redis.RedisManager;
import net.william278.mapdataapi.MapBanner;
import net.william278.mapdataapi.MapData;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Container;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BlockStateMeta;
import org.bukkit.inventory.meta.BundleMeta;
import org.bukkit.inventory.meta.MapMeta;
import org.bukkit.map.*;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.io.IOException;
import java.util.List;
import java.util.*;
import java.util.function.Function;
import java.util.logging.Level;
public interface BukkitMapHandler {
// The map used to store HuskSync data in ItemStack NBT
String MAP_DATA_KEY = "husksync:persisted_locked_map";
// Name of server the map originates from
String MAP_ORIGIN_KEY = "origin";
// Original map id
String MAP_ID_KEY = "id";
/**
* Persist locked maps in an array of {@link ItemStack}s
*
* @param items the array of {@link ItemStack}s to persist locked maps in
* @param delegateRenderer the player to delegate the rendering of map pixel canvases to
* @return the array of {@link ItemStack}s with locked maps persisted to serialized NBT
*/
@NotNull
default ItemStack[] persistLockedMaps(@NotNull ItemStack[] items, @NotNull Player delegateRenderer) {
if (!getPlugin().getSettings().getSynchronization().isPersistLockedMaps()) {
return items;
}
return forEachMap(items, map -> this.persistMapView(map, delegateRenderer));
}
/**
* Apply persisted locked maps to an array of {@link ItemStack}s
*
* @param items the array of {@link ItemStack}s to apply persisted locked maps to
* @return the array of {@link ItemStack}s with persisted locked maps applied
*/
@Nullable
default ItemStack @NotNull [] setMapViews(@Nullable ItemStack @NotNull [] items) {
if (!getPlugin().getSettings().getSynchronization().isPersistLockedMaps()) {
return items;
}
return forEachMap(items, this::applyMapView);
}
// Perform an operation on each map in an array of ItemStacks
@NotNull
private ItemStack[] forEachMap(ItemStack[] items, @NotNull Function<ItemStack, ItemStack> function) {
for (int i = 0; i < items.length; i++) {
final ItemStack item = items[i];
if (item == null) {
continue;
}
if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) {
items[i] = function.apply(item);
} else if (item.getItemMeta() instanceof BlockStateMeta b && b.getBlockState() instanceof Container box) {
forEachMap(box.getInventory().getContents(), function);
b.setBlockState(box);
item.setItemMeta(b);
} else if (item.getItemMeta() instanceof BundleMeta bundle) {
bundle.setItems(List.of(forEachMap(bundle.getItems().toArray(ItemStack[]::new), function)));
item.setItemMeta(bundle);
}
}
return items;
}
@Blocking
private void writeMapData(@NotNull String serverName, int mapId, MapData data) {
final byte[] dataBytes = getPlugin().getDataAdapter().toBytes(new AdaptableMapData(data));
getRedisManager().setMapData(serverName, mapId, dataBytes);
getPlugin().getDatabase().saveMapData(serverName, mapId, dataBytes);
}
@Nullable
@Blocking
private Map.Entry<MapData, Boolean> readMapData(@NotNull String serverName, int mapId) {
final Map.Entry<byte[], Boolean> readData = fetchMapData(serverName, mapId);
if (readData == null) {
return null;
}
return deserializeMapData(readData);
}
@Nullable
@Blocking
private Map.Entry<byte[], Boolean> fetchMapData(@NotNull String serverName, int mapId) {
return fetchMapData(serverName, mapId, true);
}
@Nullable
@Blocking
private Map.Entry<byte[], Boolean> fetchMapData(@NotNull String serverName, int mapId, boolean doReverseLookup) {
// Read from Redis cache
final byte[] redisData = getRedisManager().getMapData(serverName, mapId);
if (redisData != null) {
return new AbstractMap.SimpleImmutableEntry<>(redisData, true);
}
// Read from database and set to Redis
@Nullable Map.Entry<byte[], Boolean> databaseData = getPlugin().getDatabase().getMapData(serverName, mapId);
if (databaseData != null) {
getRedisManager().setMapData(serverName, mapId, databaseData.getKey());
return databaseData;
}
// Otherwise, lookup a reverse map binding
if (doReverseLookup) {
return fetchReversedMapData(serverName, mapId);
}
return null;
}
@Nullable
private Map.Entry<byte[], Boolean> fetchReversedMapData(@NotNull String serverName, int mapId) {
// Lookup binding from Redis cache, then fetch data if found
Map.Entry<String, Integer> binding = getRedisManager().getReversedMapBound(serverName, mapId);
if (binding != null) {
return fetchMapData(binding.getKey(), binding.getValue(), false);
}
// Lookup binding from database, then set to Redis & fetch data if found
binding = getPlugin().getDatabase().getMapBinding(serverName, mapId);
if (binding != null) {
getRedisManager().bindMapIds(binding.getKey(), binding.getValue(), serverName, mapId);
return fetchMapData(binding.getKey(), binding.getValue(), false);
}
return null;
}
@Nullable
private Map.Entry<MapData, Boolean> deserializeMapData(@NotNull Map.Entry<byte[], Boolean> data) {
try {
return new AbstractMap.SimpleImmutableEntry<>(
getPlugin().getDataAdapter().fromBytes(data.getKey(), AdaptableMapData.class)
.getData(getPlugin().getDataVersion(getPlugin().getMinecraftVersion())),
data.getValue()
);
} catch (IOException e) {
getPlugin().log(Level.WARNING, "Failed to deserialize map data", e);
return null;
}
}
// Get the bound map ID
private int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName) {
// Get the map ID from Redis, if set
final Optional<Integer> redisId = getRedisManager().getBoundMapId(fromServerName, fromMapId, toServerName);
if (redisId.isPresent()) {
return redisId.get();
}
// Get from the database; if found, set to Redis
final int result = getPlugin().getDatabase().getBoundMapId(fromServerName, fromMapId, toServerName);
if (result != -1) {
getPlugin().getRedisManager().bindMapIds(fromServerName, fromMapId, toServerName, result);
}
return result;
}
@NotNull
private ItemStack persistMapView(@NotNull ItemStack map, @NotNull Player delegateRenderer) {
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
if (!meta.hasMapView()) {
return map;
}
final MapView view = meta.getMapView();
if (view == null || view.getWorld() == null || !view.isLocked() || view.isVirtual()) {
return map;
}
NBT.modify(map, nbt -> {
// Don't save the map's data twice
if (nbt.hasTag(MAP_DATA_KEY)) {
return;
}
// Render the map
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()));
}
// Persist map data
final ReadWriteNBT mapData = nbt.getOrCreateCompound(MAP_DATA_KEY);
final String serverName = getPlugin().getServerName();
mapData.setString(MAP_ORIGIN_KEY, serverName);
mapData.setInteger(MAP_ID_KEY, meta.getMapId());
if (readMapData(serverName, meta.getMapId()) == null) {
writeMapData(serverName, meta.getMapId(), canvas.extractMapData());
}
getPlugin().debug(String.format("Saved data for locked map (#%s, server: %s)", view.getId(), serverName));
});
return map;
}
@SuppressWarnings("deprecation")
@NotNull
private ItemStack applyMapView(@NotNull ItemStack map) {
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
NBT.get(map, nbt -> {
if (!nbt.hasTag(MAP_DATA_KEY)) {
return;
}
final ReadableNBT mapData = nbt.getCompound(MAP_DATA_KEY);
if (mapData == null) {
return;
}
// Determine map ID
final String originServerName = mapData.getString(MAP_ORIGIN_KEY);
final String currentServerName = getPlugin().getServerName();
final int originalMapId = mapData.getInteger(MAP_ID_KEY);
int newId = currentServerName.equals(originServerName)
? originalMapId : getBoundMapId(originServerName, originalMapId, currentServerName);
if (newId != -1) {
meta.setMapId(newId);
map.setItemMeta(meta);
getPlugin().debug(String.format("Map ID set to %s", newId));
return;
}
// Read the pixel data and generate a map view otherwise
getPlugin().debug("Deserializing map data from NBT and generating view...");
final @Nullable Map.Entry<MapData, Boolean> readMapData = readMapData(originServerName, originalMapId);
if (readMapData == null) {
getPlugin().debug("Read pixel data was not found in database, skipping...");
return;
}
// Add a renderer to the map with the data and save to file
final MapData canvasData = Objects.requireNonNull(readMapData, "Pixel data null!").getKey();
final MapView view = generateRenderedMap(canvasData);
meta.setMapView(view);
map.setItemMeta(meta);
// Bind in the database & Redis
final int id = view.getId();
getRedisManager().bindMapIds(originServerName, originalMapId, currentServerName, id);
getPlugin().getDatabase().setMapBinding(originServerName, originalMapId, currentServerName, id);
getPlugin().debug(String.format("Bound map to view (#%s) on server %s", id, currentServerName));
});
return map;
}
default void renderPersistedMap(@NotNull MapView view) {
if (getMapView(view.getId()).isPresent()) {
return;
}
@Nullable final Map.Entry<MapData, Boolean> data = readMapData(getPlugin().getServerName(), view.getId());
if (data == null) {
final World world = view.getWorld() == null ? getDefaultMapWorld() : view.getWorld();
getPlugin().debug("Not rendering map: no data in DB for world %s, map #%s."
.formatted(world.getName(), view.getId()));
return;
}
if (data.getValue()) {
// from this server, doesn't need tweaking
return;
}
final MapData canvasData = data.getKey();
// Create a new map view renderer with the map data color at each pixel
// use view.removeRenderer() to remove all this maps renderers
view.getRenderers().forEach(view::removeRenderer);
view.addRenderer(new PersistentMapRenderer(canvasData));
view.setLocked(true);
view.setScale(MapView.Scale.NORMAL);
view.setTrackingPosition(false);
view.setUnlimitedTracking(false);
// Set the view to the map
setMapView(view);
}
// Sets the renderer of a map, and returns the generated MapView
@NotNull
private MapView generateRenderedMap(@NotNull MapData canvasData) {
final MapView view = Bukkit.createMap(getDefaultMapWorld());
view.getRenderers().clear();
// Create a new map view renderer with the map data color at each pixel
view.addRenderer(new PersistentMapRenderer(canvasData));
view.setLocked(true);
view.setScale(MapView.Scale.NORMAL);
view.setTrackingPosition(false);
view.setUnlimitedTracking(false);
// Set the view to the map and return it
setMapView(view);
return view;
}
@NotNull
private static World getDefaultMapWorld() {
final World world = Bukkit.getWorlds().get(0);
if (world == null) {
throw new IllegalStateException("No worlds are loaded on the server!");
}
return world;
}
default Optional<MapView> getMapView(int id) {
return getMapViews().containsKey(id) ? Optional.of(getMapViews().get(id)) : Optional.empty();
}
default void setMapView(@NotNull MapView view) {
getMapViews().put(view.getId(), view);
}
/**
* 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;
private PersistentMapRenderer(@NotNull MapData canvasData) {
super(false);
this.canvasData = canvasData;
}
@Override
public void render(@NotNull MapView map, @NotNull MapCanvas canvas, @NotNull Player player) {
// We set the pixels in this order to avoid the map being rendered upside down
for (int i = 0; i < 128; i++) {
for (int j = 0; j < 128; j++) {
canvas.setPixel(j, i, (byte) canvasData.getColorAt(i, j));
}
}
// Set the map banners and markers
final MapCursorCollection cursors = canvas.getCursors();
while (cursors.size() > 0) {
cursors.removeCursor(cursors.getCursor(0));
}
canvasData.getBanners().forEach(banner -> cursors.addCursor(createBannerCursor(banner)));
canvas.setCursors(cursors);
}
}
@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()
);
}
/**
* A {@link MapCanvas} implementation used for pre-rendering maps to be converted into {@link MapData}
*/
@SuppressWarnings({"deprecation", "removal"})
class PersistentMapCanvas implements MapCanvas {
private static final String BANNER_PREFIX = "banner_";
private final int mapDataVersion;
private final MapView mapView;
private final int[][] pixels = new int[128][128];
private MapCursorCollection cursors;
private PersistentMapCanvas(@NotNull MapView mapView, int mapDataVersion) {
this.mapDataVersion = mapDataVersion;
this.mapView = mapView;
}
@NotNull
@Override
public MapView getMapView() {
return mapView;
}
@NotNull
@Override
public MapCursorCollection getCursors() {
return cursors == null ? (cursors = new MapCursorCollection()) : cursors;
}
@Override
public void setCursors(@NotNull MapCursorCollection cursors) {
this.cursors = cursors;
}
@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 (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
public void drawImage(int x, int y, @NotNull Image image) {
// Not implemented
}
@Override
public void drawText(int x, int y, @NotNull MapFont font, @NotNull String text) {
// Not implemented
}
@NotNull
private String getDimension() {
return mapView.getWorld() != null ? switch (mapView.getWorld().getEnvironment()) {
case NETHER -> "minecraft:the_nether";
case THE_END -> "minecraft:the_end";
default -> "minecraft:overworld";
} : "minecraft:overworld";
}
/**
* Extract the map data from the canvas. Must be rendered first
*
* @return the extracted map data
*/
@NotNull
private MapData extractMapData() {
final List<MapBanner> banners = Lists.newArrayList();
for (int i = 0; i < getCursors().size(); i++) {
final MapCursor cursor = getCursors().getCursor(i);
//#if MC==12001
//$$ final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH);
//#else
final String type = cursor.getType().getKey().getKey();
//#endif
if (type.startsWith(BANNER_PREFIX)) {
banners.add(new MapBanner(
type.replaceAll(BANNER_PREFIX, ""),
cursor.getCaption() == null ? "" : cursor.getCaption(),
cursor.getX(),
mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128,
cursor.getY()
));
}
}
return MapData.fromPixels(mapDataVersion, pixels, getDimension(), (byte) 2, banners, List.of());
}
}
@NotNull
Map<Integer, MapView> getMapViews();
@ApiStatus.Internal
RedisManager getRedisManager();
@ApiStatus.Internal
@NotNull
BukkitHuskSync getPlugin();
}

View File

@@ -1,26 +1,51 @@
/*
* 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.migrator;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.zaxxer.hikari.HikariDataSource;
import me.william278.husksync.bukkit.data.DataSerializer;
import net.william278.hslmigrator.HSLConverter;
import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.*;
import net.william278.husksync.player.User;
import net.william278.husksync.data.BukkitData;
import net.william278.husksync.data.Data;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import net.william278.husksync.util.BukkitLegacyConverter;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.entity.EntityType;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.regex.Pattern;
import static net.william278.husksync.config.Settings.DatabaseSettings;
public class LegacyMigrator extends Migrator {
private final HSLConverter hslConverter;
@@ -32,50 +57,50 @@ public class LegacyMigrator extends Migrator {
private String sourcePlayersTable;
private String sourceDataTable;
private final String minecraftVersion;
public LegacyMigrator(@NotNull HuskSync plugin) {
super(plugin);
this.hslConverter = HSLConverter.getInstance();
this.sourceHost = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_HOST);
this.sourcePort = plugin.getSettings().getIntegerValue(Settings.ConfigOption.DATABASE_PORT);
this.sourceUsername = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_USERNAME);
this.sourcePassword = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_PASSWORD);
this.sourceDatabase = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_NAME);
final DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
this.sourceHost = credentials.getHost();
this.sourcePort = credentials.getPort();
this.sourceUsername = credentials.getUsername();
this.sourcePassword = credentials.getPassword();
this.sourceDatabase = credentials.getDatabase();
this.sourcePlayersTable = "husksync_players";
this.sourceDataTable = "husksync_data";
this.minecraftVersion = plugin.getMinecraftVersion().toString();
}
@Override
public CompletableFuture<Boolean> start() {
plugin.getLoggingAdapter().log(Level.INFO, "Starting migration of legacy HuskSync v1.x data...");
plugin.log(Level.INFO, "Starting migration of legacy HuskSync v1.x data...");
final long startTime = System.currentTimeMillis();
return CompletableFuture.supplyAsync(() -> {
return plugin.supplyAsync(() -> {
// Wipe the existing database, preparing it for data import
plugin.getLoggingAdapter().log(Level.INFO, "Preparing existing database (wiping)...");
plugin.getDatabase().wipeDatabase().join();
plugin.getLoggingAdapter().log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
plugin.log(Level.INFO, "Preparing existing database (wiping)...");
plugin.getDatabase().wipeDatabase();
plugin.log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
// Create jdbc driver connection url
final String jdbcUrl = "jdbc:mysql://" + sourceHost + ":" + sourcePort + "/" + sourceDatabase;
// Create a new data source for the mpdb converter
try (final HikariDataSource connectionPool = new HikariDataSource()) {
plugin.getLoggingAdapter().log(Level.INFO, "Establishing connection to legacy database...");
plugin.log(Level.INFO, "Establishing connection to legacy database...");
connectionPool.setJdbcUrl(jdbcUrl);
connectionPool.setUsername(sourceUsername);
connectionPool.setPassword(sourcePassword);
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase());
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase(Locale.ENGLISH));
plugin.getLoggingAdapter().log(Level.INFO, "Downloading raw data from the legacy database...");
final List<LegacyData> dataToMigrate = new ArrayList<>();
plugin.log(Level.INFO, "Downloading raw data from the legacy database (this might take a while)...");
final List<LegacyData> dataToMigrate = Lists.newArrayList();
try (final Connection connection = connectionPool.getConnection()) {
try (final PreparedStatement statement = connection.prepareStatement("""
SELECT `uuid`, `username`, `inventory`, `ender_chest`, `health`, `max_health`, `health_scale`, `hunger`, `saturation`, `saturation_exhaustion`, `selected_slot`, `status_effects`, `total_experience`, `exp_level`, `exp_progress`, `game_mode`, `statistics`, `is_flying`, `advancements`, `location`
FROM `%source_players_table%`
INNER JOIN `%source_data_table%`
ON `%source_players_table%`.`id` = `%source_data_table%`.`player_id`;
ON `%source_players_table%`.`id` = `%source_data_table%`.`player_id`
WHERE `username` IS NOT NULL;
""".replaceAll(Pattern.quote("%source_players_table%"), sourcePlayersTable)
.replaceAll(Pattern.quote("%source_data_table%"), sourceDataTable))) {
try (final ResultSet resultSet = statement.executeQuery()) {
@@ -104,26 +129,36 @@ public class LegacyMigrator extends Migrator {
resultSet.getString("location")
));
playersMigrated++;
if (playersMigrated % 25 == 0) {
plugin.getLoggingAdapter().log(Level.INFO, "Downloaded legacy data for " + playersMigrated + " players...");
if (playersMigrated % 50 == 0) {
plugin.log(Level.INFO, "Downloaded legacy data for " + playersMigrated + " players...");
}
}
}
}
}
plugin.getLoggingAdapter().log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the legacy database!");
plugin.getLoggingAdapter().log(Level.INFO, "Converting HuskSync 1.x data to the latest HuskSync user data format...");
dataToMigrate.forEach(data -> data.toUserData(hslConverter, minecraftVersion).thenAccept(convertedData ->
plugin.getDatabase().ensureUser(data.user()).thenRun(() ->
plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.LEGACY_MIGRATION)
.exceptionally(exception -> {
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().username + ": " + exception.getMessage());
return null;
}))));
plugin.getLoggingAdapter().log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
plugin.log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the legacy database!");
plugin.log(Level.INFO, "Converting HuskSync 1.x data to the new user data format (this might take a while)...");
final AtomicInteger playersConverted = new AtomicInteger();
dataToMigrate.forEach(data -> {
final DataSnapshot.Packed convertedData = data.toUserData(hslConverter, plugin);
plugin.getDatabase().ensureUser(data.user());
try {
plugin.getDatabase().addSnapshot(data.user(), convertedData);
} catch (Throwable e) {
plugin.log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().getName() + ": " + e.getMessage());
return;
}
playersConverted.getAndIncrement();
if (playersConverted.get() % 50 == 0) {
plugin.log(Level.INFO, "Converted legacy data for " + playersConverted + " players...");
}
});
plugin.log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
return true;
} catch (Exception e) {
plugin.getLoggingAdapter().log(Level.SEVERE, "Error while migrating legacy data: " + e.getMessage() + " - are your source database credentials correct?");
} catch (Throwable e) {
plugin.log(Level.SEVERE, "Error while migrating legacy data: " + e.getMessage() + " - are your source database credentials correct?", e);
return false;
}
});
@@ -132,7 +167,7 @@ public class LegacyMigrator extends Migrator {
@Override
public void handleConfigurationCommand(@NotNull String[] args) {
if (args.length == 2) {
if (switch (args[0].toLowerCase()) {
if (switch (args[0].toLowerCase(Locale.ENGLISH)) {
case "host" -> {
this.sourceHost = args[1];
yield true;
@@ -167,15 +202,15 @@ public class LegacyMigrator extends Migrator {
}
default -> false;
}) {
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu());
plugin.getLoggingAdapter().log(Level.INFO, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1]));
plugin.log(Level.INFO, getHelpMenu());
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1]));
} else {
plugin.getLoggingAdapter().log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
obfuscateDataString(args[1]) + " (is it a valid option?)");
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
obfuscateDataString(args[1]) + " (is it a valid option?)");
}
} else {
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu());
plugin.log(Level.INFO, getHelpMenu());
}
}
@@ -188,22 +223,22 @@ public class LegacyMigrator extends Migrator {
@NotNull
@Override
public String getName() {
return "HuskSync v1.x --> v2.x Migrator";
return "HuskSync v1.x --> v3.x Migrator";
}
@NotNull
@Override
public String getHelpMenu() {
return """
=== HuskSync v1.x --> v2.x Migration Wizard =========
=== HuskSync v1.x --> v3.x Migration Wizard =========
This will migrate all user data from HuskSync v1.x to
HuskSync v2.x's new format. To perform the migration,
HuskSync v3.x's new format. To perform the migration,
please follow the steps below carefully.
[!] Existing data in the database will be wiped. [!]
STEP 1] Please ensure no players are on any servers.
STEP 2] HuskSync will need to connect to the database
used to hold the existing, legacy HuskSync data.
If this is the same database as the one you are
@@ -223,12 +258,12 @@ public class LegacyMigrator extends Migrator {
using the command:
"husksync migrate legacy set <parameter> <value>"
(e.g.: "husksync migrate legacy set host 1.2.3.4")
STEP 3] HuskSync will migrate data into the database
tables configures in the config.yml file of this
server. Please make sure you're happy with this
before proceeding.
STEP 4] To start the migration, please run:
"husksync migrate legacy start"
""".replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost))
@@ -249,72 +284,90 @@ public class LegacyMigrator extends Migrator {
@NotNull String serializedAdvancements, @NotNull String serializedLocation) {
@NotNull
public CompletableFuture<UserData> toUserData(@NotNull HSLConverter converter,
@NotNull String minecraftVersion) {
return CompletableFuture.supplyAsync(() -> {
try {
final DataSerializer.StatisticData legacyStatisticData = converter
.deserializeStatisticData(serializedStatistics);
final StatisticsData convertedStatisticData = new StatisticsData(
convertStatisticMap(legacyStatisticData.untypedStatisticValues()),
convertMaterialStatisticMap(legacyStatisticData.blockStatisticValues()),
convertMaterialStatisticMap(legacyStatisticData.itemStatisticValues()),
convertEntityStatisticMap(legacyStatisticData.entityStatisticValues()));
public DataSnapshot.Packed toUserData(@NotNull HSLConverter converter, @NotNull HuskSync plugin) {
try {
final DataSerializer.StatisticData stats = converter.deserializeStatisticData(serializedStatistics);
final DataSerializer.PlayerLocation loc = converter.deserializePlayerLocationData(serializedLocation);
final BukkitLegacyConverter adapter = (BukkitLegacyConverter) plugin.getLegacyConverter()
.orElseThrow(() -> new IllegalStateException("Legacy converter not present"));
final List<AdvancementData> convertedAdvancements = converter
.deserializeAdvancementData(serializedAdvancements)
.stream().map(data -> new AdvancementData(data.key(), data.criteriaMap())).toList();
return DataSnapshot.builder(plugin)
// Inventory
.inventory(BukkitData.Items.Inventory.from(
adapter.deserializeLegacyItemStacks(serializedInventory),
selectedSlot
))
final DataSerializer.PlayerLocation legacyLocationData = converter
.deserializePlayerLocationData(serializedLocation);
final LocationData convertedLocationData = new LocationData(
legacyLocationData == null ? "world" : legacyLocationData.worldName(),
UUID.randomUUID(),
"NORMAL",
legacyLocationData == null ? 0d : legacyLocationData.x(),
legacyLocationData == null ? 64d : legacyLocationData.y(),
legacyLocationData == null ? 0d : legacyLocationData.z(),
legacyLocationData == null ? 90f : legacyLocationData.yaw(),
legacyLocationData == null ? 180f : legacyLocationData.pitch());
// Ender chest
.enderChest(BukkitData.Items.EnderChest.adapt(
adapter.deserializeLegacyItemStacks(serializedEnderChest)
))
return new UserData(new StatusData(health, maxHealth, healthScale, hunger, saturation,
saturationExhaustion, selectedSlot, totalExp, expLevel, expProgress, gameMode, isFlying),
new ItemData(serializedInventory), new ItemData(serializedEnderChest),
new PotionEffectData(serializedPotionEffects), convertedAdvancements,
convertedStatisticData, convertedLocationData,
new PersistentDataContainerData(new HashMap<>()),
minecraftVersion);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
// Location
.location(BukkitData.Location.from(
loc == null ? 0d : loc.x(),
loc == null ? 64d : loc.y(),
loc == null ? 0d : loc.z(),
loc == null ? 90f : loc.yaw(),
loc == null ? 180f : loc.pitch(),
new Data.Location.World(
loc == null ? "world" : loc.worldName(),
UUID.randomUUID(), "NORMAL"
)))
// Advancements
.advancements(BukkitData.Advancements.from(converter
.deserializeAdvancementData(serializedAdvancements).stream()
.map(data -> Data.Advancements.Advancement.adapt(data.key(), data.criteriaMap()))
.toList()))
// Stats
.statistics(BukkitData.Statistics.from(
convertStatisticMap(stats.untypedStatisticValues()),
convertMaterialStatisticMap(stats.blockStatisticValues()),
convertMaterialStatisticMap(stats.itemStatisticValues()),
convertEntityStatisticMap(stats.entityStatisticValues())
))
// Health, hunger, experience & game mode
.health(BukkitData.Health.from(health, healthScale, false))
.hunger(BukkitData.Hunger.from(hunger, saturation, saturationExhaustion))
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
.gameMode(BukkitData.GameMode.from(gameMode))
.flightStatus(BukkitData.FlightStatus.from(isFlying, isFlying))
// Build & pack into new format
.saveCause(DataSnapshot.SaveCause.LEGACY_MIGRATION).buildAndPack();
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
private Map<String, Integer> convertStatisticMap(@NotNull HashMap<Statistic, Integer> rawMap) {
final HashMap<String, Integer> convertedMap = new HashMap<>();
final HashMap<String, Integer> convertedMap = Maps.newHashMap();
for (Map.Entry<Statistic, Integer> entry : rawMap.entrySet()) {
convertedMap.put(entry.getKey().toString(), entry.getValue());
convertedMap.put(entry.getKey().getKey().toString(), entry.getValue());
}
return convertedMap;
}
private Map<String, Map<String, Integer>> convertMaterialStatisticMap(@NotNull HashMap<Statistic, HashMap<Material, Integer>> rawMap) {
final Map<String, Map<String, Integer>> convertedMap = new HashMap<>();
final Map<String, Map<String, Integer>> convertedMap = Maps.newHashMap();
for (Map.Entry<Statistic, HashMap<Material, Integer>> entry : rawMap.entrySet()) {
for (Map.Entry<Material, Integer> materialEntry : entry.getValue().entrySet()) {
convertedMap.computeIfAbsent(entry.getKey().toString(), k -> new HashMap<>())
.put(materialEntry.getKey().toString(), materialEntry.getValue());
convertedMap.computeIfAbsent(entry.getKey().getKey().toString(), k -> new HashMap<>())
.put(materialEntry.getKey().getKey().toString(), materialEntry.getValue());
}
}
return convertedMap;
}
private Map<String, Map<String, Integer>> convertEntityStatisticMap(@NotNull HashMap<Statistic, HashMap<EntityType, Integer>> rawMap) {
final Map<String, Map<String, Integer>> convertedMap = new HashMap<>();
final Map<String, Map<String, Integer>> convertedMap = Maps.newHashMap();
for (Map.Entry<Statistic, HashMap<EntityType, Integer>> entry : rawMap.entrySet()) {
for (Map.Entry<EntityType, Integer> materialEntry : entry.getValue().entrySet()) {
convertedMap.computeIfAbsent(entry.getKey().toString(), k -> new HashMap<>())
.put(materialEntry.getKey().toString(), materialEntry.getValue());
convertedMap.computeIfAbsent(entry.getKey().getKey().toString(), k -> new HashMap<>())
.put(materialEntry.getKey().getKey().toString(), materialEntry.getValue());
}
}
return convertedMap;

View File

@@ -1,28 +1,54 @@
/*
* 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.migrator;
import com.google.common.collect.Lists;
import com.zaxxer.hikari.HikariDataSource;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.*;
import net.william278.husksync.player.User;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.BukkitData;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import net.william278.mpdbconverter.MPDBConverter;
import org.bukkit.Bukkit;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.*;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.regex.Pattern;
import static net.william278.husksync.config.Settings.DatabaseSettings;
/**
* A migrator for migrating MySQLPlayerDataBridge data to HuskSync {@link UserData}
* A migrator for migrating MySQLPlayerDataBridge data to HuskSync {@link DataSnapshot}s
*/
public class MpdbMigrator extends Migrator {
@@ -35,46 +61,48 @@ public class MpdbMigrator extends Migrator {
private String sourceInventoryTable;
private String sourceEnderChestTable;
private String sourceExperienceTable;
private final String minecraftVersion;
public MpdbMigrator(@NotNull BukkitHuskSync plugin, @NotNull Plugin mySqlPlayerDataBridge) {
public MpdbMigrator(@NotNull BukkitHuskSync plugin) {
super(plugin);
this.mpdbConverter = MPDBConverter.getInstance(mySqlPlayerDataBridge);
this.sourceHost = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_HOST);
this.sourcePort = plugin.getSettings().getIntegerValue(Settings.ConfigOption.DATABASE_PORT);
this.sourceUsername = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_USERNAME);
this.sourcePassword = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_PASSWORD);
this.sourceDatabase = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_NAME);
this.mpdbConverter = MPDBConverter.getInstance(Objects.requireNonNull(
Bukkit.getPluginManager().getPlugin("MySQLPlayerDataBridge"),
"MySQLPlayerDataBridge dependency not found!"
));
final DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
this.sourceHost = credentials.getHost();
this.sourcePort = credentials.getPort();
this.sourceUsername = credentials.getUsername();
this.sourcePassword = credentials.getPassword();
this.sourceDatabase = credentials.getDatabase();
this.sourceInventoryTable = "mpdb_inventory";
this.sourceEnderChestTable = "mpdb_enderchest";
this.sourceExperienceTable = "mpdb_experience";
this.minecraftVersion = plugin.getMinecraftVersion().toString();
}
@Override
public CompletableFuture<Boolean> start() {
plugin.getLoggingAdapter().log(Level.INFO, "Starting migration from MySQLPlayerDataBridge to HuskSync...");
plugin.log(Level.INFO, "Starting migration from MySQLPlayerDataBridge to HuskSync...");
final long startTime = System.currentTimeMillis();
return CompletableFuture.supplyAsync(() -> {
return plugin.supplyAsync(() -> {
// Wipe the existing database, preparing it for data import
plugin.getLoggingAdapter().log(Level.INFO, "Preparing existing database (wiping)...");
plugin.getDatabase().wipeDatabase().join();
plugin.getLoggingAdapter().log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
plugin.log(Level.INFO, "Preparing existing database (wiping)...");
plugin.getDatabase().wipeDatabase();
plugin.log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
// Create jdbc driver connection url
final String jdbcUrl = "jdbc:mysql://" + sourceHost + ":" + sourcePort + "/" + sourceDatabase;
// Create a new data source for the mpdb converter
try (final HikariDataSource connectionPool = new HikariDataSource()) {
plugin.getLoggingAdapter().log(Level.INFO, "Establishing connection to MySQLPlayerDataBridge database...");
plugin.log(Level.INFO, "Establishing connection to MySQLPlayerDataBridge database...");
connectionPool.setJdbcUrl(jdbcUrl);
connectionPool.setUsername(sourceUsername);
connectionPool.setPassword(sourcePassword);
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase());
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase(Locale.ENGLISH));
plugin.getLoggingAdapter().log(Level.INFO, "Downloading raw data from the MySQLPlayerDataBridge database...");
final List<MpdbData> dataToMigrate = new ArrayList<>();
plugin.log(Level.INFO, "Downloading raw data from the MySQLPlayerDataBridge database (this might take a while)...");
final List<MpdbData> dataToMigrate = Lists.newArrayList();
try (final Connection connection = connectionPool.getConnection()) {
try (final PreparedStatement statement = connection.prepareStatement("""
SELECT `%source_inventory_table%`.`player_uuid`, `%source_inventory_table%`.`player_name`, `inventory`, `armor`, `enderchest`, `exp_lvl`, `exp`, `total_exp`
@@ -101,25 +129,29 @@ public class MpdbMigrator extends Migrator {
));
playersMigrated++;
if (playersMigrated % 25 == 0) {
plugin.getLoggingAdapter().log(Level.INFO, "Downloaded MySQLPlayerDataBridge data for " + playersMigrated + " players...");
plugin.log(Level.INFO, "Downloaded MySQLPlayerDataBridge data for " + playersMigrated + " players...");
}
}
}
}
}
plugin.getLoggingAdapter().log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the MySQLPlayerDataBridge database!");
plugin.getLoggingAdapter().log(Level.INFO, "Converting raw MySQLPlayerDataBridge data to HuskSync user data...");
dataToMigrate.forEach(data -> data.toUserData(mpdbConverter, minecraftVersion).thenAccept(convertedData ->
plugin.getDatabase().ensureUser(data.user()).thenRun(() ->
plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.MPDB_MIGRATION))
.exceptionally(exception -> {
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate MySQLPlayerDataBridge data for " + data.user().username + ": " + exception.getMessage());
return null;
})));
plugin.getLoggingAdapter().log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
plugin.log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the MySQLPlayerDataBridge database!");
plugin.log(Level.INFO, "Converting raw MySQLPlayerDataBridge data to HuskSync user data (this might take a while)...");
final AtomicInteger playersConverted = new AtomicInteger();
dataToMigrate.forEach(data -> {
final DataSnapshot.Packed convertedData = data.toUserData(mpdbConverter, plugin);
plugin.getDatabase().ensureUser(data.user());
plugin.getDatabase().addSnapshot(data.user(), convertedData);
playersConverted.getAndIncrement();
if (playersConverted.get() % 50 == 0) {
plugin.log(Level.INFO, "Converted MySQLPlayerDataBridge data for " + playersConverted + " players...");
}
});
plugin.log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
return true;
} catch (Exception e) {
plugin.getLoggingAdapter().log(Level.SEVERE, "Error while migrating data: " + e.getMessage() + " - are your source database credentials correct?");
} catch (Throwable e) {
plugin.log(Level.SEVERE, "Error while migrating data: " + e.getMessage() + " - are your source database credentials correct?");
return false;
}
});
@@ -128,7 +160,7 @@ public class MpdbMigrator extends Migrator {
@Override
public void handleConfigurationCommand(@NotNull String[] args) {
if (args.length == 2) {
if (switch (args[0].toLowerCase()) {
if (switch (args[0].toLowerCase(Locale.ENGLISH)) {
case "host" -> {
this.sourceHost = args[1];
yield true;
@@ -167,15 +199,15 @@ public class MpdbMigrator extends Migrator {
}
default -> false;
}) {
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu());
plugin.getLoggingAdapter().log(Level.INFO, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1]));
plugin.log(Level.INFO, getHelpMenu());
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1]));
} else {
plugin.getLoggingAdapter().log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
obfuscateDataString(args[1]) + " (is it a valid option?)");
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
obfuscateDataString(args[1]) + " (is it a valid option?)");
}
} else {
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu());
plugin.log(Level.INFO, getHelpMenu());
}
}
@@ -196,16 +228,19 @@ public class MpdbMigrator extends Migrator {
public String getHelpMenu() {
return """
=== MySQLPlayerDataBridge Migration Wizard ==========
NOTE: This migrator currently WORKS WITH MPDB version
v4.9.2 and below!
This will migrate inventories, ender chests and XP
from the MySQLPlayerDataBridge plugin to HuskSync.
To prevent excessive migration times, other non-vital
data will not be transferred.
[!] Existing data in the database will be wiped. [!]
STEP 1] Please ensure no players are on any servers.
STEP 2] HuskSync will need to connect to the database
used to hold the source MySQLPlayerDataBridge data.
Please check these database parameters are OK:
@@ -220,15 +255,18 @@ 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
server. Please make sure you're happy with this
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!
""".replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost))
.replaceAll(Pattern.quote("%source_port%"), Integer.toString(sourcePort))
.replaceAll(Pattern.quote("%source_username%"), obfuscateDataString(sourceUsername))
@@ -250,42 +288,43 @@ public class MpdbMigrator extends Migrator {
* @param expProgress The player's current XP progress
* @param totalExp The player's total XP score
*/
private record MpdbData(@NotNull User user, @NotNull String serializedInventory,
@NotNull String serializedArmor, @NotNull String serializedEnderChest,
int expLevel, float expProgress, int totalExp) {
private record MpdbData(
@NotNull User user,
@NotNull String serializedInventory,
@NotNull String serializedArmor,
@NotNull String serializedEnderChest,
int expLevel,
float expProgress,
int totalExp
) {
/**
* Converts exported MySQLPlayerDataBridge data into HuskSync's {@link UserData} object format
* Converts exported MySQLPlayerDataBridge data into HuskSync's {@link DataSnapshot} object format
*
* @param converter The {@link MPDBConverter} to use for converting to {@link ItemStack}s
* @return A {@link CompletableFuture} that will resolve to the converted {@link UserData} object
* @return A {@link CompletableFuture} that will resolve to the converted {@link DataSnapshot} object
*/
@NotNull
public CompletableFuture<UserData> toUserData(@NotNull MPDBConverter converter,
@NotNull String minecraftVersion) {
return CompletableFuture.supplyAsync(() -> {
// Combine inventory and armour
final Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
inventory.setContents(converter.getItemStackFromSerializedData(serializedInventory));
final ItemStack[] armor = converter.getItemStackFromSerializedData(serializedArmor).clone();
for (int i = 36; i < 36 + armor.length; i++) {
inventory.setItem(i, armor[i - 36]);
}
public DataSnapshot.Packed toUserData(@NotNull MPDBConverter converter, @NotNull HuskSync plugin) {
// Combine inventory and armor
final Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
inventory.setContents(converter.getItemStackFromSerializedData(serializedInventory));
final ItemStack[] armor = converter.getItemStackFromSerializedData(serializedArmor).clone();
for (int i = 36; i < 36 + armor.length; i++) {
inventory.setItem(i, armor[i - 36]);
}
final ItemStack[] enderChest = converter.getItemStackFromSerializedData(serializedEnderChest);
// Create user data record
return new UserData(new StatusData(20, 20, 0, 20, 10,
1, 0, totalExp, expLevel, expProgress, "SURVIVAL",
false),
new ItemData(BukkitSerializer.serializeItemStackArray(inventory.getContents()).join()),
new ItemData(BukkitSerializer.serializeItemStackArray(converter
.getItemStackFromSerializedData(serializedEnderChest)).join()),
new PotionEffectData(""), new ArrayList<>(),
new StatisticsData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()),
new LocationData("world", UUID.randomUUID(), "NORMAL", 0, 0, 0,
0f, 0f),
new PersistentDataContainerData(new HashMap<>()),
minecraftVersion);
});
// Create user data record
return DataSnapshot.builder(plugin)
.inventory(BukkitData.Items.Inventory.from(inventory.getContents(), 0))
.enderChest(BukkitData.Items.EnderChest.adapt(enderChest))
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
.gameMode(BukkitData.GameMode.from("SURVIVAL"))
.saveCause(DataSnapshot.SaveCause.MPDB_MIGRATION)
.buildAndPack();
}
}
}

View File

@@ -1,497 +0,0 @@
package net.william278.husksync.player;
import de.themoep.minedown.MineDown;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.BaseComponent;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.data.*;
import net.william278.husksync.editor.ItemEditorMenu;
import net.william278.husksync.util.Version;
import org.apache.commons.lang.ArrayUtils;
import org.bukkit.*;
import org.bukkit.advancement.Advancement;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.attribute.Attribute;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
/**
* Bukkit implementation of an {@link OnlineUser}
*/
public class BukkitPlayer extends OnlineUser {
private final Player player;
private BukkitPlayer(@NotNull Player player) {
super(player.getUniqueId(), player.getName());
this.player = player;
}
public static BukkitPlayer adapt(@NotNull Player player) {
return new BukkitPlayer(player);
}
public Player getPlayer() {
return player;
}
@Override
public CompletableFuture<StatusData> getStatus() {
return CompletableFuture.supplyAsync(() -> {
final double maxHealth = getMaxHealth(player);
return new StatusData(Math.min(player.getHealth(), maxHealth),
maxHealth,
player.isHealthScaled() ? player.getHealthScale() : 0d,
player.getFoodLevel(),
player.getSaturation(),
player.getExhaustion(),
player.getInventory().getHeldItemSlot(),
player.getTotalExperience(),
player.getLevel(),
player.getExp(),
player.getGameMode().name(),
player.getAllowFlight() && player.isFlying());
});
}
@Override
public CompletableFuture<Void> setStatus(@NotNull StatusData statusData,
@NotNull List<StatusDataFlag> statusDataFlags) {
return CompletableFuture.runAsync(() -> {
double currentMaxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH))
.getBaseValue();
if (statusDataFlags.contains(StatusDataFlag.SET_MAX_HEALTH)) {
if (statusData.maxHealth != 0d) {
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH))
.setBaseValue(statusData.maxHealth);
currentMaxHealth = statusData.maxHealth;
}
}
if (statusDataFlags.contains(StatusDataFlag.SET_HEALTH)) {
final double currentHealth = player.getHealth();
if (statusData.health != currentHealth) {
final double healthToSet = currentHealth > currentMaxHealth ? currentMaxHealth : statusData.health;
if (healthToSet <= 0) {
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> player.setHealth(healthToSet));
} else {
player.setHealth(healthToSet);
}
}
if (statusData.healthScale != 0d) {
player.setHealthScale(statusData.healthScale);
} else {
player.setHealthScale(statusData.maxHealth);
}
player.setHealthScaled(statusData.healthScale != 0D);
}
if (statusDataFlags.contains(StatusDataFlag.SET_HUNGER)) {
player.setFoodLevel(statusData.hunger);
player.setSaturation(statusData.saturation);
player.setExhaustion(statusData.saturationExhaustion);
}
if (statusDataFlags.contains(StatusDataFlag.SET_SELECTED_ITEM_SLOT)) {
player.getInventory().setHeldItemSlot(statusData.selectedItemSlot);
}
if (statusDataFlags.contains(StatusDataFlag.SET_EXPERIENCE)) {
player.setTotalExperience(statusData.totalExperience);
player.setLevel(statusData.expLevel);
player.setExp(statusData.expProgress);
}
if (statusDataFlags.contains(StatusDataFlag.SET_GAME_MODE)) {
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () ->
player.setGameMode(GameMode.valueOf(statusData.gameMode)));
}
if (statusDataFlags.contains(StatusDataFlag.SET_FLYING)) {
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
if (statusData.isFlying) {
player.setAllowFlight(true);
player.setFlying(true);
}
player.setFlying(false);
});
}
});
}
@Override
public CompletableFuture<ItemData> getInventory() {
return BukkitSerializer.serializeItemStackArray(player.getInventory().getContents())
.thenApply(ItemData::new);
}
@Override
public CompletableFuture<Void> setInventory(@NotNull ItemData itemData) {
return BukkitSerializer.deserializeInventory(itemData.serializedItems).thenApplyAsync(contents -> {
final CompletableFuture<Void> inventorySetFuture = new CompletableFuture<>();
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
player.getInventory().setContents(contents.getContents());
inventorySetFuture.complete(null);
});
return inventorySetFuture.join();
});
}
@Override
public CompletableFuture<ItemData> getEnderChest() {
return BukkitSerializer.serializeItemStackArray(player.getEnderChest().getContents())
.thenApply(ItemData::new);
}
@Override
public CompletableFuture<Void> setEnderChest(@NotNull ItemData enderChestData) {
return BukkitSerializer.deserializeItemStackArray(enderChestData.serializedItems).thenApplyAsync(contents -> {
final CompletableFuture<Void> enderChestSetFuture = new CompletableFuture<>();
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
player.getEnderChest().setContents(contents);
enderChestSetFuture.complete(null);
});
return enderChestSetFuture.join();
});
}
@Override
public CompletableFuture<PotionEffectData> getPotionEffects() {
return BukkitSerializer.serializePotionEffectArray(player.getActivePotionEffects()
.toArray(new PotionEffect[0])).thenApply(PotionEffectData::new);
}
@Override
public CompletableFuture<Void> setPotionEffects(@NotNull PotionEffectData potionEffectData) {
return BukkitSerializer.deserializePotionEffectArray(potionEffectData.serializedPotionEffects)
.thenApplyAsync(effects -> {
final CompletableFuture<Void> potionEffectsSetFuture = new CompletableFuture<>();
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
for (PotionEffect effect : player.getActivePotionEffects()) {
player.removePotionEffect(effect.getType());
}
for (PotionEffect effect : effects) {
player.addPotionEffect(effect);
}
potionEffectsSetFuture.complete(null);
});
return potionEffectsSetFuture.join();
});
}
@Override
public CompletableFuture<List<AdvancementData>> getAdvancements() {
return CompletableFuture.supplyAsync(() -> {
final Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
final ArrayList<AdvancementData> advancementData = new ArrayList<>();
// Iterate through the server advancement set and add all advancements to the list
serverAdvancements.forEachRemaining(advancement -> {
final AdvancementProgress advancementProgress = player.getAdvancementProgress(advancement);
final Map<String, Date> awardedCriteria = new HashMap<>();
advancementProgress.getAwardedCriteria().forEach(criteriaKey -> awardedCriteria.put(criteriaKey,
advancementProgress.getDateAwarded(criteriaKey)));
// Only save the advancement if criteria has been completed
if (!awardedCriteria.isEmpty()) {
advancementData.add(new AdvancementData(advancement.getKey().toString(), awardedCriteria));
}
});
return advancementData;
});
}
@Override
public CompletableFuture<Void> setAdvancements(@NotNull List<AdvancementData> advancementData) {
return CompletableFuture.runAsync(() -> Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
// Temporarily disable advancement announcing if needed
boolean announceAdvancementUpdate = false;
if (Boolean.TRUE.equals(player.getWorld().getGameRuleValue(GameRule.ANNOUNCE_ADVANCEMENTS))) {
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false);
announceAdvancementUpdate = true;
}
final boolean finalAnnounceAdvancementUpdate = announceAdvancementUpdate;
// Save current experience and level
final int experienceLevel = player.getLevel();
final float expProgress = player.getExp();
// Determines whether the experience might have changed warranting an update
final AtomicBoolean correctExperience = new AtomicBoolean(false);
// Run asynchronously as advancement setting is expensive
CompletableFuture.runAsync(() -> {
// Apply the advancements to the player
final Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
while (serverAdvancements.hasNext()) {
// Iterate through all advancements
final Advancement advancement = serverAdvancements.next();
final AdvancementProgress playerProgress = player.getAdvancementProgress(advancement);
advancementData.stream().filter(record -> record.key.equals(advancement.getKey().toString())).findFirst().ifPresentOrElse(
// Award all criteria that the player does not have that they do on the cache
record -> {
record.completedCriteria.keySet().stream()
.filter(criterion -> !playerProgress.getAwardedCriteria().contains(criterion))
.forEach(criterion -> {
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(),
() -> player.getAdvancementProgress(advancement).awardCriteria(criterion));
correctExperience.set(true);
});
// Revoke all criteria that the player does have but should not
new ArrayList<>(playerProgress.getAwardedCriteria()).stream().filter(criterion -> !record.completedCriteria.containsKey(criterion))
.forEach(criterion -> Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(),
() -> player.getAdvancementProgress(advancement).revokeCriteria(criterion)));
},
// Revoke the criteria as the player shouldn't have any
() -> new ArrayList<>(playerProgress.getAwardedCriteria()).forEach(criterion ->
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(),
() -> player.getAdvancementProgress(advancement).revokeCriteria(criterion))));
// Update the player's experience in case the advancement changed that
if (correctExperience.get()) {
player.setLevel(experienceLevel);
player.setExp(expProgress);
correctExperience.set(false);
}
}
// Re-enable announcing advancements (back on main thread again)
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
if (finalAnnounceAdvancementUpdate) {
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, true);
}
});
});
}));
}
@Override
public CompletableFuture<StatisticsData> getStatistics() {
return CompletableFuture.supplyAsync(() -> {
final Map<String, Integer> untypedStatisticValues = new HashMap<>();
final Map<String, Map<String, Integer>> blockStatisticValues = new HashMap<>();
final Map<String, Map<String, Integer>> itemStatisticValues = new HashMap<>();
final Map<String, Map<String, Integer>> entityStatisticValues = new HashMap<>();
for (Statistic statistic : Statistic.values()) {
switch (statistic.getType()) {
case ITEM -> {
final Map<String, Integer> itemValues = new HashMap<>();
Arrays.stream(Material.values()).filter(Material::isItem)
.filter(itemMaterial -> (player.getStatistic(statistic, itemMaterial)) != 0)
.forEach(itemMaterial -> itemValues.put(itemMaterial.name(),
player.getStatistic(statistic, itemMaterial)));
if (!itemValues.isEmpty()) {
itemStatisticValues.put(statistic.name(), itemValues);
}
}
case BLOCK -> {
final Map<String, Integer> blockValues = new HashMap<>();
Arrays.stream(Material.values()).filter(Material::isBlock)
.filter(blockMaterial -> (player.getStatistic(statistic, blockMaterial)) != 0)
.forEach(blockMaterial -> blockValues.put(blockMaterial.name(),
player.getStatistic(statistic, blockMaterial)));
if (!blockValues.isEmpty()) {
blockStatisticValues.put(statistic.name(), blockValues);
}
}
case ENTITY -> {
final Map<String, Integer> entityValues = new HashMap<>();
Arrays.stream(EntityType.values()).filter(EntityType::isAlive)
.filter(entityType -> (player.getStatistic(statistic, entityType)) != 0)
.forEach(entityType -> entityValues.put(entityType.name(),
player.getStatistic(statistic, entityType)));
if (!entityValues.isEmpty()) {
entityStatisticValues.put(statistic.name(), entityValues);
}
}
case UNTYPED -> {
if (player.getStatistic(statistic) != 0) {
untypedStatisticValues.put(statistic.name(), player.getStatistic(statistic));
}
}
}
}
return new StatisticsData(untypedStatisticValues, blockStatisticValues,
itemStatisticValues, entityStatisticValues);
});
}
@Override
public CompletableFuture<Void> setStatistics(@NotNull StatisticsData statisticsData) {
return CompletableFuture.runAsync(() -> {
// Set untyped statistics
for (String statistic : statisticsData.untypedStatistics.keySet()) {
player.setStatistic(Statistic.valueOf(statistic), statisticsData.untypedStatistics.get(statistic));
}
// Set block statistics
for (String statistic : statisticsData.blockStatistics.keySet()) {
for (String blockMaterial : statisticsData.blockStatistics.get(statistic).keySet()) {
player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(blockMaterial),
statisticsData.blockStatistics.get(statistic).get(blockMaterial));
}
}
// Set item statistics
for (String statistic : statisticsData.itemStatistics.keySet()) {
for (String itemMaterial : statisticsData.itemStatistics.get(statistic).keySet()) {
player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(itemMaterial),
statisticsData.itemStatistics.get(statistic).get(itemMaterial));
}
}
// Set entity statistics
for (String statistic : statisticsData.entityStatistics.keySet()) {
for (String entityType : statisticsData.entityStatistics.get(statistic).keySet()) {
player.setStatistic(Statistic.valueOf(statistic), EntityType.valueOf(entityType),
statisticsData.entityStatistics.get(statistic).get(entityType));
}
}
});
}
@Override
public CompletableFuture<LocationData> getLocation() {
return CompletableFuture.supplyAsync(() ->
new LocationData(player.getWorld().getName(), player.getWorld().getUID(), player.getWorld().getEnvironment().name(),
player.getLocation().getX(), player.getLocation().getY(), player.getLocation().getZ(),
player.getLocation().getYaw(), player.getLocation().getPitch()));
}
@Override
public CompletableFuture<Void> setLocation(@NotNull LocationData locationData) {
final CompletableFuture<Void> teleportFuture = new CompletableFuture<>();
AtomicReference<World> bukkitWorld = new AtomicReference<>(Bukkit.getWorld(locationData.worldName));
if (bukkitWorld.get() == null) {
bukkitWorld.set(Bukkit.getWorld(locationData.worldUuid));
}
if (bukkitWorld.get() == null) {
Bukkit.getWorlds().stream().filter(world -> world.getEnvironment() == World.Environment
.valueOf(locationData.worldEnvironment)).findFirst().ifPresent(bukkitWorld::set);
}
if (bukkitWorld.get() != null) {
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
player.teleport(new Location(bukkitWorld.get(),
locationData.x, locationData.y, locationData.z,
locationData.yaw, locationData.pitch), PlayerTeleportEvent.TeleportCause.PLUGIN);
teleportFuture.complete(null);
});
}
return teleportFuture;
}
@Override
public CompletableFuture<PersistentDataContainerData> getPersistentDataContainer() {
return CompletableFuture.supplyAsync(() -> {
final PersistentDataContainer container = player.getPersistentDataContainer();
if (container.isEmpty()) {
return new PersistentDataContainerData(new HashMap<>());
}
final HashMap<String, Byte[]> persistentDataMap = new HashMap<>();
// Set persistent data keys; ignore keys that we cannot synchronise as byte arrays
for (final NamespacedKey key : container.getKeys()) {
try {
persistentDataMap.put(key.toString(), ArrayUtils.toObject(container.get(key, PersistentDataType.BYTE_ARRAY)));
} catch (IllegalArgumentException | NullPointerException ignored) {
}
}
return new PersistentDataContainerData(persistentDataMap);
}).exceptionally(throwable -> {
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.WARNING, "Could not read " + player.getName() + "'s persistent data map, skipping!");
throwable.printStackTrace();
return new PersistentDataContainerData(new HashMap<>());
});
}
@Override
public CompletableFuture<Void> setPersistentDataContainer(@NotNull PersistentDataContainerData persistentDataContainerData) {
return CompletableFuture.runAsync(() -> {
player.getPersistentDataContainer().getKeys().forEach(namespacedKey ->
player.getPersistentDataContainer().remove(namespacedKey));
persistentDataContainerData.persistentDataMap.keySet().forEach(keyString -> {
final NamespacedKey key = NamespacedKey.fromString(keyString);
if (key != null) {
final byte[] data = ArrayUtils.toPrimitive(persistentDataContainerData
.persistentDataMap.get(keyString));
player.getPersistentDataContainer().set(key, PersistentDataType.BYTE_ARRAY, data);
}
});
});
}
@Override
public boolean isOffline() {
try {
return player == null;
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
@NotNull
@Override
public Version getMinecraftVersion() {
return Version.minecraftVersion(Bukkit.getBukkitVersion());
}
@Override
public boolean hasPermission(@NotNull String node) {
return player.hasPermission(node);
}
@Override
public void showMenu(@NotNull ItemEditorMenu menu) {
BukkitSerializer.deserializeItemStackArray(menu.itemData.serializedItems).thenAccept(inventoryContents -> {
final Inventory inventory = Bukkit.createInventory(player, menu.itemEditorMenuType.slotCount,
BaseComponent.toLegacyText(menu.menuTitle.toComponent()));
inventory.setContents(inventoryContents);
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> player.openInventory(inventory));
});
}
@Override
public void sendActionBar(@NotNull MineDown mineDown) {
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, mineDown.replace().toComponent());
}
@Override
public void sendMessage(@NotNull MineDown mineDown) {
player.spigot().sendMessage(mineDown.replace().toComponent());
}
/**
* Returns a {@link Player}'s maximum health, minus any health boost effects
*
* @param player The {@link Player} to get the maximum health of
* @return The {@link Player}'s max health
*/
private static double getMaxHealth(@NotNull Player player) {
double maxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).getBaseValue();
// If the player has additional health bonuses from synchronised potion effects, subtract these from this number as they are synchronised separately
if (player.hasPotionEffect(PotionEffectType.HEALTH_BOOST) && maxHealth > 20D) {
PotionEffect healthBoostEffect = player.getPotionEffect(PotionEffectType.HEALTH_BOOST);
assert healthBoostEffect != null;
double healthBoostBonus = 4 * (healthBoostEffect.getAmplifier() + 1);
maxHealth -= healthBoostBonus;
}
return maxHealth;
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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.user;
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.william278.husksync.HuskSync;
import net.william278.husksync.data.BukkitData;
import net.william278.husksync.data.BukkitUserDataHolder;
import net.william278.husksync.data.Data;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.function.Consumer;
import java.util.logging.Level;
/**
* Bukkit platform implementation of an {@link OnlineUser}
*/
public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
private final HuskSync plugin;
private final Player player;
private BukkitUser(@NotNull Player player, @NotNull HuskSync plugin) {
super(player.getUniqueId(), player.getName());
this.player = player;
this.plugin = plugin;
}
@NotNull
@ApiStatus.Internal
public static BukkitUser adapt(@NotNull Player player, @NotNull HuskSync plugin) {
return new BukkitUser(player, plugin);
}
@Override
public boolean isOffline() {
return player == null || !player.isOnline();
}
@Override
@Deprecated(since = "3.6.7")
public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
@NotNull String iconMaterial, @NotNull String backgroundType) {
plugin.log(Level.WARNING, "Toast notifications are deprecated. " +
"Please change your notification display slot to CHAT, ACTION_BAR or NONE.");
this.sendActionBar(title);
}
@Override
public void showGui(@NotNull Data.Items items, @NotNull MineDown title, boolean editable, int size,
@NotNull Consumer<Data.Items> onClose) {
final ItemStack[] contents = ((BukkitData.Items) items).getContents();
final StorageBuilder builder = Gui.storage().rows((int) Math.ceil(size / 9.0d));
if (!editable) {
builder.disableAllInteractions();
}
final StorageGui gui = builder
.apply(a -> a.getInventory().setContents(contents))
.title(title.toComponent()).create();
gui.setCloseGuiAction((close) -> onClose.accept(BukkitData.Items.ItemArray.adapt(
Arrays.stream(close.getInventory().getContents()).limit(size).toArray(ItemStack[]::new)
)));
plugin.runSync(() -> gui.open(player), this);
}
@Override
public boolean hasPermission(@NotNull String node) {
return player.hasPermission(node);
}
@Override
public boolean isDead() {
return player.getHealth() <= 0;
}
@Override
public boolean isLocked() {
return plugin.getLockedPlayers().contains(player.getUniqueId());
}
@Override
public boolean isNpc() {
return player.hasMetadata("NPC");
}
/**
* Get the Bukkit {@link Player} instance of this user
*
* @return the {@link Player} instance
* @since 3.6
*/
@NotNull
public Player getPlayer() {
return player;
}
@NotNull
@Override
@ApiStatus.Internal
public HuskSync getPlugin() {
return plugin;
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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 org.bukkit.*;
import org.bukkit.attribute.Attribute;
import org.bukkit.entity.EntityType;
import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
// Utility class for adapting "Keyed" Bukkit objects
public final class BukkitKeyedAdapter {
@Nullable
public static Statistic matchStatistic(@NotNull String key) {
return getRegistryValue(Registry.STATISTIC, key);
}
@Nullable
public static EntityType matchEntityType(@NotNull String key) {
return getRegistryValue(Registry.ENTITY_TYPE, key);
}
@Nullable
public static Material matchMaterial(@NotNull String key) {
return getRegistryValue(Registry.MATERIAL, key);
}
@Nullable
public static Attribute matchAttribute(@NotNull String key) {
return getRegistryValue(Registry.ATTRIBUTE, key);
}
@Nullable
public static PotionEffectType matchEffectType(@NotNull String key) {
//#if MC==12001
//$$ return PotionEffectType.getByName(key);
//#else
return getRegistryValue(Registry.EFFECT, key);
//#endif
}
private static <T extends Keyed> T getRegistryValue(@NotNull Registry<T> registry, @NotNull String keyString) {
final NamespacedKey key = NamespacedKey.fromString(keyString);
return key != null ? registry.get(key) : null;
}
}

View File

@@ -0,0 +1,293 @@
/*
* 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 com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.DataAdapter;
import net.william278.husksync.data.BukkitData;
import net.william278.husksync.data.Data;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.data.Identifier;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
import java.io.ByteArrayInputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.OffsetDateTime;
import java.util.*;
import java.util.logging.Level;
import static net.william278.husksync.util.BukkitKeyedAdapter.matchEntityType;
import static net.william278.husksync.util.BukkitKeyedAdapter.matchMaterial;
public class BukkitLegacyConverter extends LegacyConverter {
public BukkitLegacyConverter(@NotNull HuskSync plugin) {
super(plugin);
}
@Override
@NotNull
public DataSnapshot.Packed convert(byte @NotNull [] data, @NotNull UUID id,
@NotNull OffsetDateTime timestamp) throws DataAdapter.AdaptionException {
final JSONObject object = new JSONObject(plugin.getDataAdapter().bytesToString(data));
final int version = object.getInt("format_version");
if (version != 3) {
plugin.log(Level.WARNING, String.format("Converting data from older v2 data format (%s).", version));
}
// Read legacy data from the JSON object
final DataSnapshot.Builder builder = DataSnapshot.builder(plugin)
.id(id).timestamp(timestamp)
.saveCause(DataSnapshot.SaveCause.CONVERTED_FROM_V2)
.data(readStatusData(object));
readInventory(object).ifPresent(builder::inventory);
readEnderChest(object).ifPresent(builder::enderChest);
readLocation(object).ifPresent(builder::location);
readAdvancements(object).ifPresent(builder::advancements);
readStatistics(object).ifPresent(builder::statistics);
return builder.buildAndPack();
}
@NotNull
private Map<Identifier, Data> readStatusData(@NotNull JSONObject object) {
if (!object.has("status_data")) {
return Map.of();
}
final JSONObject status = object.getJSONObject("status_data");
final HashMap<Identifier, Data> containers = Maps.newHashMap();
if (Identifier.HEALTH.isEnabled()) {
containers.put(Identifier.HEALTH, BukkitData.Health.from(
status.getDouble("health"),
status.getDouble("health_scale"),
false
));
}
if (Identifier.HUNGER.isEnabled()) {
containers.put(Identifier.HUNGER, BukkitData.Hunger.from(
status.getInt("hunger"),
status.getFloat("saturation"),
status.getFloat("saturation_exhaustion")
));
}
if (Identifier.EXPERIENCE.isEnabled()) {
containers.put(Identifier.EXPERIENCE, BukkitData.Experience.from(
status.getInt("total_experience"),
status.getInt("experience_level"),
status.getFloat("experience_progress")
));
}
if (Identifier.GAME_MODE.isEnabled()) {
containers.put(Identifier.GAME_MODE, BukkitData.GameMode.from(
status.getString("game_mode")
));
}
if (Identifier.FLIGHT_STATUS.isEnabled()) {
containers.put(Identifier.FLIGHT_STATUS, BukkitData.FlightStatus.from(
status.getBoolean("is_flying"),
status.getBoolean("is_flying")
));
}
return containers;
}
@NotNull
private Optional<Data.Items.Inventory> readInventory(@NotNull JSONObject object) {
if (!object.has("inventory") || !Identifier.INVENTORY.isEnabled()) {
return Optional.empty();
}
final JSONObject inventoryData = object.getJSONObject("inventory");
return Optional.of(BukkitData.Items.Inventory.from(
deserializeLegacyItemStacks(inventoryData.getString("serialized_items")), 0
));
}
@NotNull
private Optional<Data.Items.EnderChest> readEnderChest(@NotNull JSONObject object) {
if (!object.has("ender_chest") || !Identifier.ENDER_CHEST.isEnabled()) {
return Optional.empty();
}
final JSONObject inventoryData = object.getJSONObject("ender_chest");
return Optional.of(BukkitData.Items.EnderChest.adapt(
deserializeLegacyItemStacks(inventoryData.getString("serialized_items"))
));
}
@NotNull
private Optional<Data.Location> readLocation(@NotNull JSONObject object) {
if (!object.has("location") || !Identifier.LOCATION.isEnabled()) {
return Optional.empty();
}
final JSONObject locationData = object.getJSONObject("location");
return Optional.of(BukkitData.Location.from(
locationData.getDouble("x"),
locationData.getDouble("y"),
locationData.getDouble("z"),
locationData.getFloat("yaw"),
locationData.getFloat("pitch"),
new Data.Location.World(
locationData.getString("world_name"),
UUID.fromString(locationData.getString("world_uuid")),
locationData.getString("world_environment")
)
));
}
@NotNull
private Optional<Data.Advancements> readAdvancements(@NotNull JSONObject object) {
if (!object.has("advancements") || !Identifier.ADVANCEMENTS.isEnabled()) {
return Optional.empty();
}
final JSONArray advancements = object.getJSONArray("advancements");
final List<Data.Advancements.Advancement> converted = Lists.newArrayList();
advancements.iterator().forEachRemaining(o -> {
final JSONObject advancement = (JSONObject) JSONObject.wrap(o);
final String key = advancement.getString("key");
final JSONObject criteria = advancement.getJSONObject("completed_criteria");
final Map<String, Date> criteriaMap = new LinkedHashMap<>();
criteria.keys().forEachRemaining(criteriaKey -> criteriaMap.put(
criteriaKey, parseDate(criteria.getString(criteriaKey)))
);
converted.add(Data.Advancements.Advancement.adapt(key, criteriaMap));
});
return Optional.of(BukkitData.Advancements.from(converted));
}
@NotNull
private Optional<Data.Statistics> readStatistics(@NotNull JSONObject object) {
if (!object.has("statistics") || !Identifier.STATISTICS.isEnabled()) {
return Optional.empty();
}
final JSONObject stats = object.getJSONObject("statistics");
return Optional.of(readStatisticMaps(
stats.getJSONObject("untyped_statistics"),
stats.getJSONObject("block_statistics"),
stats.getJSONObject("item_statistics"),
stats.getJSONObject("entity_statistics")
));
}
@NotNull
private BukkitData.Statistics readStatisticMaps(@NotNull JSONObject untyped, @NotNull JSONObject blocks,
@NotNull JSONObject items, @NotNull JSONObject entities) {
// Read generic stats
final Map<String, Integer> genericStats = Maps.newHashMap();
untyped.keys().forEachRemaining(stat -> genericStats.put(stat, untyped.getInt(stat)));
// Read block & item stats
final Map<String, Map<String, Integer>> blockStats, itemStats, entityStats;
blockStats = readMaterialStatistics(blocks);
itemStats = readMaterialStatistics(items);
// Read entity stats
entityStats = Maps.newHashMap();
entities.keys().forEachRemaining(stat -> {
final JSONObject entityStat = entities.getJSONObject(stat);
final Map<String, Integer> entityMap = Maps.newHashMap();
entityStat.keys().forEachRemaining(entity -> {
if (matchEntityType(entity) != null) {
entityMap.put(entity, entityStat.getInt(entity));
}
});
entityStats.put(stat, entityMap);
});
return BukkitData.Statistics.from(genericStats, blockStats, itemStats, entityStats);
}
@NotNull
private Map<String, Map<String, Integer>> readMaterialStatistics(@NotNull JSONObject items) {
final Map<String, Map<String, Integer>> itemStats = Maps.newHashMap();
items.keys().forEachRemaining(stat -> {
final JSONObject itemStat = items.getJSONObject(stat);
final Map<String, Integer> itemMap = Maps.newHashMap();
itemStat.keys().forEachRemaining(item -> {
if (matchMaterial(item) != null) {
itemMap.put(item, itemStat.getInt(item));
}
});
itemStats.put(stat, itemMap);
});
return itemStats;
}
// Deserialize a legacy item stack array
@NotNull
public ItemStack[] deserializeLegacyItemStacks(@NotNull String items) {
// Return an empty array if there is no inventory data (set the player as having an empty inventory)
if (items.isEmpty()) {
return new ItemStack[0];
}
// Create a byte input stream to read the serialized data
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(items))) {
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
// Read the length of the Bukkit input stream and set the length of the array to this value
final ItemStack[] inventoryContents = new ItemStack[bukkitInputStream.readInt()];
// Set the ItemStacks in the array from deserialized ItemStack data
int slotIndex = 0;
for (ItemStack ignored : inventoryContents) {
final ItemStack deserialized = deserializeLegacyItemStack(bukkitInputStream.readObject());
inventoryContents[slotIndex] = deserialized;
slotIndex++;
}
// Return the converted contents
return inventoryContents;
}
} catch (Throwable e) {
throw new DataAdapter.AdaptionException("Failed to deserialize legacy item stack data", e);
}
}
// Deserialize a single legacy item stack
@SuppressWarnings("unchecked")
@Nullable
private static ItemStack deserializeLegacyItemStack(@Nullable Object serializedItemStack) {
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
}
@NotNull
private Date parseDate(@NotNull String dateString) {
try {
return new SimpleDateFormat().parse(dateString);
} catch (ParseException e) {
return new Date();
}
}
}

View File

@@ -1,47 +0,0 @@
package net.william278.husksync.util;
import de.themoep.minedown.MineDown;
import net.md_5.bungee.api.chat.TextComponent;
import org.jetbrains.annotations.NotNull;
import java.util.logging.Level;
public class BukkitLogger extends Logger {
private final java.util.logging.Logger logger;
public BukkitLogger(@NotNull java.util.logging.Logger logger) {
this.logger = logger;
}
@Override
public void log(@NotNull Level level, @NotNull String message, @NotNull Exception e) {
logger.log(level, message, e);
}
@Override
public void log(@NotNull Level level, @NotNull String message) {
logger.log(level, message);
}
@Override
public void log(@NotNull Level level, @NotNull MineDown mineDown) {
logger.log(level, TextComponent.toLegacyText(mineDown.toComponent()));
}
@Override
public void info(@NotNull String message) {
logger.info(message);
}
@Override
public void severe(@NotNull String message) {
logger.severe(message);
}
@Override
public void config(@NotNull String message) {
logger.config(message);
}
}

View File

@@ -1,28 +0,0 @@
package net.william278.husksync.util;
import net.william278.husksync.BukkitHuskSync;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.InputStream;
import java.util.Objects;
public class BukkitResourceReader implements ResourceReader {
private final BukkitHuskSync plugin;
public BukkitResourceReader(BukkitHuskSync plugin) {
this.plugin = plugin;
}
@Override
public @NotNull InputStream getResource(String fileName) {
return Objects.requireNonNull(plugin.getResource(fileName));
}
@Override
public @NotNull File getDataFolder() {
return plugin.getDataFolder();
}
}

View File

@@ -0,0 +1,190 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.util;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.UserDataHolder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
import space.arim.morepaperlib.scheduling.AttachedScheduler;
import space.arim.morepaperlib.scheduling.RegionalScheduler;
import space.arim.morepaperlib.scheduling.ScheduledTask;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
public interface BukkitTask extends Task {
class Sync extends Task.Sync implements BukkitTask {
private ScheduledTask task;
private final @Nullable UserDataHolder user;
protected Sync(@NotNull HuskSync plugin, @NotNull Runnable runnable,
@Nullable UserDataHolder user, long delayTicks) {
super(plugin, runnable, delayTicks);
this.user = user;
}
@Override
public void cancel() {
if (task != null && !cancelled) {
task.cancel();
}
super.cancel();
}
@Override
public void run() {
if (isPluginDisabled()) {
runnable.run();
return;
}
if (cancelled) {
return;
}
// Use entity-specific scheduler if user is not null
if (user != null) {
final AttachedScheduler scheduler = ((BukkitHuskSync) getPlugin()).getUserSyncScheduler(user);
if (delayTicks > 0) {
this.task = scheduler.runDelayed(runnable, null, delayTicks);
} else {
this.task = scheduler.run(runnable, null);
}
return;
}
// Or default to the global scheduler
final RegionalScheduler scheduler = ((BukkitHuskSync) getPlugin()).getSyncScheduler();
if (delayTicks > 0) {
this.task = scheduler.runDelayed(runnable, delayTicks);
} else {
this.task = scheduler.run(runnable);
}
}
}
class Async extends Task.Async implements BukkitTask {
private ScheduledTask task;
protected Async(@NotNull HuskSync plugin, @NotNull Runnable runnable, long delayTicks) {
super(plugin, runnable, delayTicks);
}
@Override
public void cancel() {
if (task != null && !cancelled) {
task.cancel();
}
super.cancel();
}
@Override
public void run() {
if (isPluginDisabled()) {
runnable.run();
return;
}
if (cancelled) {
return;
}
final AsynchronousScheduler scheduler = ((BukkitHuskSync) getPlugin()).getAsyncScheduler();
if (delayTicks > 0) {
plugin.debug("Running async task with delay of " + delayTicks + " ticks");
this.task = scheduler.runDelayed(
runnable,
Duration.of(delayTicks * 50L, ChronoUnit.MILLIS)
);
} else {
this.task = scheduler.run(runnable);
}
}
}
class Repeating extends Task.Repeating implements BukkitTask {
private ScheduledTask task;
protected Repeating(@NotNull HuskSync plugin, @NotNull Runnable runnable, long repeatingTicks) {
super(plugin, runnable, repeatingTicks);
}
@Override
public void cancel() {
if (task != null && !cancelled) {
task.cancel();
}
super.cancel();
}
@Override
public void run() {
if (isPluginDisabled()) {
return;
}
if (!cancelled) {
final AsynchronousScheduler scheduler = ((BukkitHuskSync) getPlugin()).getAsyncScheduler();
this.task = scheduler.runAtFixedRate(
runnable, Duration.ZERO,
Duration.of(repeatingTicks * 50L, ChronoUnit.MILLIS)
);
}
}
}
// Returns if the Bukkit HuskSync plugin is disabled
default boolean isPluginDisabled() {
return !((BukkitHuskSync) getPlugin()).isEnabled();
}
interface Supplier extends Task.Supplier {
@NotNull
@Override
default Task.Sync getSyncTask(@NotNull Runnable runnable, @Nullable UserDataHolder user, long delayTicks) {
return new Sync(getPlugin(), runnable, user, delayTicks);
}
@NotNull
@Override
default Task.Async getAsyncTask(@NotNull Runnable runnable, long delayTicks) {
return new Async(getPlugin(), runnable, delayTicks);
}
@NotNull
@Override
default Task.Repeating getRepeatingTask(@NotNull Runnable runnable, long repeatingTicks) {
return new Repeating(getPlugin(), runnable, repeatingTicks);
}
@Override
default void cancelTasks() {
((BukkitHuskSync) getPlugin()).getScheduler().cancelGlobalTasks();
}
}
}

View File

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

View File

@@ -0,0 +1,8 @@
# Dependencies for HuskSync on Paper
libraries:
- 'redis.clients:jedis:${jedis_version}'
- 'com.mysql:mysql-connector-j:${mysql_driver_version}'
- 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}'
- 'org.postgresql:postgresql:${postgres_driver_version}'
- 'org.mongodb:mongodb-driver-sync:${mongodb_driver_version}'
- 'org.xerial.snappy:snappy-java:${snappy_version}'

View File

@@ -0,0 +1,27 @@
name: 'HuskSync'
description: '${description}'
author: 'William278'
website: 'https://william278.net/'
main: 'net.william278.husksync.PaperHuskSync'
loader: 'net.william278.husksync.PaperHuskSyncLoader'
version: '${version}'
api-version: '${minecraft_api_version}'
folia-supported: true
dependencies:
server:
packetevents:
required: false
load: BEFORE
join-classpath: true
ProtocolLib:
required: false
load: BEFORE
join-classpath: true
MysqlPlayerDataBridge:
required: false
load: BEFORE
join-classpath: true
Plan:
required: false
load: BEFORE
join-classpath: true

View File

@@ -1,27 +1,20 @@
name: HuskSync
version: ${version}
main: net.william278.husksync.BukkitHuskSync
api-version: 1.16
author: William278
description: 'A modern, cross-server player data synchronization system'
name: 'HuskSync'
version: '${version}'
main: 'net.william278.husksync.BukkitHuskSync'
api-version: '${minecraft_api_version}'
author: 'William278'
description: '${description}'
website: 'https://william278.net'
folia-supported: true
softdepend:
- MysqlPlayerDataBridge
- Plan
- 'packetevents'
- 'ProtocolLib'
- 'MysqlPlayerDataBridge'
- 'Plan'
libraries:
- 'mysql:mysql-connector-java:8.0.29'
- 'org.xerial.snappy:snappy-java:1.1.8.4'
- 'dev.dejvokep:boosted-yaml:1.2'
commands:
husksync:
usage: '/husksync <update/info/reload/migrate>'
description: 'Manage the HuskSync plugin'
userdata:
usage: '/userdata <view/list/delete/restore/pin> <username> [version_uuid]'
description: 'View, manage & restore player userdata'
inventory:
usage: '/inventory <username> [version_uuid]'
description: 'View & edit a player''s inventory'
enderchest:
usage: '/enderchest <username> [version_uuid]'
description: 'View & edit a player''s Ender Chest'
- 'redis.clients:jedis:${jedis_version}'
- 'com.mysql:mysql-connector-j:${mysql_driver_version}'
- 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}'
- 'org.postgresql:postgresql:${postgres_driver_version}'
- 'org.mongodb:mongodb-driver-sync:${mongodb_driver_version}'
- 'org.xerial.snappy:snappy-java:${snappy_version}'

View File

@@ -1,31 +1,46 @@
dependencies {
implementation 'commons-io:commons-io:2.11.0'
implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
implementation 'com.google.code.gson:gson:2.9.0'
implementation('redis.clients:jedis:4.2.3') {
exclude module: 'slf4j-api'
}
implementation ('com.zaxxer:HikariCP:5.0.1') {
exclude module: 'slf4j-api'
}
compileOnly 'dev.dejvokep:boosted-yaml:1.2'
compileOnly 'org.xerial.snappy:snappy-java:1.1.8.4'
compileOnly 'org.jetbrains:annotations:23.0.0'
compileOnly 'com.github.plan-player-analytics:Plan:5.4.1690'
testImplementation 'org.xerial.snappy:snappy-java:1.1.8.4'
testImplementation 'com.github.plan-player-analytics:Plan:5.4.1690'
testCompileOnly 'org.jetbrains:annotations:23.0.0'
plugins {
id 'java-library'
}
shadowJar {
relocate 'org.apache', 'net.william278.husksync.libraries'
relocate 'de.themoep', 'net.william278.husksync.libraries'
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
relocate 'org.intellij', 'net.william278.husksync.libraries'
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
relocate 'com.google', 'net.william278.husksync.libraries'
relocate 'redis.clients', 'net.william278.husksync.libraries'
relocate 'org.json', 'net.william278.husksync.libraries.json'
dependencies {
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 'net.william278:mapdataapi:2.0'
api 'org.json:json:20250107'
api 'com.google.code.gson:gson:2.12.1'
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:6.2.1') {
exclude module: 'slf4j-api'
}
compileOnlyApi 'net.william278.toilet:toilet-common:1.0.12'
compileOnly 'net.william278.uniform:uniform-common:1.3.1'
compileOnly 'com.mojang:brigadier:1.1.8'
compileOnly 'org.projectlombok:lombok:1.18.36'
compileOnly 'org.jetbrains:annotations:26.0.2'
compileOnly 'net.kyori:adventure-api:4.19.0'
compileOnly 'net.kyori:adventure-platform-api:4.3.4'
compileOnly "net.kyori:adventure-text-serializer-plain:4.19.0"
compileOnly 'com.google.guava:guava:33.4.0-jre'
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
compileOnly "redis.clients:jedis:$jedis_version"
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
compileOnly "org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version"
compileOnly "org.postgresql:postgresql:$postgres_driver_version"
compileOnly "org.mongodb:mongodb-driver-sync:$mongodb_driver_version"
compileOnly "org.xerial.snappy:snappy-java:$snappy_version"
testImplementation "redis.clients:jedis:$jedis_version"
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
testImplementation 'com.google.guava:guava:33.4.0-jre'
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
testCompileOnly 'de.exlll:configlib-yaml:4.5.0'
testCompileOnly 'org.jetbrains:annotations:26.0.2'
annotationProcessor 'org.projectlombok:lombok:1.18.36'
}

View File

@@ -1,28 +1,64 @@
/*
* 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;
import net.william278.husksync.config.Locales;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataAdapter;
import net.william278.husksync.editor.DataEditor;
import com.fatboyindustrial.gsonjavatime.Converters;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.AudienceProvider;
import net.william278.desertwell.util.ThrowingConsumer;
import net.william278.desertwell.util.UpdateChecker;
import net.william278.desertwell.util.Version;
import net.william278.husksync.adapter.DataAdapter;
import net.william278.husksync.config.ConfigProvider;
import net.william278.husksync.data.Data;
import net.william278.husksync.data.Identifier;
import net.william278.husksync.data.SerializerRegistry;
import net.william278.husksync.database.Database;
import net.william278.husksync.event.EventCannon;
import net.william278.husksync.event.EventDispatcher;
import net.william278.husksync.listener.LockedHandler;
import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.util.Logger;
import net.william278.husksync.util.Version;
import net.william278.husksync.sync.DataSyncer;
import net.william278.husksync.user.ConsoleUser;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.util.CompatibilityChecker;
import net.william278.husksync.util.DumpProvider;
import net.william278.husksync.util.LegacyConverter;
import net.william278.husksync.util.Task;
import net.william278.uniform.Uniform;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Level;
/**
* Abstract implementation of the HuskSync plugin.
*/
public interface HuskSync {
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider, SerializerRegistry,
CompatibilityChecker, DumpProvider {
int SPIGOT_RESOURCE_ID = 97144;
/**
* Returns a set of online players.
@@ -54,33 +90,39 @@ public interface HuskSync {
*
* @return the {@link RedisManager} implementation
*/
@NotNull
RedisManager getRedisManager();
/**
* Returns the data adapter implementation
* Returns the implementing adapter for serializing data
*
* @return the {@link DataAdapter} implementation
* @return the {@link DataAdapter}
*/
@NotNull
DataAdapter getDataAdapter();
/**
* Returns the data editor implementation
* Returns the data syncer implementation
*
* @return the {@link DataEditor} implementation
* @return the {@link DataSyncer} implementation
*/
@NotNull
DataEditor getDataEditor();
DataSyncer getDataSyncer();
/**
* Returns the event firing cannon
* Set the data syncer implementation
*
* @return the {@link EventCannon} implementation
* @param dataSyncer the {@link DataSyncer} implementation
*/
void setDataSyncer(@NotNull DataSyncer dataSyncer);
/**
* Get the uniform command provider
*
* @return the command provider
*/
@NotNull
EventCannon getEventCannon();
Uniform getUniform();
/**
* Returns a list of available data {@link Migrator}s
@@ -90,29 +132,108 @@ public interface HuskSync {
@NotNull
List<Migrator> getAvailableMigrators();
/**
* Returns the plugin {@link Settings}
*
* @return the {@link Settings}
*/
@NotNull
Settings getSettings();
Map<UUID, Map<Identifier, Data>> getPlayerCustomDataStore();
@NotNull
default Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
if (getPlayerCustomDataStore().containsKey(user.getUuid())) {
return getPlayerCustomDataStore().get(user.getUuid());
}
final Map<Identifier, Data> data = new HashMap<>();
getPlayerCustomDataStore().put(user.getUuid(), data);
return data;
}
/**
* Returns the plugin {@link Locales}
* Initialize a faucet of the plugin.
*
* @return the {@link Locales}
* @param name the name of the faucet
* @param runner a runnable for initializing the faucet
*/
@NotNull
Locales getLocales();
default void initialize(@NotNull String name, @NotNull ThrowingConsumer<HuskSync> runner) {
log(Level.INFO, "Initializing " + name + "...");
try {
runner.accept(this);
} catch (Throwable e) {
throw new FailedToLoadException("Failed to initialize " + name, e);
}
log(Level.INFO, "Successfully initialized " + name);
}
/**
* Returns the plugin {@link Logger}
* Returns if a dependency is loaded
*
* @return the {@link Logger}
* @param name the name of the dependency
* @return {@code true} if the dependency is loaded, {@code false} otherwise
*/
boolean isDependencyLoaded(@NotNull String name);
/**
* Get a resource as an {@link InputStream} from the plugin jar
*
* @param name the path to the resource
* @return the {@link InputStream} of the resource
*/
InputStream getResource(@NotNull String name);
/**
* Log a message to the console
*
* @param level the level of the message
* @param message the message to log
* @param throwable a throwable to log
*/
void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable);
/**
* Send a debug message to the console, if debug logging is enabled
*
* @param message the message to log
* @param throwable a throwable to log
*/
default void debug(@NotNull String message, @NotNull Throwable... throwable) {
if (getSettings().isDebugLogging()) {
log(Level.INFO, getDebugString(message), throwable);
}
}
// Get the debug log message format
@NotNull
private String getDebugString(@NotNull String message) {
return String.format("[DEBUG] [%s] %s", new SimpleDateFormat("mm:ss.SSS").format(new Date()), message);
}
/**
* Get the {@link AudienceProvider} instance
*
* @return the {@link AudienceProvider} instance
* @since 1.0
*/
@NotNull
Logger getLoggingAdapter();
AudienceProvider getAudiences();
/**
* Get the {@link Audience} instance for the given {@link OnlineUser}
*
* @param user the {@link OnlineUser} to get the {@link Audience} for
* @return the {@link Audience} instance
*/
@NotNull
default Audience getAudience(@NotNull UUID user) {
return getAudiences().player(user);
}
/**
* Get the {@link ConsoleUser} instance
*
* @return the {@link ConsoleUser} instance
* @since 1.0
*/
@NotNull
default ConsoleUser getConsole() {
return new ConsoleUser(getAudiences());
}
/**
* Returns the plugin version
@@ -131,10 +252,115 @@ public interface HuskSync {
Version getMinecraftVersion();
/**
* Reloads the {@link Settings} and {@link Locales} from their respective config files
* Returns the data version for a Minecraft version
*
* @return a {@link CompletableFuture} that will be completed when the plugin reload is complete and if it was successful
* @param minecraftVersion the Minecraft version
* @return the data version int
*/
CompletableFuture<Boolean> reload();
int getDataVersion(@NotNull Version minecraftVersion);
/**
* Returns the platform type
*
* @return the platform type
*/
@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
*
* @return the {@link LegacyConverter}
*/
Optional<LegacyConverter> getLegacyConverter();
@NotNull
default UpdateChecker getUpdateChecker() {
return UpdateChecker.builder()
.currentVersion(getPluginVersion())
.endpoint(UpdateChecker.Endpoint.SPIGOT)
.resource(Integer.toString(SPIGOT_RESOURCE_ID))
.build();
}
default void checkForUpdates() {
if (getSettings().isCheckForUpdates()) {
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())
);
}
});
}
}
@NotNull
LockedHandler getLockedHandler();
/**
* Get the set of UUIDs of "locked players", for which events will be canceled.
* </p>
* Players are locked while their items are being set (on join) or saved (on quit)
*/
@NotNull
Set<UUID> getLockedPlayers();
default boolean isLocked(@NotNull UUID uuid) {
return getLockedPlayers().contains(uuid);
}
default void lockPlayer(@NotNull UUID uuid) {
getLockedPlayers().add(uuid);
}
default void unlockPlayer(@NotNull UUID uuid) {
getLockedPlayers().remove(uuid);
}
@NotNull
Gson getGson();
boolean isDisabling();
@NotNull
default Gson createGson() {
return Converters.registerOffsetDateTime(new GsonBuilder()).create();
}
/**
* An exception indicating the plugin has been accessed before it has been registered.
*/
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""";
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

@@ -1,12 +0,0 @@
package net.william278.husksync;
import org.jetbrains.annotations.NotNull;
/**
* Indicates an exception occurred while initialising the HuskSync plugin
*/
public class HuskSyncInitializationException extends RuntimeException {
public HuskSyncInitializationException(@NotNull String message) {
super(message);
}
}

View File

@@ -0,0 +1,24 @@
/*
* 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.adapter;
public interface Adaptable {
}

View File

@@ -0,0 +1,108 @@
/*
* 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.adapter;
import org.jetbrains.annotations.NotNull;
import java.nio.charset.StandardCharsets;
/**
* An adapter that adapts data to and from a portable byte array.
*/
public interface DataAdapter {
/**
* Converts an {@link Adaptable} to a string.
*
* @param data The {@link Adaptable} to adapt
* @param <A> The type of the {@link Adaptable}
* @return The string
* @throws AdaptionException If an error occurred during adaptation.
*/
@NotNull
default <A extends Adaptable> String toString(@NotNull A data) throws AdaptionException {
return new String(this.toBytes(data), StandardCharsets.UTF_8);
}
/**
* Converts an {@link Adaptable} to a byte array.
*
* @param data The {@link Adaptable} to adapt
* @param <A> The type of the {@link Adaptable}
* @return The byte array
* @throws AdaptionException If an error occurred during adaptation.
*/
<A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException;
/**
* Converts a JSON string to an {@link Adaptable}.
*
* @param data The JSON string to adapt.
* @param type The class type of the {@link Adaptable} to adapt to.
* @param <A> The type of the {@link Adaptable}
* @return The {@link Adaptable}
* @throws AdaptionException If an error occurred during adaptation.
*/
@NotNull
<A extends Adaptable> A fromJson(@NotNull String data, @NotNull Class<A> type) throws AdaptionException;
/**
* Converts an {@link Adaptable} to a JSON string.
*
* @param data The {@link Adaptable} to adapt
* @param <A> The type of the {@link Adaptable}
* @return The JSON string
* @throws AdaptionException If an error occurred during adaptation.
*/
@NotNull
<A extends Adaptable> String toJson(@NotNull A data) throws AdaptionException;
/**
* Converts a byte array to an {@link Adaptable}.
*
* @param data The byte array to adapt.
* @param type The class type of the {@link Adaptable} to adapt to.
* @param <A> The type of the {@link Adaptable}
* @return The {@link Adaptable}
* @throws AdaptionException If an error occurred during adaptation.
*/
<A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException;
/**
* Converts a byte array to a string, including decompression if required.
*
* @param bytes The byte array to convert
* @return the string form of the bytes
*/
@NotNull
String bytesToString(byte[] bytes);
final class AdaptionException extends IllegalStateException {
static final String FORMAT = "An exception occurred when adapting serialized/deserialized data: %s";
public AdaptionException(@NotNull String message, @NotNull Throwable cause) {
super(String.format(FORMAT, message), cause);
}
public AdaptionException(@NotNull String message) {
super(String.format(FORMAT, message));
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.adapter;
import net.william278.husksync.HuskSync;
import org.jetbrains.annotations.NotNull;
import java.nio.charset.StandardCharsets;
public class GsonAdapter implements DataAdapter {
private final HuskSync plugin;
public GsonAdapter(@NotNull HuskSync plugin) {
this.plugin = plugin;
}
@Override
public <A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException {
return this.toJson(data).getBytes(StandardCharsets.UTF_8);
}
@NotNull
@Override
public <A extends Adaptable> String toJson(@NotNull A data) throws AdaptionException {
try {
return plugin.getGson().toJson(data);
} catch (Throwable e) {
throw new AdaptionException("Failed to adapt data to JSON via Gson", e);
}
}
@Override
@NotNull
public <A extends Adaptable> A fromBytes(byte[] data, @NotNull Class<A> type) throws AdaptionException {
return this.fromJson(new String(data, StandardCharsets.UTF_8), type);
}
@NotNull
@Override
public String bytesToString(byte[] bytes) {
return new String(bytes, StandardCharsets.UTF_8);
}
@Override
@NotNull
public <A extends Adaptable> A fromJson(@NotNull String data, @NotNull Class<A> type) throws AdaptionException {
try {
return plugin.getGson().fromJson(data, type);
} catch (Throwable e) {
throw new AdaptionException("Failed to adapt data from JSON via Gson", e);
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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.adapter;
import net.william278.husksync.HuskSync;
import org.jetbrains.annotations.NotNull;
import org.xerial.snappy.Snappy;
import java.io.IOException;
public class SnappyGsonAdapter extends GsonAdapter {
public SnappyGsonAdapter(@NotNull HuskSync plugin) {
super(plugin);
}
@Override
public <A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException {
try {
return Snappy.compress(super.toBytes(data));
} catch (IOException e) {
throw new AdaptionException("Failed to compress data through Snappy", e);
}
}
@NotNull
@Override
public <A extends Adaptable> A fromBytes(byte[] data, @NotNull Class<A> type) throws AdaptionException {
try {
return super.fromBytes(decompressBytes(data), type);
} catch (IOException e) {
throw new AdaptionException("Failed to decompress data through Snappy", e);
}
}
@Override
@NotNull
public String bytesToString(byte[] bytes) {
try {
return super.bytesToString(decompressBytes(bytes));
} catch (IOException e) {
throw new AdaptionException("Failed to decompress data through Snappy", e);
}
}
private byte[] decompressBytes(byte[] bytes) throws IOException {
return Snappy.uncompress(bytes);
}
}

View File

@@ -1,137 +0,0 @@
package net.william278.husksync.api;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.UserData;
import net.william278.husksync.data.UserDataSnapshot;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
* The base implementation of the HuskSync API, containing cross-platform API calls.
* </p>
* This class should not be used directly, but rather through platform-specific extending API classes.
*/
@SuppressWarnings("unused")
public abstract class BaseHuskSyncAPI {
/**
* <b>(Internal use only)</b> - Instance of the implementing plugin.
*/
protected final HuskSync plugin;
protected BaseHuskSyncAPI(@NotNull HuskSync plugin) {
this.plugin = plugin;
}
/**
* Returns a {@link User} by the given player's account {@link UUID}, if they exist.
*
* @param uuid the unique id of the player to get the {@link User} instance for
* @return future returning the {@link User} instance for the given player's unique id if they exist, otherwise an empty {@link Optional}
* @apiNote The player does not have to be online
* @since 2.0
*/
public final CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid) {
return plugin.getDatabase().getUser(uuid);
}
/**
* Returns a {@link User} by the given player's username (case-insensitive), if they exist.
*
* @param username the username of the {@link User} instance for
* @return future returning the {@link User} instance for the given player's username if they exist,
* otherwise an empty {@link Optional}
* @apiNote The player does not have to be online, though their username has to be the username
* they had when they last joined the server.
* @since 2.0
*/
public final CompletableFuture<Optional<User>> getUser(@NotNull String username) {
return plugin.getDatabase().getUserByName(username);
}
/**
* Returns a {@link User}'s current {@link UserData}
*
* @param user the {@link User} to get the {@link UserData} for
* @return future returning the {@link UserData} for the given {@link User} if they exist, otherwise an empty {@link Optional}
* @apiNote If the user is not online on the implementing bukkit server,
* the {@link UserData} returned will be their last database-saved UserData.
* </p>
* Because of this, if the user is online on another server on the network,
* then the {@link UserData} returned by this method will <i>not necessarily reflective of
* their current state</i>
* @since 2.0
*/
public final CompletableFuture<Optional<UserData>> getUserData(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> {
if (user instanceof OnlineUser) {
return ((OnlineUser) user).getUserData(plugin.getLoggingAdapter()).join();
} else {
return plugin.getDatabase().getCurrentUserData(user).join().map(UserDataSnapshot::userData);
}
});
}
/**
* Sets the {@link UserData} to the database for the given {@link User}.
* </p>
* If the user is online and on the same cluster, their data will be updated in game.
*
* @param user the {@link User} to set the {@link UserData} for
* @param userData the {@link UserData} to set for the given {@link User}
* @return future returning void when complete
* @since 2.0
*/
public final CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData) {
return CompletableFuture.runAsync(() ->
plugin.getDatabase().setUserData(user, userData, DataSaveCause.API)
.thenRun(() -> plugin.getRedisManager().sendUserDataUpdate(user, userData).join()));
}
/**
* Saves the {@link UserData} of an {@link OnlineUser} to the database
*
* @param user the {@link OnlineUser} to save the {@link UserData} of
* @return future returning void when complete
* @since 2.0
*/
public final CompletableFuture<Void> saveUserData(@NotNull OnlineUser user) {
return CompletableFuture.runAsync(() -> user.getUserData(plugin.getLoggingAdapter()).thenAccept(optionalUserData -> optionalUserData.ifPresent(
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.API).join())));
}
/**
* Returns the saved {@link UserDataSnapshot} records for the given {@link User}
*
* @param user the {@link User} to get the {@link UserDataSnapshot} for
* @return future returning a list {@link UserDataSnapshot} for the given {@link User} if they exist,
* otherwise an empty {@link Optional}
* @apiNote The length of the list of VersionedUserData will correspond to the configured
* {@code max_user_data_records} config option
* @since 2.0
*/
public final CompletableFuture<List<UserDataSnapshot>> getSavedUserData(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> plugin.getDatabase().getUserData(user).join());
}
/**
* Returns the JSON string representation of the given {@link UserData}
*
* @param userData the {@link UserData} to get the JSON string representation of
* @param prettyPrint whether to pretty print the JSON string
* @return the JSON string representation of the given {@link UserData}
* @since 2.0
*/
@NotNull
public final String getUserDataJson(@NotNull UserData userData, boolean prettyPrint) {
return plugin.getDataAdapter().toJson(userData, prettyPrint);
}
}

View File

@@ -0,0 +1,543 @@
/*
* 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.api;
import net.william278.desertwell.util.ThrowingConsumer;
import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.Adaptable;
import net.william278.husksync.data.Data;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.data.Identifier;
import net.william278.husksync.data.Serializer;
import net.william278.husksync.sync.DataSyncer;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
/**
* The common implementation of the HuskSync API, containing cross-platform API calls.
* </p>
* Retrieve an instance of the API class via {@link #getInstance()}.
*
* @since 2.0
*/
@SuppressWarnings("unused")
public class HuskSyncAPI {
// Instance of the plugin
protected static HuskSyncAPI instance;
/**
* <b>(Internal use only)</b> - Instance of the implementing plugin.
*/
protected final HuskSync plugin;
/**
* <b>(Internal use only)</b> - Constructor, instantiating the base API class.
*/
@ApiStatus.Internal
protected HuskSyncAPI(@NotNull HuskSync plugin) {
this.plugin = plugin;
}
/**
* Entrypoint to the HuskSync API on the common platform - returns an instance of the API
*
* @return instance of the HuskSync API
* @since 3.3
*/
@NotNull
public static HuskSyncAPI getInstance() {
if (instance == null) {
throw new NotRegisteredException();
}
return instance;
}
/**
* <b>(Internal use only)</b> - Unregister the API for this platform.
*/
@ApiStatus.Internal
public static void unregister() {
instance = null;
}
/**
* Get a {@link User} by their UUID
*
* @param uuid The UUID of the user to get
* @return A future containing the user, or an empty optional if the user doesn't exist
* @since 3.0
*/
@NotNull
public CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid) {
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
*
* @param username The username of the user to get
* @return A future containing the user, or an empty optional if the user doesn't exist
* @since 3.0
*/
@NotNull
public CompletableFuture<Optional<User>> getUser(@NotNull String username) {
return plugin.supplyAsync(() -> plugin.getDatabase().getUserByName(username));
}
/**
* Create a new data snapshot of an {@link OnlineUser}'s data.
*
* @param user The user to create the snapshot of
* @return The snapshot of the user's data
* @since 3.0
*/
@NotNull
public DataSnapshot.Packed createSnapshot(@NotNull OnlineUser user) {
return snapshotBuilder().saveCause(DataSnapshot.SaveCause.API).buildAndPack();
}
/**
* Get a {@link User}'s current data, as a {@link DataSnapshot.Unpacked}
* <p>
* If the user is online, this will create a new snapshot of their data with the {@code API} data save cause.
* </p>
* If the user is offline, this will return the latest snapshot of their data if that exists
* (an empty optional will be returned otherwise).
*
* @param user The user to get the data of
* @return A future containing the user's current data, or an empty optional if the user has no data
* @since 3.0
*/
public CompletableFuture<Optional<DataSnapshot.Unpacked>> getCurrentData(@NotNull User user) {
return plugin.getRedisManager()
.getOnlineUserData(UUID.randomUUID(), user, DataSnapshot.SaveCause.API)
.thenApply(data -> data.or(() -> plugin.getDatabase().getLatestSnapshot(user)))
.thenApply(data -> data.map(snapshot -> snapshot.unpack(plugin)));
}
/**
* Set a user's current data.
* <p>
* This will update the user's data in the database (creating a new snapshot) and send a data update,
* updating the user if they are online.
*
* @param user The user to set the data of
* @param data The data to set
* @since 3.0
*/
public void setCurrentData(@NotNull User user, @NotNull DataSnapshot data) {
plugin.runAsync(() -> {
final DataSnapshot.Packed packed = data instanceof DataSnapshot.Unpacked unpacked
? unpacked.pack(plugin) : (DataSnapshot.Packed) data;
addSnapshot(user, packed);
plugin.getRedisManager().sendUserDataUpdate(user, packed);
});
}
/**
* Edit a user's current data.
* <p>
* This will update the user's data in the database (creating a new snapshot) and send a data update,
* updating the user if they are online.
*
* @param user The user to edit the data of
* @param editor The editor function
* @since 3.0
*/
public void editCurrentData(@NotNull User user, @NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
getCurrentData(user).thenAccept(optional -> optional.ifPresent(data -> {
editor.accept(data);
data.setId(UUID.randomUUID());
setCurrentData(user, data);
}));
}
/**
* Get a list of all saved data snapshots for a user
*
* @param user The user to get the data snapshots of
* @return The user's data snapshots
* @since 3.0
*/
public CompletableFuture<List<DataSnapshot.Unpacked>> getSnapshots(@NotNull User user) {
return plugin.supplyAsync(
() -> plugin.getDatabase().getAllSnapshots(user).stream()
.map(snapshot -> snapshot.unpack(plugin))
.toList()
);
}
/**
* Get a specific data snapshot for a user
*
* @param user The user to get the data snapshot of
* @param versionId The version ID of the snapshot to get
* @return The user's data snapshot, or an empty optional if the user has no data
* @see #getSnapshots(User)
* @since 3.0
*/
public CompletableFuture<List<DataSnapshot.Unpacked>> getSnapshot(@NotNull User user, @NotNull UUID versionId) {
return plugin.supplyAsync(
() -> plugin.getDatabase().getSnapshot(user, versionId).stream()
.map(snapshot -> snapshot.unpack(plugin))
.toList()
);
}
/**
* Edit a data snapshot for a user
*
* @param user The user to edit the snapshot of
* @param versionId The version ID of the snapshot to edit
* @param editor The editor function
* @since 3.0
*/
public void editSnapshot(@NotNull User user, @NotNull UUID versionId,
@NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
plugin.runAsync(() -> plugin.getDatabase().getSnapshot(user, versionId).ifPresent(snapshot -> {
final DataSnapshot.Unpacked unpacked = snapshot.unpack(plugin);
editor.accept(unpacked);
plugin.getDatabase().updateSnapshot(user, unpacked.pack(plugin));
}));
}
/**
* Get the latest data snapshot for a user that has been saved in the database.
* <p>
* Not to be confused with {@link #getCurrentData(User)}, which will return the current data of a user
* if they are online (this method will only return their latest <i>saved</i> snapshot).
* </p>
*
* @param user The user to get the latest data snapshot of
* @return The user's latest data snapshot, or an empty optional if the user has no data
* @since 3.0
*/
public CompletableFuture<Optional<DataSnapshot.Unpacked>> getLatestSnapshot(@NotNull User user) {
return plugin.supplyAsync(
() -> plugin.getDatabase().getLatestSnapshot(user).map(snapshot -> snapshot.unpack(plugin))
);
}
/**
* Edit the latest data snapshot for a user
*
* @param user The user to edit the latest snapshot of
* @param editor The editor function
* @since 3.0
*/
public void editLatestSnapshot(@NotNull User user, @NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
plugin.runAsync(() -> plugin.getDatabase().getLatestSnapshot(user).ifPresent(snapshot -> {
final DataSnapshot.Unpacked unpacked = snapshot.unpack(plugin);
editor.accept(unpacked);
plugin.getDatabase().updateSnapshot(user, unpacked.pack(plugin));
}));
}
/**
* Adds a data snapshot to the database
*
* @param user The user to save the data for
* @param snapshot The snapshot to save
* @param callback A callback to run after the data has been saved (if the DataSaveEvent was not canceled)
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will be fired unless the
* {@link DataSnapshot.SaveCause#fireDataSaveEvent()} is {@code false}
* @since 3.3.2
*/
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot,
@Nullable BiConsumer<User, DataSnapshot.Packed> callback) {
plugin.runAsync(() -> plugin.getDataSyncer().saveData(
user,
snapshot instanceof DataSnapshot.Unpacked unpacked
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot,
callback
));
}
/**
* Adds a data snapshot to the database
*
* @param user The user to save the data for
* @param snapshot The snapshot to save
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will be fired unless the
* {@link DataSnapshot.SaveCause#fireDataSaveEvent()} is {@code false}
* @since 3.0
*/
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
this.addSnapshot(user, snapshot, null);
}
/**
* Update an <i>existing</i> data snapshot in the database.
* Not to be confused with {@link #addSnapshot(User, DataSnapshot)}, which will add a new snapshot if one
* snapshot doesn't exist.
*
* @param user The user to update the snapshot of
* @param snapshot The snapshot to update
* @since 3.0
*/
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
plugin.runAsync(() -> plugin.getDatabase().updateSnapshot(
user, snapshot instanceof DataSnapshot.Unpacked unpacked
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot
));
}
/**
* Pin a data snapshot, preventing it from being rotated
*
* @param user The user to pin the snapshot of
* @param snapshotVersion The version ID of the snapshot to pin
* @since 3.0
*/
public void pinSnapshot(@NotNull User user, @NotNull UUID snapshotVersion) {
plugin.runAsync(() -> plugin.getDatabase().pinSnapshot(user, snapshotVersion));
}
/**
* Unpin a data snapshot, allowing it to be rotated
*
* @param user The user to unpin the snapshot of
* @param snapshotVersion The version ID of the snapshot to unpin
* @since 3.0
*/
public void unpinSnapshot(@NotNull User user, @NotNull UUID snapshotVersion) {
plugin.runAsync(() -> plugin.getDatabase().unpinSnapshot(user, snapshotVersion));
}
/**
* Delete a data snapshot from the database
*
* @param user The user to delete the snapshot of
* @param versionId The version ID of the snapshot to delete
* @return A future which will complete with true if the snapshot was deleted, or false if it wasn't
* (e.g., if the snapshot didn't exist)
* @since 3.0
*/
public CompletableFuture<Boolean> deleteSnapshot(@NotNull User user, @NotNull UUID versionId) {
return plugin.supplyAsync(() -> plugin.getDatabase().deleteSnapshot(user, versionId));
}
/**
* Delete a data snapshot from the database
*
* @param user The user to delete the snapshot of
* @param snapshot The snapshot to delete
* @return A future which will complete with true if the snapshot was deleted, or false if it wasn't
* (e.g., if the snapshot hasn't been saved to the database yet)
* @since 3.0
*/
public CompletableFuture<Boolean> deleteSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
return deleteSnapshot(user, snapshot.getId());
}
/**
* Registers a new custom data type serializer.
* <p>
* This allows for custom {@link Data} types to be persisted in {@link DataSnapshot}s. To register
* a new data type, you must provide a {@link Serializer} for serializing and deserializing the data type
* and invoke this method.
* </p>
* You'll need to do this on every server you wish to sync data between. On servers where the registered
* data type is not present, the data will be ignored and snapshots created on that server will not
* contain the data.
*
* @param identifier The identifier of the data type to register.
* Create one using {@code Identifier.from(Key.of("your_plugin_name", "key"))}
* @param serializer An implementation of {@link Serializer} for serializing and deserializing the {@link Data}
* @param <T> A type extending {@link Data}; this will represent the data being held.
*/
public <T extends Data> void registerDataSerializer(@NotNull Identifier identifier,
@NotNull Serializer<T> serializer) {
plugin.registerSerializer(identifier, serializer);
}
/**
* Get a registered data serializer by its identifier
*
* @param identifier The identifier of the data type to get the serializer for
* @return The serializer for the given identifier, or an empty optional if the serializer isn't registered
* @since 3.5.4
*/
public Optional<Serializer<Data>> getDataSerializer(@NotNull Identifier identifier) {
return plugin.getSerializer(identifier);
}
/**
* Get a {@link DataSnapshot.Unpacked} from a {@link DataSnapshot.Packed}
*
* @param unpacked The unpacked snapshot
* @return The packed snapshot
* @since 3.0
*/
@NotNull
public DataSnapshot.Packed packSnapshot(@NotNull DataSnapshot.Unpacked unpacked) {
return unpacked.pack(plugin);
}
/**
* Get a {@link DataSnapshot.Unpacked} from a {@link DataSnapshot.Packed}
*
* @param packed The packed snapshot
* @return The unpacked snapshot
* @since 3.0
*/
@NotNull
public DataSnapshot.Unpacked unpackSnapshot(@NotNull DataSnapshot.Packed packed) {
return packed.unpack(plugin);
}
/**
* Unpack, edit, and repack a data snapshot.
* </p>
* This won't save the snapshot to the database; it'll just edit the data snapshot in place.
*
* @param packed The packed snapshot
* @param editor An editor function for editing the unpacked snapshot
* @return The edited packed snapshot
* @since 3.0
*/
@NotNull
public DataSnapshot.Packed editPackedSnapshot(@NotNull DataSnapshot.Packed packed,
@NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
final DataSnapshot.Unpacked unpacked = packed.unpack(plugin);
editor.accept(unpacked);
return unpacked.pack(plugin);
}
/**
* Get the estimated size of a {@link DataSnapshot} in bytes
*
* @param snapshot The snapshot to get the size of
* @return The size of the snapshot in bytes
* @since 3.0
*/
public int getSnapshotFileSize(@NotNull DataSnapshot snapshot) {
return (snapshot instanceof DataSnapshot.Packed packed)
? packed.getFileSize(plugin)
: ((DataSnapshot.Unpacked) snapshot).pack(plugin).getFileSize(plugin);
}
/**
* Get a builder for creating a new data snapshot
*
* @return The builder
* @since 3.0
*/
@NotNull
public DataSnapshot.Builder snapshotBuilder() {
return DataSnapshot.builder(plugin).saveCause(DataSnapshot.SaveCause.API);
}
/**
* Deserialize a JSON string to an {@link Adaptable}
*
* @param serialized The serialized JSON string
* @param type The type of the element
* @param <T> The type of the element
* @return The deserialized element
* @throws Serializer.DeserializationException If the element could not be deserialized
* @since 3.0
*/
@NotNull
public <T extends Adaptable> T deserializeData(@NotNull String serialized, Class<T> type)
throws Serializer.DeserializationException {
return plugin.getDataAdapter().fromJson(serialized, type);
}
/**
* Serialize an {@link Adaptable} to a JSON string
*
* @param element The element to serialize
* @param <T> The type of the element
* @return The serialized JSON string
* @throws Serializer.SerializationException If the element could not be serialized
* @since 3.0
*/
@NotNull
public <T extends Adaptable> String serializeData(@NotNull T element)
throws Serializer.SerializationException {
return plugin.getDataAdapter().toJson(element);
}
/**
* Set the {@link DataSyncer} to be used to sync data
*
* @param syncer The data syncer to use for synchronizing user data
* @since 3.1
*/
public void setDataSyncer(@NotNull DataSyncer syncer) {
plugin.setDataSyncer(syncer);
}
/**
* <b>(Internal use only)</b> - Get the plugin instance
*
* @return The plugin instance
*/
@ApiStatus.Internal
public HuskSync getPlugin() {
return plugin;
}
/**
* An exception indicating the plugin has been accessed before it has been registered.
*/
static final class NotRegisteredException extends IllegalStateException {
private static final String REASONS = """
This may be because:
1) HuskSync has failed to enable successfully
2) Your plugin isn't set to load after HuskSync has
(Check if it set as a (soft)depend in plugin.yml or to load: BEFORE in paper-plugin.yml?)
3) You are attempting to access HuskSync on plugin construction/before your plugin has enabled.""";
NotRegisteredException(@NotNull String reasons) {
super("Could not access the HuskSync API as it has not yet been registered. %s".formatted(reasons));
}
NotRegisteredException() {
this(REASONS);
}
}
}

View File

@@ -1,58 +0,0 @@
package net.william278.husksync.command;
import net.william278.husksync.HuskSync;
import net.william278.husksync.player.OnlineUser;
import org.jetbrains.annotations.NotNull;
/**
* Represents an abstract cross-platform representation for a plugin command
*/
public abstract class CommandBase {
/**
* The input string to match for this command
*/
public final String command;
/**
* The permission node required to use this command
*/
public final String permission;
/**
* Alias input strings for this command
*/
public final String[] aliases;
/**
* Instance of the implementing plugin
*/
public final HuskSync plugin;
public CommandBase(@NotNull String command, @NotNull Permission permission, @NotNull HuskSync implementor, String... aliases) {
this.command = command;
this.permission = permission.node;
this.plugin = implementor;
this.aliases = aliases;
}
/**
* Fires when the command is executed
*
* @param player {@link OnlineUser} executing the command
* @param args Command arguments
*/
public abstract void onExecute(@NotNull OnlineUser player, @NotNull String[] args);
/**
* Returns the localised description string of this command
*
* @return the command description
*/
public String getDescription() {
return plugin.getLocales().getRawLocale(command + "_command_description")
.orElse("A HuskHomes command");
}
}

View File

@@ -1,17 +0,0 @@
package net.william278.husksync.command;
import org.jetbrains.annotations.NotNull;
/**
* Interface providing console execution of commands
*/
public interface ConsoleExecutable {
/**
* What to do when console executes a command
*
* @param args command argument strings
*/
void onConsoleExecute(@NotNull String[] args);
}

View File

@@ -1,90 +1,101 @@
/*
* 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.command;
import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.UserData;
import net.william278.husksync.data.UserDataSnapshot;
import net.william278.husksync.editor.ItemEditorMenu;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
import net.william278.husksync.data.Data;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
import org.jetbrains.annotations.NotNull;
import java.text.DateFormat;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.Optional;
public class EnderChestCommand extends CommandBase implements TabCompletable {
public class EnderChestCommand extends ItemsCommand {
public EnderChestCommand(@NotNull HuskSync implementor) {
super("enderchest", Permission.COMMAND_ENDER_CHEST, implementor, "echest", "openechest");
public EnderChestCommand(@NotNull HuskSync plugin) {
super("enderchest", List.of("echest", "openechest"), DataSnapshot.SaveCause.ENDERCHEST_COMMAND, plugin);
}
@Override
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
if (args.length == 0 || args.length > 2) {
plugin.getLocales().getLocale("error_invalid_syntax", "/enderchest <player>")
.ifPresent(player::sendMessage);
protected void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
@NotNull User user, boolean allowEdit) {
final Optional<Data.Items.EnderChest> optionalEnderChest = snapshot.getEnderChest();
if (optionalEnderChest.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
return;
}
plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAccept(optionalUser ->
optionalUser.ifPresentOrElse(user -> {
if (args.length == 2) {
// View user data by specified UUID
try {
final UUID versionUuid = UUID.fromString(args[1]);
plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> data.ifPresentOrElse(
userData -> showEnderChestMenu(player, userData, user, false),
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage)));
} catch (IllegalArgumentException e) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/enderchest <player> [version_uuid]").ifPresent(player::sendMessage);
}
} else {
// View latest user data
plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse(
versionedUserData -> showEnderChestMenu(player, versionedUserData, user, true),
() -> plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(player::sendMessage)));
// Display opening message
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getName(),
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.getName())
.orElse(new MineDown(String.format("%s's Ender Chest", user.getName()))),
allowEdit,
enderChest.getSlotCount(),
(itemsOnClose) -> {
if (allowEdit && !enderChest.equals(itemsOnClose)) {
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
}
}, () -> plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(player::sendMessage)));
}
);
}
private void showEnderChestMenu(@NotNull OnlineUser player, @NotNull UserDataSnapshot userDataSnapshot,
@NotNull User dataOwner, final boolean allowEdit) {
CompletableFuture.runAsync(() -> {
final UserData data = userDataSnapshot.userData();
final ItemEditorMenu menu = ItemEditorMenu.createEnderChestMenu(data.getEnderChestData(),
dataOwner, player, plugin.getLocales(), allowEdit);
plugin.getLocales().getLocale("viewing_ender_chest_of", dataOwner.username,
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
.format(userDataSnapshot.versionTimestamp()))
.ifPresent(player::sendMessage);
plugin.getDataEditor().openItemEditorMenu(player, menu).thenAccept(enderChestDataOnClose -> {
if (!menu.canEdit) {
return;
}
final UserData updatedUserData = new UserData(data.getStatusData(), data.getInventoryData(),
enderChestDataOnClose, data.getPotionEffectsData(), data.getAdvancementData(),
data.getStatisticsData(), data.getLocationData(),
data.getPersistentDataContainerData(),
plugin.getMinecraftVersion().toString());
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.ENDERCHEST_COMMAND).join();
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
});
// Creates a new snapshot with the updated enderChest
@SuppressWarnings("DuplicatedCode")
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
return;
}
// Create and pack the snapshot with the updated enderChest
final DataSnapshot.Packed snapshot = latestData.get().copy();
boolean pin = plugin.getSettings().getSynchronization().doAutoPin(saveCause);
snapshot.edit(plugin, (data) -> {
data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items));
data.setSaveCause(saveCause);
data.setPinned(pin);
});
}
@Override
public List<String> onTabComplete(@NotNull String[] args) {
return plugin.getOnlineUsers().stream().map(user -> user.username)
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
.sorted().collect(Collectors.toList());
// Save data
final RedisManager redis = plugin.getRedisManager();
plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot));
redis.sendUserDataUpdate(user, data);
});
}
}

View File

@@ -1,141 +1,259 @@
/*
* 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.command;
import de.themoep.minedown.MineDown;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import de.themoep.minedown.adventure.MineDown;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.william278.desertwell.about.AboutMenu;
import net.william278.desertwell.util.UpdateChecker;
import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Locales;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.database.Database;
import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.util.UpdateChecker;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.util.LegacyConverter;
import net.william278.husksync.util.StatusLine;
import net.william278.uniform.BaseCommand;
import net.william278.uniform.CommandProvider;
import net.william278.uniform.Permission;
import net.william278.uniform.element.ArgumentElement;
import org.jetbrains.annotations.NotNull;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Level;
import java.util.stream.Collectors;
public class HuskSyncCommand extends CommandBase implements TabCompletable, ConsoleExecutable {
public class HuskSyncCommand extends PluginCommand {
private final String[] COMMAND_ARGUMENTS = {"update", "about", "reload", "migrate"};
private final UpdateChecker updateChecker;
private final AboutMenu aboutMenu;
public HuskSyncCommand(@NotNull HuskSync implementor) {
super("husksync", Permission.COMMAND_HUSKSYNC, implementor);
public HuskSyncCommand(@NotNull HuskSync 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)"),
AboutMenu.Credit.of("VinerDream").description("Code"))
.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
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
if (args.length < 1) {
displayPluginInformation(player);
return;
}
switch (args[0].toLowerCase()) {
case "update", "version" -> {
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_UPDATE.node)) {
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
return;
}
final UpdateChecker updateChecker = new UpdateChecker(plugin.getPluginVersion(), plugin.getLoggingAdapter());
updateChecker.fetchLatestVersion().thenAccept(latestVersion -> {
if (updateChecker.isUpdateAvailable(latestVersion)) {
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| A new update is available:](#00fb9a) [HuskSync " + latestVersion + "](#00fb9a bold)" +
"[•](white) [Currently running:](#00fb9a) [Version " + updateChecker.getCurrentVersion() + "](gray)" +
"[•](white) [Download links:](#00fb9a) [[⏩ Spigot]](gray open_url=https://www.spigotmc.org/resources/husksync.97144/updates) [•](#262626) [[⏩ Polymart]](gray open_url=https://polymart.org/resource/husksync.1634/updates) [•](#262626) [[⏩ Songoda]](gray open_url=https://songoda.com/marketplace/product/husksync-a-modern-cross-server-player-data-synchronization-system.758)"));
public void provide(@NotNull BaseCommand<?> command) {
command.setDefaultExecutor((ctx) -> about(command, ctx));
command.addSubCommand("about", (sub) -> sub.setDefaultExecutor((ctx) -> about(command, ctx)));
command.addSubCommand("status", needsOp("status"), status());
command.addSubCommand("dump", needsOp("dump"), dump());
command.addSubCommand("reload", needsOp("reload"), reload());
command.addSubCommand("update", needsOp("update"), update());
command.addSubCommand("forceupgrade", forceUpgrade());
command.addSubCommand("migrate", migrate());
}
private void about(@NotNull BaseCommand<?> c, @NotNull CommandContext<?> ctx) {
user(c, ctx).getAudience().sendMessage(aboutMenu.toComponent());
}
@NotNull
private CommandProvider status() {
return (sub) -> sub.setDefaultExecutor((ctx) -> {
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()
));
});
}
@NotNull
private CommandProvider dump() {
return (sub) -> {
sub.setDefaultExecutor((ctx) -> {
final CommandUser user = user(sub, ctx);
plugin.getLocales().getLocale("system_dump_confirm").ifPresent(user::sendMessage);
});
sub.addSubCommand("confirm", (con) -> con.setDefaultExecutor((ctx) -> {
final CommandUser user = user(sub, ctx);
plugin.getLocales().getLocale("system_dump_started").ifPresent(user::sendMessage);
plugin.runAsync(() -> {
final String url = plugin.createDump(user);
plugin.getLocales().getLocale("system_dump_ready").ifPresent(user::sendMessage);
user.sendMessage(Component.text(url).clickEvent(ClickEvent.openUrl(url))
.decorate(TextDecoration.UNDERLINED).color(NamedTextColor.GRAY));
});
}));
};
}
@NotNull
private CommandProvider reload() {
return (sub) -> sub.setDefaultExecutor((ctx) -> {
final CommandUser user = user(sub, ctx);
try {
plugin.loadSettings();
plugin.loadLocales();
plugin.loadServer();
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)"
));
plugin.log(Level.SEVERE, "Failed to reload the plugin", e);
}
});
}
@NotNull
private CommandProvider update() {
return (sub) -> sub.setDefaultExecutor((ctx) -> updateChecker.check().thenAccept(checked -> {
final CommandUser user = user(sub, ctx);
if (checked.isUpToDate()) {
plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString())
.ifPresent(user::sendMessage);
return;
}
plugin.getLocales().getLocale("update_available", checked.getLatestVersion().toString(),
plugin.getPluginVersion().toString()).ifPresent(user::sendMessage);
}));
}
@NotNull
private CommandProvider migrate() {
return (sub) -> {
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
sub.setDefaultExecutor((ctx) -> {
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"))
));
});
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 -> {
if (succeeded) {
plugin.log(Level.INFO, "Migration completed successfully!");
} else {
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date, running version " + updateChecker.getCurrentVersion() + "](#00fb9a)"));
plugin.log(Level.WARNING, "Migration failed!");
}
});
}
case "info", "about" -> displayPluginInformation(player);
case "reload" -> {
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_RELOAD.node)) {
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
return;
}
plugin.reload();
plugin.getLocales().getLocale("reload_complete").ifPresent(player::sendMessage);
}
case "migrate" ->
plugin.getLocales().getLocale("error_console_command_only").ifPresent(player::sendMessage);
default -> plugin.getLocales().getLocale("error_invalid_syntax",
"/husksync <update/about/reload>")
.ifPresent(player::sendMessage);
}
}, migrator()));
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(args);
}, migrator(), BaseCommand.greedyString("args")));
};
}
@Override
public void onConsoleExecute(@NotNull String[] args) {
if (args.length < 1) {
plugin.getLoggingAdapter().log(Level.INFO, "Console usage: \"husksync <update/about/reload/migrate>\"");
return;
}
switch (args[0].toLowerCase()) {
case "update", "version" ->
new UpdateChecker(plugin.getPluginVersion(), plugin.getLoggingAdapter()).logToConsole();
case "info", "about" ->
plugin.getLoggingAdapter().log(Level.INFO, new MineDown(plugin.getLocales().stripMineDown(
Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getPluginVersion().toString()))));
case "reload" -> {
plugin.reload();
plugin.getLoggingAdapter().log(Level.INFO, "Reloaded config & message files.");
}
case "migrate" -> {
if (args.length < 2) {
plugin.getLoggingAdapter().log(Level.INFO,
"Please choose a migrator, then run \"husksync migrate <migrator>\"");
logMigratorsList();
@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;
}
final Optional<Migrator> selectedMigrator = plugin.getAvailableMigrators().stream().filter(availableMigrator ->
availableMigrator.getIdentifier().equalsIgnoreCase(args[1])).findFirst();
selectedMigrator.ifPresentOrElse(migrator -> {
if (args.length < 3) {
plugin.getLoggingAdapter().log(Level.INFO, migrator.getHelpMenu());
return;
}
switch (args[2]) {
case "start" -> migrator.start().thenAccept(succeeded -> {
if (succeeded) {
plugin.getLoggingAdapter().log(Level.INFO, "Migration completed successfully!");
} else {
plugin.getLoggingAdapter().log(Level.WARNING, "Migration failed!");
}
});
case "set" -> migrator.handleConfigurationCommand(Arrays.copyOfRange(args, 3, args.length));
default -> plugin.getLoggingAdapter().log(Level.INFO,
"Invalid syntax. Console usage: \"husksync migrate " + args[1] + " <start/set>");
}
}, () -> {
plugin.getLoggingAdapter().log(Level.INFO,
"Please specify a valid migrator.\n" +
"If a migrator is not available, please verify that you meet the prerequisites to use it.");
logMigratorsList();
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);
if (migrator == null) {
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
}
default -> plugin.getLoggingAdapter().log(Level.INFO,
"Invalid syntax. Console usage: \"husksync <update/about/reload/migrate>\"");
}
return migrator;
}, (context, builder) -> {
for (Migrator material : plugin.getAvailableMigrators()) {
builder.suggest(material.getIdentifier());
}
return builder.buildFuture();
});
}
private void logMigratorsList() {
plugin.getLoggingAdapter().log(Level.INFO,
"List of available migrators:\nMigrator ID / Migrator Name:\n" +
plugin.getAvailableMigrators().stream()
.map(migrator -> migrator.getIdentifier() + " - " + migrator.getName())
.collect(Collectors.joining("\n")));
}
@Override
public List<String> onTabComplete(@NotNull String[] args) {
return Arrays.stream(COMMAND_ARGUMENTS)
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
.sorted().collect(Collectors.toList());
}
private void displayPluginInformation(@NotNull OnlineUser player) {
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_INFO.node)) {
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
return;
}
player.sendMessage(new MineDown(Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getPluginVersion().toString())));
}
}

View File

@@ -1,88 +1,102 @@
/*
* 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.command;
import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.UserData;
import net.william278.husksync.data.UserDataSnapshot;
import net.william278.husksync.editor.ItemEditorMenu;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
import net.william278.husksync.data.Data;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
import org.jetbrains.annotations.NotNull;
import java.text.DateFormat;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.Optional;
public class InventoryCommand extends CommandBase implements TabCompletable {
public class InventoryCommand extends ItemsCommand {
public InventoryCommand(@NotNull HuskSync implementor) {
super("inventory", Permission.COMMAND_INVENTORY, implementor, "invsee", "openinv");
public InventoryCommand(@NotNull HuskSync plugin) {
super("inventory", List.of("invsee", "openinv"), DataSnapshot.SaveCause.INVENTORY_COMMAND, plugin);
}
@Override
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
if (args.length == 0 || args.length > 2) {
plugin.getLocales().getLocale("error_invalid_syntax", "/inventory <player>")
.ifPresent(player::sendMessage);
protected void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
@NotNull User user, boolean allowEdit) {
final Optional<Data.Items.Inventory> optionalInventory = snapshot.getInventory();
if (optionalInventory.isEmpty()) {
viewer.sendMessage(new MineDown("what the FUCK is happening"));
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
return;
}
plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAccept(optionalUser ->
optionalUser.ifPresentOrElse(user -> {
if (args.length == 2) {
// View user data by specified UUID
try {
final UUID versionUuid = UUID.fromString(args[1]);
plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> data.ifPresentOrElse(
userData -> showInventoryMenu(player, userData, user, false),
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage)));
} catch (IllegalArgumentException e) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/inventory <player> [version_uuid]").ifPresent(player::sendMessage);
}
} else {
// View latest user data
plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse(
versionedUserData -> showInventoryMenu(player, versionedUserData, user, true),
() -> plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(player::sendMessage)));
// Display opening message
plugin.getLocales().getLocale("inventory_viewer_opened", user.getName(),
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.getName())
.orElse(new MineDown(String.format("%s's Inventory", user.getName()))),
allowEdit,
inventory.getSlotCount(),
(itemsOnClose) -> {
if (allowEdit && !inventory.equals(itemsOnClose)) {
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
}
}, () -> plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(player::sendMessage)));
}
);
}
private void showInventoryMenu(@NotNull OnlineUser player, @NotNull UserDataSnapshot userDataSnapshot,
@NotNull User dataOwner, boolean allowEdit) {
CompletableFuture.runAsync(() -> {
final UserData data = userDataSnapshot.userData();
final ItemEditorMenu menu = ItemEditorMenu.createInventoryMenu(data.getInventoryData(),
dataOwner, player, plugin.getLocales(), allowEdit);
plugin.getLocales().getLocale("viewing_inventory_of", dataOwner.username,
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
.format(userDataSnapshot.versionTimestamp()))
.ifPresent(player::sendMessage);
plugin.getDataEditor().openItemEditorMenu(player, menu).thenAccept(inventoryDataOnClose -> {
if (!menu.canEdit) {
return;
}
final UserData updatedUserData = new UserData(data.getStatusData(), inventoryDataOnClose,
data.getEnderChestData(), data.getPotionEffectsData(), data.getAdvancementData(),
data.getStatisticsData(), data.getLocationData(),
data.getPersistentDataContainerData(),
plugin.getMinecraftVersion().toString());
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND).join();
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
});
// Creates a new snapshot with the updated inventory
@SuppressWarnings("DuplicatedCode")
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
return;
}
// Create and pack the snapshot with the updated inventory
final DataSnapshot.Packed snapshot = latestData.get().copy();
boolean pin = plugin.getSettings().getSynchronization().doAutoPin(saveCause);
snapshot.edit(plugin, (data) -> {
data.getInventory().ifPresent(inventory -> inventory.setContents(items));
data.setSaveCause(saveCause);
data.setPinned(pin);
});
// Save data
final RedisManager redis = plugin.getRedisManager();
plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot));
redis.sendUserDataUpdate(user, data);
});
}
@Override
public List<String> onTabComplete(@NotNull String[] args) {
return plugin.getOnlineUsers().stream().map(user -> user.username)
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
.sorted().collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,117 @@
/*
* 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.command;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
import net.william278.uniform.BaseCommand;
import net.william278.uniform.Permission;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public abstract class ItemsCommand extends PluginCommand {
protected final DataSnapshot.SaveCause saveCause;
protected ItemsCommand(@NotNull String name, @NotNull List<String> aliases,
@NotNull DataSnapshot.SaveCause saveCause, @NotNull HuskSync plugin) {
super(name, aliases, Permission.Default.IF_OP, ExecutionScope.IN_GAME, plugin);
this.saveCause = saveCause;
}
@Override
public void provide(@NotNull BaseCommand<?> command) {
command.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
final CommandUser executor = user(command, ctx);
if (!(executor instanceof OnlineUser online)) {
plugin.getLocales().getLocale("error_in_game_command_only")
.ifPresent(executor::sendMessage);
return;
}
this.showSnapshotItems(online, user, version);
}, user("username"), versionUuid());
command.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final CommandUser executor = user(command, ctx);
if (!(executor instanceof OnlineUser online)) {
plugin.getLocales().getLocale("error_in_game_command_only")
.ifPresent(executor::sendMessage);
return;
}
this.showLatestItems(online, user);
}, user("username"));
}
// View (and edit) the latest user data
private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) {
plugin.getRedisManager().getOnlineUserData(user.getUuid(), user, saveCause).thenAccept(d -> d
.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);
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);
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
protected abstract void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
@NotNull User user, boolean allowEdit);
}

View File

@@ -1,96 +0,0 @@
package net.william278.husksync.command;
import org.jetbrains.annotations.NotNull;
/**
* Static plugin permission nodes required to execute commands
*/
public enum Permission {
/*
* /husksync command permissions
*/
/**
* Lets the user use the {@code /husksync} command (subcommand permissions required)
*/
COMMAND_HUSKSYNC("husksync.command.husksync", DefaultAccess.EVERYONE),
/**
* Lets the user view plugin info {@code /husksync info}
*/
COMMAND_HUSKSYNC_INFO("husksync.command.husksync.info", DefaultAccess.EVERYONE),
/**
* Lets the user reload the plugin {@code /husksync reload}
*/
COMMAND_HUSKSYNC_RELOAD("husksync.command.husksync.reload", DefaultAccess.OPERATORS),
/**
* Lets the user view the plugin version and check for updates {@code /husksync update}
*/
COMMAND_HUSKSYNC_UPDATE("husksync.command.husksync.update", DefaultAccess.OPERATORS),
/*
* /userdata command permissions
*/
/**
* Lets the user view user data {@code /userdata view/list (player) (version_uuid)}
*/
COMMAND_USER_DATA("husksync.command.userdata", DefaultAccess.OPERATORS),
/**
* Lets the user restore and delete user data {@code /userdata restore/delete (player) (version_uuid)}
*/
COMMAND_USER_DATA_MANAGE("husksync.command.userdata.manage", DefaultAccess.OPERATORS),
/*
* /inventory command permissions
*/
/**
* Lets the user use the {@code /inventory (player)} command and view offline players' inventories
*/
COMMAND_INVENTORY("husksync.command.inventory", DefaultAccess.OPERATORS),
/**
* Lets the user edit the contents of offline players' inventories
*/
COMMAND_INVENTORY_EDIT("husksync.command.inventory.edit", DefaultAccess.OPERATORS),
/*
* /enderchest command permissions
*/
/**
* Lets the user use the {@code /enderchest (player)} command and view offline players' ender chests
*/
COMMAND_ENDER_CHEST("husksync.command.enderchest", DefaultAccess.OPERATORS),
/**
* Lets the user edit the contents of offline players' ender chests
*/
COMMAND_ENDER_CHEST_EDIT("husksync.command.enderchest.edit", DefaultAccess.OPERATORS);
public final String node;
public final DefaultAccess defaultAccess;
Permission(@NotNull String node, @NotNull DefaultAccess defaultAccess) {
this.node = node;
this.defaultAccess = defaultAccess;
}
/**
* Identifies who gets what permissions by default
*/
public enum DefaultAccess {
/**
* Everyone gets this permission node by default
*/
EVERYONE,
/**
* Nobody gets this permission node by default
*/
NOBODY,
/**
* Server operators ({@code /op}) get this permission node by default
*/
OPERATORS
}
}

View File

@@ -0,0 +1,154 @@
/*
* 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.command;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.william278.husksync.HuskSync;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
import net.william278.uniform.BaseCommand;
import net.william278.uniform.Command;
import net.william278.uniform.Permission;
import net.william278.uniform.element.ArgumentElement;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.function.Function;
public abstract class PluginCommand extends Command {
protected final HuskSync plugin;
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;
}
private static String getDescription(@NotNull HuskSync plugin, @NotNull String name) {
return plugin.getLocales().getRawLocale("%s_command_description".formatted(name)).orElse("");
}
@NotNull
private static String createPermission(@NotNull String name, @NotNull String... sub) {
return "husksync.command." + name + (sub.length > 0 ? "." + String.join(".", sub) : "");
}
@NotNull
protected String getPermission(@NotNull String... sub) {
return createPermission(this.getName(), sub);
}
@NotNull
@SuppressWarnings("rawtypes")
protected CommandUser user(@NotNull BaseCommand base, @NotNull CommandContext context) {
return adapt(base.getUser(context.getSource()));
}
@NotNull
protected Permission needsOp(@NotNull String... nodes) {
return new Permission(getPermission(nodes), Permission.Default.IF_OP);
}
@NotNull
protected CommandUser adapt(net.william278.uniform.CommandUser user) {
return user.getUuid() == null ? plugin.getConsole() : plugin.getOnlineUser(user.getUuid()).orElseThrow();
}
@NotNull
@SuppressWarnings("SameParameterValue")
protected <S> ArgumentElement<S, OnlineUser> onlineUser(@NotNull String name) {
return new ArgumentElement<>(name, reader -> {
final String username = reader.readString();
return plugin.getOnlineUsers().stream()
.filter(user -> username.equals(user.getName()))
.findFirst().orElse(null);
}, (context, builder) -> {
plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getName()));
return builder.buildFuture();
});
}
@NotNull
protected <S> ArgumentElement<S, User> user(@NotNull String name) {
return new ArgumentElement<>(name, reader -> {
final String username = reader.readString();
return plugin.getDatabase().getUserByName(username).orElseThrow(
() -> CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader)
);
}, (context, builder) -> {
plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getName()));
return builder.buildFuture();
});
}
@NotNull
protected <S> ArgumentElement<S, UUID> versionUuid() {
return new ArgumentElement<>("version", reader -> {
try {
return UUID.fromString(reader.readString());
} catch (IllegalArgumentException e) {
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
}
}, (context, builder) -> {
try {
plugin.getDatabase().getAllSnapshots(context.getArgument("username", User.class))
.stream().sorted(Comparator.comparing(d -> d.getTimestamp().toEpochSecond()))
.forEach(id -> builder.suggest(id.getId().toString()));
return builder.buildFuture();
} catch (IllegalArgumentException e) {
return builder.buildFuture();
}
});
}
public enum Type {
HUSKSYNC_COMMAND(HuskSyncCommand::new),
USERDATA_COMMAND(UserDataCommand::new),
INVENTORY_COMMAND(InventoryCommand::new),
ENDER_CHEST_COMMAND(EnderChestCommand::new);
public final Function<HuskSync, PluginCommand> commandSupplier;
Type(@NotNull Function<HuskSync, PluginCommand> supplier) {
this.commandSupplier = supplier;
}
@NotNull
public PluginCommand supply(@NotNull HuskSync plugin) {
return commandSupplier.apply(plugin);
}
@NotNull
public static PluginCommand[] create(@NotNull HuskSync plugin) {
return Arrays.stream(values()).map(type -> type.supply(plugin))
.filter(command -> !plugin.getSettings().isCommandDisabled(command))
.toArray(PluginCommand[]::new);
}
}
}

View File

@@ -1,20 +0,0 @@
package net.william278.husksync.command;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* Interface providing tab completions for a command
*/
public interface TabCompletable {
/**
* What should be returned when the player or console attempts to TAB-complete a command
*
* @param args Current command arguments
* @return List of String arguments to offer TAB suggestions
*/
List<String> onTabComplete(@NotNull String[] args);
}

View File

@@ -1,239 +1,321 @@
/*
* 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.command;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
import net.william278.husksync.util.DataSnapshotList;
import net.william278.husksync.util.DataSnapshotOverview;
import net.william278.husksync.util.UserDataDumper;
import net.william278.uniform.BaseCommand;
import net.william278.uniform.CommandProvider;
import net.william278.uniform.Permission;
import net.william278.uniform.element.ArgumentElement;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.logging.Level;
public class UserDataCommand extends CommandBase implements TabCompletable {
public class UserDataCommand extends PluginCommand {
private final String[] COMMAND_ARGUMENTS = {"view", "list", "delete", "restore", "pin"};
public UserDataCommand(@NotNull HuskSync implementor) {
super("userdata", Permission.COMMAND_USER_DATA, implementor, "playerdata");
public UserDataCommand(@NotNull HuskSync plugin) {
super("userdata", List.of("playerdata"), Permission.Default.IF_OP, ExecutionScope.ALL, plugin);
}
@Override
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
if (args.length < 1) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata <view/list/delete/restore/pin> <username> [version_uuid]")
.ifPresent(player::sendMessage);
public void provide(@NotNull BaseCommand<?> command) {
command.addSubCommand("view", needsOp("view"), view());
command.addSubCommand("list", needsOp("list"), list());
command.addSubCommand("delete", needsOp("delete"), delete());
command.addSubCommand("save", needsOp("save"), save());
command.addSubCommand("restore", needsOp("restore"), restore());
command.addSubCommand("pin", needsOp("pin"), pin());
command.addSubCommand("dump", needsOp("dump"), dump());
}
// Show the latest snapshot
private void viewLatestSnapshot(@NotNull CommandUser executor, @NotNull User user) {
plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse(
data -> {
if (data.isInvalid()) {
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
.ifPresent(executor::sendMessage);
return;
}
DataSnapshotOverview.of(data.unpack(plugin), data.getFileSize(plugin), user, plugin)
.show(executor);
},
() -> plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(executor::sendMessage)
);
}
// Show the specified snapshot
private void viewSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
plugin.getDatabase().getSnapshot(user, version).ifPresentOrElse(
data -> {
if (data.isInvalid()) {
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
.ifPresent(executor::sendMessage);
return;
}
DataSnapshotOverview.of(data.unpack(plugin), data.getFileSize(plugin), user, plugin)
.show(executor);
},
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(executor::sendMessage)
);
}
// View a list of snapshots
private void listSnapshots(@NotNull CommandUser executor, @NotNull User user, int page) {
final List<DataSnapshot.Packed> dataList = plugin.getDatabase().getAllSnapshots(user);
if (dataList.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(executor::sendMessage);
return;
}
DataSnapshotList.create(dataList, user, plugin).displayPage(executor, page);
}
// Create and save a snapshot of a user's current data
private void createAndSaveSnapshot(@NotNull CommandUser executor, @NotNull OnlineUser onlineUser) {
plugin.getDataSyncer().saveCurrentUserData(onlineUser, DataSnapshot.SaveCause.SAVE_COMMAND);
plugin.getLocales().getLocale("data_saved", onlineUser.getName())
.ifPresent(executor::sendMessage);
}
// Delete a snapshot
private void deleteSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
if (!plugin.getDatabase().deleteSnapshot(user, version)) {
plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(executor::sendMessage);
return;
}
plugin.getRedisManager().clearUserData(user);
plugin.getLocales().getLocale("data_deleted",
version.toString().split("-")[0],
version.toString(),
user.getName(),
user.getUuid().toString())
.ifPresent(executor::sendMessage);
}
// Restore a snapshot
private void restoreSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, version);
if (optionalData.isEmpty()) {
plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(executor::sendMessage);
return;
}
switch (args[0].toLowerCase()) {
case "view" -> {
if (args.length < 2) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata view <username> [version_uuid]")
.ifPresent(player::sendMessage);
return;
}
final String username = args[1];
if (args.length >= 3) {
try {
final UUID versionUuid = UUID.fromString(args[2]);
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
optionalUser -> optionalUser.ifPresentOrElse(
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data ->
data.ifPresentOrElse(userData -> plugin.getDataEditor()
.displayDataOverview(player, userData, user),
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage))),
() -> plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(player::sendMessage))));
} catch (IllegalArgumentException e) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata view <username> [version_uuid]")
.ifPresent(player::sendMessage);
}
} else {
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
optionalUser -> optionalUser.ifPresentOrElse(
user -> plugin.getDatabase().getCurrentUserData(user).thenAccept(
latestData -> latestData.ifPresentOrElse(
userData -> plugin.getDataEditor()
.displayDataOverview(player, userData, user),
() -> plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(player::sendMessage))),
() -> plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(player::sendMessage))));
}
}
case "list" -> {
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
return;
}
if (args.length < 2) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata list <username>")
.ifPresent(player::sendMessage);
return;
}
final String username = args[1];
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
optionalUser -> optionalUser.ifPresentOrElse(
user -> plugin.getDatabase().getUserData(user).thenAccept(dataList -> {
if (dataList.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(player::sendMessage);
return;
}
plugin.getDataEditor().displayDataList(player, dataList, user);
}),
() -> plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(player::sendMessage))));
}
case "delete" -> {
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
return;
}
// Delete user data by specified UUID
if (args.length < 3) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata delete <username> <version_uuid>")
.ifPresent(player::sendMessage);
return;
}
final String username = args[1];
try {
final UUID versionUuid = UUID.fromString(args[2]);
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
optionalUser -> optionalUser.ifPresentOrElse(
user -> plugin.getDatabase().deleteUserData(user, versionUuid).thenAccept(deleted -> {
if (deleted) {
plugin.getLocales().getLocale("data_deleted",
versionUuid.toString().split("-")[0],
versionUuid.toString(),
user.username,
user.uuid.toString())
.ifPresent(player::sendMessage);
} else {
plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage);
}
}),
() -> plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(player::sendMessage))));
} catch (IllegalArgumentException e) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata delete <username> <version_uuid>")
.ifPresent(player::sendMessage);
}
}
case "restore" -> {
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
return;
}
// Get user data by specified uuid and username
if (args.length < 3) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata restore <username> <version_uuid>")
.ifPresent(player::sendMessage);
return;
}
final String username = args[1];
try {
final UUID versionUuid = UUID.fromString(args[2]);
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
optionalUser -> optionalUser.ifPresentOrElse(
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> {
if (data.isEmpty()) {
plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage);
return;
}
plugin.getDatabase().setUserData(user, data.get().userData(),
DataSaveCause.BACKUP_RESTORE);
plugin.getRedisManager().sendUserDataUpdate(user, data.get().userData()).join();
plugin.getLocales().getLocale("data_restored",
user.username,
user.uuid.toString(),
versionUuid.toString().split("-")[0],
versionUuid.toString())
.ifPresent(player::sendMessage);
}),
() -> plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(player::sendMessage))));
} catch (IllegalArgumentException e) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata restore <username> <version_uuid>")
.ifPresent(player::sendMessage);
}
}
case "pin" -> {
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
return;
}
if (args.length < 3) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata pin <username> <version_uuid>")
.ifPresent(player::sendMessage);
return;
}
final String username = args[1];
try {
final UUID versionUuid = UUID.fromString(args[2]);
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
optionalUser -> optionalUser.ifPresentOrElse(
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(
optionalUserData -> optionalUserData.ifPresentOrElse(userData -> {
if (userData.pinned()) {
plugin.getDatabase().unpinUserData(user, versionUuid).join();
plugin.getLocales().getLocale("data_unpinned",
versionUuid.toString().split("-")[0],
versionUuid.toString(),
user.username,
user.uuid.toString())
.ifPresent(player::sendMessage);
} else {
plugin.getDatabase().pinUserData(user, versionUuid).join();
plugin.getLocales().getLocale("data_pinned",
versionUuid.toString().split("-")[0],
versionUuid.toString(),
user.username,
user.uuid.toString())
.ifPresent(player::sendMessage);
}
}, () -> plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage))),
() -> plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(player::sendMessage))));
} catch (IllegalArgumentException e) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata pin <username> <version_uuid>")
.ifPresent(player::sendMessage);
}
}
// Restore users with a minimum of one health (prevent restoring players with <= 0 health)
final DataSnapshot.Packed data = optionalData.get().copy();
if (data.isInvalid()) {
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
.ifPresent(executor::sendMessage);
return;
}
data.edit(plugin, (unpacked -> {
unpacked.getHealth().ifPresent(status -> status.setHealth(Math.max(1, status.getHealth())));
unpacked.setSaveCause(DataSnapshot.SaveCause.BACKUP_RESTORE);
unpacked.setPinned(
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.BACKUP_RESTORE)
);
}));
// Save data
final RedisManager redis = plugin.getRedisManager();
plugin.getDataSyncer().saveData(user, data, (u, s) -> {
redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s));
redis.sendUserDataUpdate(u, s);
plugin.getLocales().getLocale("data_restored", u.getName(), u.getUuid().toString(),
s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage);
});
}
// Pin a snapshot
private void pinSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, version);
if (optionalData.isEmpty()) {
plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(executor::sendMessage);
return;
}
// Pin or unpin the data
final DataSnapshot.Packed data = optionalData.get();
if (data.isPinned()) {
plugin.getDatabase().unpinSnapshot(user, data.getId());
} else {
plugin.getDatabase().pinSnapshot(user, data.getId());
}
plugin.getLocales().getLocale(data.isPinned() ? "data_unpinned" : "data_pinned", data.getShortId(),
data.getId().toString(), user.getName(), user.getUuid().toString())
.ifPresent(executor::sendMessage);
}
// Lookup a snapshot by UUID and dump
private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version,
@NotNull DumpType type) {
final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, version);
if (data.isEmpty()) {
plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(executor::sendMessage);
return;
}
this.dumpSnapshot(executor, user, data.get(), type);
}
// Dump a snapshot
private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user,
@NotNull DataSnapshot.Packed userData, @NotNull DumpType type) {
final UserDataDumper dumper = UserDataDumper.create(userData, user, plugin);
try {
final String url = type == DumpType.WEB ? dumper.toWeb() : dumper.toFile();
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getName())
.ifPresent(executor::sendMessage);
executor.sendMessage(Component.text(url)
.clickEvent(type == DumpType.WEB ? ClickEvent.openUrl(url) : ClickEvent.copyToClipboard(url))
.decorate(TextDecoration.UNDERLINED).color(NamedTextColor.GRAY));
} catch (Throwable e) {
plugin.log(Level.SEVERE, "Failed to dump user data", e);
}
}
@Override
public List<String> onTabComplete(@NotNull String[] args) {
switch (args.length) {
case 0, 1 -> {
return Arrays.stream(COMMAND_ARGUMENTS)
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
.sorted().collect(Collectors.toList());
}
case 2 -> {
return plugin.getOnlineUsers().stream().map(user -> user.username)
.filter(argument -> argument.startsWith(args[1]))
.sorted().collect(Collectors.toList());
}
}
return Collections.emptyList();
@NotNull
private CommandProvider view() {
return (sub) -> sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
viewSnapshot(user(sub, ctx), user, version);
}, user("username"), versionUuid());
}
@NotNull
private CommandProvider list() {
return (sub) -> {
sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
listSnapshots(user(sub, ctx), user, 1);
}, user("username"));
sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final int page = ctx.getArgument("page", Integer.class);
listSnapshots(user(sub, ctx), user, page);
}, user("username"), BaseCommand.intNum("page", 1));
};
}
@NotNull
private CommandProvider delete() {
return (sub) -> sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
deleteSnapshot(user(sub, ctx), user, version);
}, user("username"), versionUuid());
}
@NotNull
private CommandProvider save() {
return (sub) -> sub.addSyntax((ctx) -> {
final OnlineUser user = ctx.getArgument("username", OnlineUser.class);
createAndSaveSnapshot(user(sub, ctx), user);
}, onlineUser("username"));
}
@NotNull
private CommandProvider restore() {
return (sub) -> sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
restoreSnapshot(user(sub, ctx), user, version);
}, user("username"), versionUuid());
}
@NotNull
private CommandProvider pin() {
return (sub) -> sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
pinSnapshot(user(sub, ctx), user, version);
}, user("username"), versionUuid());
}
@NotNull
private CommandProvider dump() {
return (sub) -> {
sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final CommandUser executor = user(sub, ctx);
plugin.getRedisManager()
.getOnlineUserData(UUID.randomUUID(), user, DataSnapshot.SaveCause.DUMP_COMMAND)
.thenAccept((data) -> data
.or(() -> plugin.getDatabase().getLatestSnapshot(user))
.ifPresentOrElse(
(s) -> dumpSnapshot(executor, user, s, DumpType.WEB),
() -> plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(executor::sendMessage)
));
}, user("username"));
sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
final DumpType type = ctx.getArgument("type", DumpType.class);
dumpSnapshot(user(sub, ctx), user, version, type);
}, user("username"), versionUuid(), dumpType());
};
}
private <S> ArgumentElement<S, DumpType> dumpType() {
return new ArgumentElement<>("type", reader -> {
final String type = reader.readString();
return switch (type.toLowerCase(Locale.ENGLISH)) {
case "web" -> DumpType.WEB;
case "file" -> DumpType.FILE;
default -> throw CommandSyntaxException.BUILT_IN_EXCEPTIONS
.dispatcherUnknownArgument().createWithContext(reader);
};
}, (context, builder) -> {
builder.suggest("web");
builder.suggest("file");
return builder.buildFuture();
});
}
enum DumpType {
WEB,
FILE
}
}

View File

@@ -0,0 +1,164 @@
/*
* 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.config;
import de.exlll.configlib.NameFormatters;
import de.exlll.configlib.YamlConfigurationProperties;
import de.exlll.configlib.YamlConfigurationStore;
import de.exlll.configlib.YamlConfigurations;
import net.william278.husksync.HuskSync;
import org.jetbrains.annotations.NotNull;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Level;
/**
* Interface for getting and setting data from plugin configuration files
*
* @since 1.0
*/
public interface ConfigProvider {
@NotNull
YamlConfigurationProperties.Builder<?> YAML_CONFIGURATION_PROPERTIES = YamlConfigurationProperties.newBuilder()
.charset(StandardCharsets.UTF_8)
.setNameFormatter(NameFormatters.LOWER_UNDERSCORE);
/**
* Get the plugin settings, read from the config file
*
* @return the plugin settings
* @since 1.0
*/
@NotNull
Settings getSettings();
/**
* Set the plugin settings
*
* @param settings The settings to set
* @since 1.0
*/
void setSettings(@NotNull Settings settings);
/**
* Load the plugin settings from the config file
*
* @since 1.0
*/
default void loadSettings() {
setSettings(YamlConfigurations.update(
getConfigDirectory().resolve("config.yml"),
Settings.class,
YAML_CONFIGURATION_PROPERTIES.header(Settings.CONFIG_HEADER).build()
));
}
/**
* Get the locales for the plugin
*
* @return the locales for the plugin
* @since 1.0
*/
@NotNull
Locales getLocales();
/**
* Set the locales for the plugin
*
* @param locales The locales to set
* @since 1.0
*/
void setLocales(@NotNull Locales locales);
/**
* Load the locales from the config file
*
* @since 1.0
*/
default void loadLocales() {
final YamlConfigurationStore<Locales> store = new YamlConfigurationStore<>(
Locales.class, YAML_CONFIGURATION_PROPERTIES.header(Locales.CONFIG_HEADER).build()
);
// Read existing locales if present
final Path path = getConfigDirectory().resolve(String.format("messages-%s.yml", getSettings().getLanguage()));
if (Files.exists(path)) {
setLocales(store.load(path));
return;
}
// Otherwise, save and read the default locales
try (InputStream input = getResource(String.format("locales/%s.yml", getSettings().getLanguage()))) {
final Locales locales = store.read(input);
store.save(locales, path);
setLocales(locales);
} catch (Throwable e) {
getPlugin().log(Level.SEVERE, "An error occurred loading the locales (invalid lang code?)", e);
}
}
@NotNull
String getServerName();
void setServerName(@NotNull Server server);
default void loadServer() {
setServerName(YamlConfigurations.update(
getConfigDirectory().resolve("server.yml"),
Server.class,
YAML_CONFIGURATION_PROPERTIES.header(Server.CONFIG_HEADER).build()
));
}
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
*
* @param name The name of the resource
* @return the resource, if found
* @since 1.0
*/
InputStream getResource(@NotNull String name);
/**
* Get the plugin config directory
*
* @return the plugin config directory
* @since 1.0
*/
@NotNull
Path getConfigDirectory();
@NotNull
HuskSync getPlugin();
}

View File

@@ -1,54 +1,75 @@
/*
* 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.config;
import de.themoep.minedown.MineDown;
import dev.dejvokep.boostedyaml.YamlDocument;
import com.google.common.collect.Maps;
import de.exlll.configlib.Configuration;
import de.themoep.minedown.adventure.MineDown;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import net.william278.paginedown.ListOptions;
import org.apache.commons.text.StringEscapeUtils;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Loaded locales used by the plugin to display various locales
* Plugin locale configuration
*
* @since 1.0
*/
@SuppressWarnings("FieldMayBeFinal")
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Locales {
public static final String PLUGIN_INFORMATION = """
[HuskSync](#00fb9a bold) [| Version %version%](#00fb9a)
[A modern, cross-server player data synchronization system](gray)
[• Author:](white) [William278](gray show_text=&7Click to visit website open_url=https://william278.net)
[• Contributors:](white) [HarvelsX](gray show_text=&7Code), [HookWoods](gray show_text=&7Code)
[• Translators:](white) [Namiu](gray show_text=&7\\(うにたろう\\) - Japanese, ja-jp), [anchelthe](gray show_text=&7Spanish, es-es), [Melonzio](gray show_text=&7Spanish, es-es), [Ceddix](gray show_text=&7German, de-de), [mateusneresrb](gray show_text=&7Brazilian Portuguese, pt-br], [小蔡](gray show_text=&7Traditional Chinese, zh-tw), [Ghost-chu](gray show_text=&7Simplified Chinese, zh-cn), [DJelly4K](gray show_text=&7Simplified Chinese, zh-cn), [Thourgard](gray show_text=&7Ukrainian, uk-ua), [xF3d3](gray show_text=&7Italian, it-it)
[• Documentation:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://william278.net/docs/husksync/Home/)
[• Bug reporting:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/issues)
[• Discord support:](white) [[Link]](#00fb9a show_text=&7Click to join open_url=https://discord.gg/tVYhJfyDWG)""";
static final String CONFIG_HEADER = """
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ HuskSync - Locales ┃
┃ Developed by William278
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┣╸ See plugin about menu for international locale credits
┣╸ Formatted in MineDown: https://github.com/Phoenix616/MineDown
┗╸ Translate HuskSync: https://william278.net/docs/husksync/translations""";
@NotNull
private final HashMap<String, String> rawLocales;
protected static final String DEFAULT_LOCALE = "en-gb";
private Locales(@NotNull YamlDocument localesConfig) {
this.rawLocales = new HashMap<>();
for (String localeId : localesConfig.getRoutesAsStrings(false)) {
rawLocales.put(localeId, localesConfig.getString(localeId));
}
}
// The raw set of locales loaded from yaml
Map<String, String> locales = Maps.newTreeMap();
/**
* Returns an un-formatted locale loaded from the locales file
* Returns a raw, unformatted locale loaded from the locale file
*
* @param localeId String identifier of the locale, corresponding to a key in the file
* @return An {@link Optional} containing the locale corresponding to the id, if it exists
*/
public Optional<String> getRawLocale(@NotNull String localeId) {
if (rawLocales.containsKey(localeId)) {
return Optional.of(rawLocales.get(localeId).replaceAll(Pattern.quote("\\n"), "\n"));
}
return Optional.empty();
return Optional.ofNullable(locales.get(localeId)).map(StringEscapeUtils::unescapeJava);
}
/**
* Returns an un-formatted locale loaded from the locales file, with replacements applied
* Returns a raw, un-formatted locale loaded from the locales file, with replacements applied
* <p>
* Note that replacements will not be MineDown-escaped; use {@link #escapeText(String)} to escape replacements
*
* @param localeId String identifier of the locale, corresponding to a key in the file
* @param replacements Ordered array of replacement strings to fill in placeholders with
@@ -65,18 +86,32 @@ public class Locales {
* @return An {@link Optional} containing the formatted locale corresponding to the id, if it exists
*/
public Optional<MineDown> getLocale(@NotNull String localeId) {
return getRawLocale(localeId).map(MineDown::new);
return getRawLocale(localeId).map(this::format);
}
/**
* Returns a MineDown-formatted locale from the locales file, with replacements applied
* <p>
* Note that replacements will be MineDown-escaped before application
*
* @param localeId String identifier of the locale, corresponding to a key in the file
* @param replacements Ordered array of replacement strings to fill in placeholders with
* @return An {@link Optional} containing the replacement-applied, formatted locale corresponding to the id, if it exists
*/
public Optional<MineDown> getLocale(@NotNull String localeId, @NotNull String... replacements) {
return getRawLocale(localeId, replacements).map(MineDown::new);
return getRawLocale(localeId, Arrays.stream(replacements).map(Locales::escapeText)
.toArray(String[]::new)).map(this::format);
}
/**
* Returns a MineDown-formatted string
*
* @param text The text to format
* @return A {@link MineDown} object containing the formatted text
*/
@NotNull
public MineDown format(@NotNull String text) {
return new MineDown(text);
}
/**
@@ -86,54 +121,95 @@ public class Locales {
* @param replacements Ordered array of replacement strings to fill in placeholders with
* @return the raw locale, with inserted placeholders
*/
@NotNull
private String applyReplacements(@NotNull String rawLocale, @NotNull String... replacements) {
int replacementIndexer = 1;
for (String replacement : replacements) {
String replacementString = "%" + replacementIndexer + "%";
rawLocale = rawLocale.replace(replacementString, replacement);
replacementIndexer = replacementIndexer + 1;
replacementIndexer += 1;
}
return rawLocale;
}
/**
* Load the locales from a BoostedYaml {@link YamlDocument} locales file
* Escape a string from {@link MineDown} formatting for use in a MineDown-formatted locale
*
* @param localesConfig The loaded {@link YamlDocument} locales.yml file
* @return the loaded {@link Locales}
* @param string The string to escape
* @return The escaped string
*/
public static Locales load(@NotNull YamlDocument localesConfig) {
return new Locales(localesConfig);
@NotNull
public static String escapeText(@NotNull String string) {
final StringBuilder value = new StringBuilder();
for (int i = 0; i < string.length(); ++i) {
char c = string.charAt(i);
boolean isEscape = c == '\\';
boolean isColorCode = i + 1 < string.length() && (c == 167 || c == '&');
boolean isEvent = c == '[' || c == ']' || c == '(' || c == ')';
if (isEscape || isColorCode || isEvent) {
value.append('\\');
}
value.append(c);
}
return value.toString();
}
/**
* Strips a string of basic MineDown formatting, used for displaying plugin info to console
* Returns the base list options to use for a paginated chat list
*
* @param string The string to strip
* @return The MineDown-stripped string
* @param itemsPerPage The number of items to display per page
* @return The list options
*/
public String stripMineDown(@NotNull String string) {
final String[] in = string.split("\n");
final StringBuilder out = new StringBuilder();
String regex = "[^\\[\\]() ]*\\[([^()]+)]\\([^()]+open_url=(\\S+).*\\)";
for (int i = 0; i < in.length; i++) {
Pattern pattern = Pattern.compile(regex);
Matcher m = pattern.matcher(in[i]);
if (m.find()) {
out.append(in[i].replace(m.group(0), ""));
out.append(m.group(2));
} else {
out.append(in[i]);
}
if (i + 1 != in.length) {
out.append("\n");
}
}
return out.toString();
@NotNull
public ListOptions.Builder getBaseChatList(int itemsPerPage) {
return new ListOptions.Builder()
.setFooterFormat(getRawLocale("list_footer",
"%previous_page_button%", "%current_page%",
"%total_pages%", "%next_page_button%", "%page_jumpers%").orElse(""))
.setNextButtonFormat(getRawLocale("list_next_page_button",
"%next_page_index%", "%command%").orElse(""))
.setPreviousButtonFormat(getRawLocale("list_previous_page_button",
"%previous_page_index%", "%command%").orElse(""))
.setPageJumpersFormat(getRawLocale("list_page_jumpers",
"%page_jump_buttons%").orElse(""))
.setPageJumperPageFormat(getRawLocale("list_page_jumper_button",
"%target_page_index%", "%command%").orElse(""))
.setPageJumperCurrentPageFormat(getRawLocale("list_page_jumper_current_page",
"%current_page%").orElse(""))
.setPageJumperPageSeparator(getRawLocale("list_page_jumper_separator").orElse(""))
.setPageJumperGroupSeparator(getRawLocale("list_page_jumper_group_separator").orElse(""))
.setItemsPerPage(itemsPerPage)
.setEscapeItemsMineDown(false)
.setSpaceAfterHeader(false)
.setSpaceBeforeFooter(false);
}
/**
* Determines the slot a system notification should be displayed in
*/
public enum NotificationSlot {
/**
* 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
*/
NONE
}
}

View File

@@ -0,0 +1,70 @@
/*
* 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.config;
import de.exlll.configlib.Configuration;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.nio.file.Path;
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Server {
static final String CONFIG_HEADER = """
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ HuskSync - Server ID ┃
┃ Developed by William278 ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┣╸ This file should contain the ID of this server as defined in your proxy config.
┗╸ If you join it using /server alpha, then set it to 'alpha' (case-sensitive)""";
private String name = getDefault();
@NotNull
public static Server of(@NotNull String name) {
return new Server(name);
}
/**
* Find a sensible default name for the server name property
*/
@NotNull
private static String getDefault() {
final String serverFolder = System.getProperty("user.dir");
return serverFolder == null ? "server" : Path.of(serverFolder).getFileName().toString().trim();
}
@Override
public boolean equals(@NotNull Object other) {
// If the name of this server matches another, the servers are the same.
if (other instanceof Server server) {
return server.getName().equalsIgnoreCase(this.getName());
}
return super.equals(other);
}
}

View File

@@ -1,273 +1,348 @@
/*
* 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.config;
import dev.dejvokep.boostedyaml.YamlDocument;
import com.google.common.collect.Lists;
import de.exlll.configlib.Comment;
import de.exlll.configlib.Configuration;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.william278.husksync.command.PluginCommand;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.data.Identifier;
import net.william278.husksync.database.Database;
import net.william278.husksync.listener.EventListener;
import net.william278.husksync.sync.DataSyncer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Settings used for the plugin, as read from the config file
* Plugin settings, read from config.yml
*/
@SuppressWarnings("FieldMayBeFinal")
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Settings {
/**
* Map of {@link ConfigOption}s read from the config file
*/
private final HashMap<ConfigOption, Object> configOptions;
protected static final String CONFIG_HEADER = """
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ HuskSync Config
┃ Developed by William278 ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┣╸ Information: https://william278.net/project/husksync
┣╸ Config Help: https://william278.net/docs/husksync/config-file/
┗╸ Documentation: https://william278.net/docs/husksync""";
// Load the settings from the document
private Settings(@NotNull YamlDocument config) {
this.configOptions = new HashMap<>();
Arrays.stream(ConfigOption.values()).forEach(configOption -> configOptions
.put(configOption, switch (configOption.optionType) {
case BOOLEAN -> configOption.getBooleanValue(config);
case STRING -> configOption.getStringValue(config);
case DOUBLE -> configOption.getDoubleValue(config);
case FLOAT -> configOption.getFloatValue(config);
case INTEGER -> configOption.getIntValue(config);
case STRING_LIST -> configOption.getStringListValue(config);
}));
}
// Top-level settings
@Comment({"Locale of the default language file to use.", "Docs: https://william278.net/docs/husksync/translations"})
private String language = Locales.DEFAULT_LOCALE;
/**
* Get the value of the specified {@link ConfigOption}
*
* @param option the {@link ConfigOption} to check
* @return the value of the {@link ConfigOption} as a boolean
* @throws ClassCastException if the option is not a boolean
*/
public boolean getBooleanValue(@NotNull ConfigOption option) throws ClassCastException {
return (Boolean) configOptions.get(option);
}
@Comment("Whether to automatically check for plugin updates on startup")
private boolean checkForUpdates = true;
/**
* Get the value of the specified {@link ConfigOption}
*
* @param option the {@link ConfigOption} to check
* @return the value of the {@link ConfigOption} as a string
* @throws ClassCastException if the option is not a string
*/
public String getStringValue(@NotNull ConfigOption option) throws ClassCastException {
return (String) configOptions.get(option);
}
@Comment("Specify a common ID for grouping servers running HuskSync. "
+ "Don't modify this unless you know what you're doing!")
private String clusterId = "";
/**
* Get the value of the specified {@link ConfigOption}
*
* @param option the {@link ConfigOption} to check
* @return the value of the {@link ConfigOption} as a double
* @throws ClassCastException if the option is not a double
*/
public double getDoubleValue(@NotNull ConfigOption option) throws ClassCastException {
return (Double) configOptions.get(option);
}
@Comment("Enable development debug logging")
private boolean debugLogging = false;
/**
* Get the value of the specified {@link ConfigOption}
*
* @param option the {@link ConfigOption} to check
* @return the value of the {@link ConfigOption} as a float
* @throws ClassCastException if the option is not a float
*/
public double getFloatValue(@NotNull ConfigOption option) throws ClassCastException {
return (Float) configOptions.get(option);
}
@Comment({"Whether to enable the Player Analytics hook.", "Docs: https://william278.net/docs/husksync/plan-hook"})
private boolean enablePlanHook = true;
/**
* Get the value of the specified {@link ConfigOption}
*
* @param option the {@link ConfigOption} to check
* @return the value of the {@link ConfigOption} as an integer
* @throws ClassCastException if the option is not an integer
*/
public int getIntegerValue(@NotNull ConfigOption option) throws ClassCastException {
return (Integer) configOptions.get(option);
}
@Comment("Whether to cancel game event packets directly when handling locked players if ProtocolLib or PacketEvents is installed")
private boolean cancelPackets = true;
/**
* Get the value of the specified {@link ConfigOption}
*
* @param option the {@link ConfigOption} to check
* @return the value of the {@link ConfigOption} as a string {@link List}
* @throws ClassCastException if the option is not a string list
*/
@SuppressWarnings("unchecked")
public List<String> getStringListValue(@NotNull ConfigOption option) throws ClassCastException {
return (List<String>) configOptions.get(option);
}
@Comment("Add HuskSync commands to this list to prevent them from being registered (e.g. ['userdata'])")
@Getter(AccessLevel.NONE)
private List<String> disabledCommands = Lists.newArrayList();
// Database settings
@Comment("Database settings")
private DatabaseSettings database = new DatabaseSettings();
/**
* Load the settings from a BoostedYaml {@link YamlDocument} config file
*
* @param config The loaded {@link YamlDocument} config.yml file
* @return the loaded {@link Settings}
*/
public static Settings load(@NotNull YamlDocument config) {
return new Settings(config);
}
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class DatabaseSettings {
/**
* Represents an option stored by a path in config.yml
*/
public enum ConfigOption {
LANGUAGE("language", OptionType.STRING, "en-gb"),
CHECK_FOR_UPDATES("check_for_updates", OptionType.BOOLEAN, true),
@Comment("Type of database to use (MYSQL, MARIADB, POSTGRES, MONGO)")
private Database.Type type = Database.Type.MYSQL;
CLUSTER_ID("cluster_id", OptionType.STRING, ""),
DEBUG_LOGGING("debug_logging", OptionType.BOOLEAN, false),
@Comment("Specify credentials here for your MYSQL, MARIADB, POSTGRES OR MONGO database")
private DatabaseCredentials credentials = new DatabaseCredentials();
DATABASE_HOST("database.credentials.host", OptionType.STRING, "localhost"),
DATABASE_PORT("database.credentials.port", OptionType.INTEGER, 3306),
DATABASE_NAME("database.credentials.database", OptionType.STRING, "HuskSync"),
DATABASE_USERNAME("database.credentials.username", OptionType.STRING, "root"),
DATABASE_PASSWORD("database.credentials.password", OptionType.STRING, "pa55w0rd"),
DATABASE_CONNECTION_PARAMS("database.credentials.params", OptionType.STRING, "?autoReconnect=true&useSSL=false"),
DATABASE_CONNECTION_POOL_MAX_SIZE("database.connection_pool.maximum_pool_size", OptionType.INTEGER, 10),
DATABASE_CONNECTION_POOL_MIN_IDLE("database.connection_pool.minimum_idle", OptionType.INTEGER, 10),
DATABASE_CONNECTION_POOL_MAX_LIFETIME("database.connection_pool.maximum_lifetime", OptionType.INTEGER, 1800000),
DATABASE_CONNECTION_POOL_KEEPALIVE("database.connection_pool.keepalive_time", OptionType.INTEGER, 0),
DATABASE_CONNECTION_POOL_TIMEOUT("database.connection_pool.connection_timeout", OptionType.INTEGER, 5000),
DATABASE_USERS_TABLE_NAME("database.table_names.users_table", OptionType.STRING, "husksync_users"),
DATABASE_USER_DATA_TABLE_NAME("database.table_names.user_data_table", OptionType.STRING, "husksync_user_data"),
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class DatabaseCredentials {
private String host = "localhost";
private int port = 3306;
private String database = "HuskSync";
private String username = "root";
private String password = "pa55w0rd";
@Comment("Only change this if you're using MARIADB or POSTGRES")
private String parameters = String.join("&",
"?autoReconnect=true", "useSSL=false",
"useUnicode=true", "characterEncoding=UTF-8");
}
REDIS_HOST("redis.credentials.host", OptionType.STRING, "localhost"),
REDIS_PORT("redis.credentials.port", OptionType.INTEGER, 6379),
REDIS_PASSWORD("redis.credentials.password", OptionType.STRING, ""),
REDIS_USE_SSL("redis.use_ssl", OptionType.BOOLEAN, false),
@Comment("MYSQL, MARIADB, POSTGRES database Hikari connection pool properties. Don't modify this unless you know what you're doing!")
private PoolSettings connectionPool = new PoolSettings();
SYNCHRONIZATION_MAX_USER_DATA_SNAPSHOTS("synchronization.max_user_data_snapshots", OptionType.INTEGER, 5),
SYNCHRONIZATION_SAVE_ON_WORLD_SAVE("synchronization.save_on_world_save", OptionType.BOOLEAN, true),
SYNCHRONIZATION_COMPRESS_DATA("synchronization.compress_data", OptionType.BOOLEAN, true),
SYNCHRONIZATION_NETWORK_LATENCY_MILLISECONDS("synchronization.network_latency_milliseconds", OptionType.INTEGER, 500),
SYNCHRONIZATION_SYNC_INVENTORIES("synchronization.features.inventories", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_ENDER_CHESTS("synchronization.features.ender_chests", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_HEALTH("synchronization.features.health", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_MAX_HEALTH("synchronization.features.max_health", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_HUNGER("synchronization.features.hunger", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_EXPERIENCE("synchronization.features.experience", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_POTION_EFFECTS("synchronization.features.potion_effects", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_ADVANCEMENTS("synchronization.features.advancements", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_GAME_MODE("synchronization.features.game_mode", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_STATISTICS("synchronization.features.statistics", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER("synchronization.features.persistent_data_container", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_LOCATION("synchronization.features.location", OptionType.BOOLEAN, true);
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class PoolSettings {
private int maximumPoolSize = 10;
private int minimumIdle = 10;
private long maximumLifetime = 1800000;
private long keepaliveTime = 0;
private long connectionTimeout = 5000;
}
@Comment("Advanced MongoDB settings. Don't modify unless you know what you're doing!")
private MongoSettings mongoSettings = new MongoSettings();
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class MongoSettings {
private boolean usingAtlas = false;
private String parameters = String.join("&",
"?retryWrites=true", "w=majority",
"authSource=HuskSync");
}
@Comment("Names of tables to use on your database. Don't modify this unless you know what you're doing!")
@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;
/**
* The path in the config.yml file to the value
*/
@NotNull
public final String configPath;
/**
* The {@link OptionType} of this option
*/
@NotNull
public final OptionType optionType;
/**
* The default value of this option if not set in config
*/
@Nullable
private final Object defaultValue;
ConfigOption(@NotNull String configPath, @NotNull OptionType optionType, @Nullable Object defaultValue) {
this.configPath = configPath;
this.optionType = optionType;
this.defaultValue = defaultValue;
}
ConfigOption(@NotNull String configPath, @NotNull OptionType optionType) {
this.configPath = configPath;
this.optionType = optionType;
this.defaultValue = null;
}
/**
* Get the value at the path specified (or return default if set), as a string
*
* @param config The {@link YamlDocument} config file
* @return the value defined in the config, as a string
*/
public String getStringValue(@NotNull YamlDocument config) {
return defaultValue != null
? config.getString(configPath, (String) defaultValue)
: config.getString(configPath);
}
/**
* Get the value at the path specified (or return default if set), as a boolean
*
* @param config The {@link YamlDocument} config file
* @return the value defined in the config, as a boolean
*/
public boolean getBooleanValue(@NotNull YamlDocument config) {
return defaultValue != null
? config.getBoolean(configPath, (Boolean) defaultValue)
: config.getBoolean(configPath);
}
/**
* Get the value at the path specified (or return default if set), as a double
*
* @param config The {@link YamlDocument} config file
* @return the value defined in the config, as a double
*/
public double getDoubleValue(@NotNull YamlDocument config) {
return defaultValue != null
? config.getDouble(configPath, (Double) defaultValue)
: config.getDouble(configPath);
}
/**
* Get the value at the path specified (or return default if set), as a float
*
* @param config The {@link YamlDocument} config file
* @return the value defined in the config, as a float
*/
public float getFloatValue(@NotNull YamlDocument config) {
return defaultValue != null
? config.getFloat(configPath, (Float) defaultValue)
: config.getFloat(configPath);
}
/**
* Get the value at the path specified (or return default if set), as an int
*
* @param config The {@link YamlDocument} config file
* @return the value defined in the config, as an int
*/
public int getIntValue(@NotNull YamlDocument config) {
return defaultValue != null
? config.getInt(configPath, (Integer) defaultValue)
: config.getInt(configPath);
}
/**
* Get the value at the path specified (or return default if set), as a string {@link List}
*
* @param config The {@link YamlDocument} config file
* @return the value defined in the config, as a string {@link List}
*/
public List<String> getStringListValue(@NotNull YamlDocument config) {
return config.getStringList(configPath, new ArrayList<>());
}
/**
* Represents the type of the object
*/
public enum OptionType {
BOOLEAN,
STRING,
DOUBLE,
FLOAT,
INTEGER,
STRING_LIST
public String getTableName(@NotNull Database.TableName tableName) {
return tableNames.getOrDefault(tableName.name().toLowerCase(Locale.ENGLISH), tableName.getDefaultName());
}
}
// 𝓡𝓮𝓭𝓲𝓼 settings
@Comment("Redis settings")
private RedisSettings redis = new RedisSettings();
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class RedisSettings {
@Comment("Specify the credentials of your Redis server here. Set \"password\" to '' if you don't have one")
private RedisCredentials credentials = new RedisCredentials();
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class RedisCredentials {
private String host = "localhost";
private int port = 6379;
private String password = "";
private boolean useSsl = false;
}
@Comment("Options for if you're using Redis sentinel. Don't modify this unless you know what you're doing!")
private RedisSentinel sentinel = new RedisSentinel();
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class RedisSentinel {
@Comment("The master set name for the Redis sentinel.")
private String master = "";
@Comment("List of host:port pairs")
private List<String> nodes = Lists.newArrayList();
private String password = "";
}
}
// Synchronization settings
@Comment("Data syncing settings")
private SynchronizationSettings synchronization = new SynchronizationSettings();
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class SynchronizationSettings {
@Comment({"The data synchronization mode to use (LOCKSTEP or DELAY). LOCKSTEP is recommended for most networks.",
"Docs: https://william278.net/docs/husksync/sync-modes"})
private DataSyncer.Mode mode = DataSyncer.Mode.LOCKSTEP;
@Comment("The number of data snapshot backups that should be kept at once per user")
private int maxUserDataSnapshots = 16;
@Comment("Number of hours between new snapshots being saved as backups (Use \"0\" to backup all snapshots)")
private int snapshotBackupFrequency = 4;
@Comment({"List of save cause IDs for which a snapshot will be automatically pinned (so it won't be rotated).",
"Docs: https://william278.net/docs/husksync/data-rotation#save-causes"})
@Getter(AccessLevel.NONE)
private List<String> autoPinnedSaveCauses = List.of(
DataSnapshot.SaveCause.INVENTORY_COMMAND.name(),
DataSnapshot.SaveCause.ENDERCHEST_COMMAND.name(),
DataSnapshot.SaveCause.BACKUP_RESTORE.name(),
DataSnapshot.SaveCause.LEGACY_MIGRATION.name(),
DataSnapshot.SaveCause.MPDB_MIGRATION.name()
);
@Comment("Whether to create a snapshot for users on a world when the server saves that world")
private boolean saveOnWorldSave = true;
@Comment("Configuration for how and when to sync player data when they die")
private SaveOnDeathSettings saveOnDeath = new SaveOnDeathSettings();
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class SaveOnDeathSettings {
@Comment("Whether to create a snapshot for users when they die (containing their death drops)")
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.")
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?")
private boolean saveEmptyItems = true;
@Comment("Whether dead players who log out and log in to a different server should have their items saved.")
private boolean syncDeadPlayersChangingServer = true;
/**
* Represents the mode of saving items on death
*/
public enum DeathItemsMode {
DROPS,
ITEMS_TO_KEEP
}
}
@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 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).")
private int networkLatencyMilliseconds = 500;
@Comment({"Which data types to synchronize.", "Docs: https://william278.net/docs/husksync/sync-features"})
@Getter(AccessLevel.NONE)
private Map<String, Boolean> features = Identifier.getConfigMap();
@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("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)
private Map<String, String> eventPriorities = EventListener.ListenerType.getDefaults();
public boolean doAutoPin(@NotNull DataSnapshot.SaveCause cause) {
return autoPinnedSaveCauses.contains(cause.name());
}
public boolean isFeatureEnabled(@NotNull Identifier id) {
return id.isCustom() || features.getOrDefault(id.getKeyValue(), id.isEnabledByDefault());
}
@NotNull
public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) {
try {
return EventListener.Priority.valueOf(eventPriorities.get(type.name().toLowerCase(Locale.ENGLISH)));
} catch (IllegalArgumentException e) {
return EventListener.Priority.NORMAL;
}
}
}
public boolean isCommandDisabled(@NotNull PluginCommand command) {
return disabledCommands.stream().map(c -> c.startsWith("/") ? c.substring(1) : c)
.anyMatch(c -> c.equalsIgnoreCase(command.getName()) || command.getAliases().contains(c));
}
}

View File

@@ -1,34 +0,0 @@
package net.william278.husksync.data;
import com.google.gson.annotations.SerializedName;
import org.jetbrains.annotations.NotNull;
import java.util.Date;
import java.util.Map;
/**
* A mapped piece of advancement data
*/
public class AdvancementData {
/**
* The advancement namespaced key
*/
@SerializedName("key")
public String key;
/**
* A map of completed advancement criteria to when it was completed
*/
@SerializedName("completed_criteria")
public Map<String, Date> completedCriteria;
public AdvancementData(@NotNull String key, @NotNull Map<String, Date> awardedCriteria) {
this.key = key;
this.completedCriteria = awardedCriteria;
}
@SuppressWarnings("unused")
protected AdvancementData() {
}
}

View File

@@ -1,27 +0,0 @@
package net.william278.husksync.data;
import org.jetbrains.annotations.NotNull;
import org.xerial.snappy.Snappy;
import java.io.IOException;
public class CompressedDataAdapter extends JsonDataAdapter {
@Override
public byte[] toBytes(@NotNull UserData data) throws DataAdaptionException {
try {
return Snappy.compress(super.toBytes(data));
} catch (IOException e) {
throw new DataAdaptionException("Failed to compress data", e);
}
}
@Override
public @NotNull UserData fromBytes(byte[] data) throws DataAdaptionException {
try {
return super.fromBytes(Snappy.uncompress(data));
} catch (IOException e) {
throw new DataAdaptionException("Failed to decompress data", e);
}
}
}

View File

@@ -0,0 +1,558 @@
/*
* 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.data;
import com.google.common.collect.Sets;
import com.google.gson.annotations.SerializedName;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import net.kyori.adventure.key.Key;
import net.william278.husksync.HuskSync;
import net.william278.husksync.user.OnlineUser;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.stream.Collectors;
/**
* A piece of data, held by a {@link DataHolder}
*/
@SuppressWarnings("unused")
public interface Data {
/**
* Apply (set) this data container to the given {@link OnlineUser}
*
* @param user the user to apply this element to
* @param plugin the plugin instance
*/
void apply(@NotNull UserDataHolder user, @NotNull HuskSync plugin);
/**
* A data container holding data for:
* <ul>
* <li>Inventories</li>
* <li>Ender Chests</li>
* </ul>
*/
interface Items extends Data {
@Nullable
Stack @NotNull [] getStack();
default int getSlotCount() {
return getStack().length;
}
record Stack(@NotNull String material, int amount, @Nullable String name,
@Nullable List<String> lore, @NotNull List<String> enchantments) {
}
default boolean isEmpty() {
return Arrays.stream(getStack()).allMatch(Objects::isNull) || getStack().length == 0;
}
void clear();
void setContents(@NotNull Items contents);
/**
* A data container holding data for inventories and selected hotbar slot
*/
interface Inventory extends Items {
int INVENTORY_SLOT_COUNT = 41;
String ITEMS_TAG = "items";
String HELD_ITEM_SLOT_TAG = "held_item_slot";
int getHeldItemSlot();
void setHeldItemSlot(int heldItemSlot) throws IllegalArgumentException;
default Optional<Stack> getHelmet() {
return Optional.ofNullable(getStack()[39]);
}
default Optional<Stack> getChestplate() {
return Optional.ofNullable(getStack()[38]);
}
default Optional<Stack> getLeggings() {
return Optional.ofNullable(getStack()[37]);
}
default Optional<Stack> getBoots() {
return Optional.ofNullable(getStack()[36]);
}
default Optional<Stack> getOffHand() {
return Optional.ofNullable(getStack()[40]);
}
}
/**
* Data container holding data for ender chests
*/
interface EnderChest extends Items {
int ENDER_CHEST_SLOT_COUNT = 27;
}
}
/**
* Data container holding data for potion effects
*/
interface PotionEffects extends Data {
@NotNull
List<Effect> getActiveEffects();
/**
* Represents a 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
* @param showParticles whether the potion effect shows particles
* @param hasIcon whether the potion effect displays a HUD icon
*/
record Effect(@SerializedName("type") @NotNull String type,
@SerializedName("amplifier") int amplifier,
@SerializedName("duration") int duration,
@SerializedName("is_ambient") boolean isAmbient,
@SerializedName("show_particles") boolean showParticles,
@SerializedName("has_icon") boolean hasIcon) {
}
}
/**
* Data container holding data for advancements
*/
interface Advancements extends Data {
String RECIPE_ADVANCEMENT = "minecraft:recipe";
@NotNull
List<Advancement> getCompleted();
@NotNull
default List<Advancement> getCompletedExcludingRecipes() {
return getCompleted().stream().filter(adv -> !adv.getKey().startsWith(RECIPE_ADVANCEMENT)).toList();
}
void setCompleted(@NotNull List<Advancement> completed);
class Advancement {
@SerializedName("key")
private String key;
@SerializedName("completed_criteria")
private Map<String, Long> completedCriteria;
private Advancement(@NotNull String key, @NotNull Map<String, Date> completedCriteria) {
this.key = key;
this.completedCriteria = adaptDateMap(completedCriteria);
}
@SuppressWarnings("unused")
private Advancement() {
}
@NotNull
public static Advancement adapt(@NotNull String key, @NotNull Map<String, Date> completedCriteria) {
return new Advancement(key, completedCriteria);
}
@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()));
}
@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())));
}
@NotNull
public String getKey() {
return key;
}
public void setKey(@NotNull String key) {
this.key = key;
}
public Map<String, Date> getCompletedCriteria() {
return adaptLongMap(completedCriteria);
}
public void setCompletedCriteria(Map<String, Date> completedCriteria) {
this.completedCriteria = adaptDateMap(completedCriteria);
}
}
}
/**
* Data container holding data for the player's location
*/
interface Location extends Data {
double getX();
void setX(double x);
double getY();
void setY(double y);
double getZ();
void setZ(double z);
float getYaw();
void setYaw(float yaw);
float getPitch();
void setPitch(float pitch);
@NotNull
World getWorld();
void setWorld(@NotNull World world);
record World(
@SerializedName("name") @NotNull String name,
@SerializedName("uuid") @NotNull UUID uuid,
@SerializedName("environment") @NotNull String environment
) {
}
}
/**
* Data container holding data for statistics
*/
interface Statistics extends Data {
@NotNull
Map<String, Integer> getGenericStatistics();
@NotNull
Map<String, Map<String, Integer>> getBlockStatistics();
@NotNull
Map<String, Map<String, Integer>> getItemStatistics();
@NotNull
Map<String, Map<String, Integer>> getEntityStatistics();
}
/**
* Data container holding data for persistent data containers
*/
interface PersistentData extends Data {
}
/**
* A data container holding data for:
* <ul>
* <li>Health</li>
* <li>Max Health</li>
* <li>Health Scale</li>
* </ul>
*/
interface Health extends Data {
double getHealth();
void setHealth(double health);
/**
* @deprecated Use {@link Attributes#getMaxHealth()} instead
*/
@Deprecated(forRemoval = true, since = "3.5")
default double getMaxHealth() {
return getHealth();
}
/**
* @deprecated Use {@link Attributes#setMaxHealth(double)} instead
*/
@Deprecated(forRemoval = true, since = "3.5")
default void setMaxHealth(double maxHealth) {
}
double getHealthScale();
void setHealthScale(double healthScale);
}
/**
* A data container holding player attribute data
*/
interface Attributes extends Data {
Key MAX_HEALTH_KEY = Key.key("generic.max_health");
List<Attribute> getAttributes();
record Attribute(
@NotNull String name,
double baseValue,
@NotNull Set<Modifier> modifiers
) {
public double getValue() {
double value = baseValue;
for (Modifier modifier : modifiers) {
value = modifier.modify(value);
}
return value;
}
}
@Getter
@Accessors(fluent = true)
@RequiredArgsConstructor
final class Modifier {
final static String ANY_EQUIPMENT_SLOT_GROUP = "any";
@Getter(AccessLevel.NONE)
@Nullable
@SerializedName("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 operation;
@SerializedName("equipment_slot")
@Deprecated(since = "3.7")
private 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.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) {
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 (operation) {
case 0 -> value + amount;
case 1 -> value * amount;
case 2 -> value * (1 + amount);
default -> value;
};
}
public boolean hasUuid() {
return uuid != null;
}
@NotNull
public UUID uuid() {
return uuid != null ? uuid : UUID.nameUUIDFromBytes(name.getBytes());
}
}
default Optional<Attribute> getAttribute(@NotNull Key key) {
return getAttributes().stream()
.filter(attribute -> attribute.name().equals(key.asString()))
.findFirst();
}
default void removeAttribute(@NotNull Key key) {
getAttributes().removeIf(attribute -> attribute.name().equals(key.asString()));
}
default double getMaxHealth() {
return getAttribute(MAX_HEALTH_KEY)
.map(Attribute::getValue)
.orElse(20.0);
}
default void setMaxHealth(double maxHealth) {
removeAttribute(MAX_HEALTH_KEY);
getAttributes().add(new Attribute(MAX_HEALTH_KEY.asString(), maxHealth, Sets.newHashSet()));
}
}
/**
* A data container holding data for:
* <ul>
*
* <li>Food Level</li>
* <li>Saturation</li>
* <li>Exhaustion</li>
* </ul>
*/
interface Hunger extends Data {
int getFoodLevel();
void setFoodLevel(int foodLevel);
float getSaturation();
void setSaturation(float saturation);
float getExhaustion();
void setExhaustion(float exhaustion);
}
/**
* A data container holding data for:
* <ul>
* <li>Total experience</li>
* <li>Experience level</li>
* <li>Experience progress</li>
* </ul>
*/
interface Experience extends Data {
int getTotalExperience();
void setTotalExperience(int totalExperience);
int getExpLevel();
void setExpLevel(int expLevel);
float getExpProgress();
void setExpProgress(float expProgress);
}
/**
* Data container holding data for the player's current game mode
*/
interface GameMode extends Data {
@NotNull
String getGameMode();
void setGameMode(@NotNull String gameMode);
/**
* Get if the player can fly.
*
* @return {@code false} since v3.5
* @deprecated Moved to its own data type. This will always return {@code false}.
* Use {@link FlightStatus#isAllowFlight()} instead
*/
@Deprecated(forRemoval = true, since = "3.5")
default boolean getAllowFlight() {
return false;
}
/**
* Set if the player can fly.
*
* @deprecated Moved to its own data type.
* Use {@link FlightStatus#setAllowFlight(boolean)} instead
*/
@Deprecated(forRemoval = true, since = "3.5")
default void setAllowFlight(boolean allowFlight) {
}
/**
* Get if the player is flying.
*
* @return {@code false} since v3.5
* @deprecated Moved to its own data type. This will always return {@code false}.
* Use {@link FlightStatus#isFlying()} instead
*/
@Deprecated(forRemoval = true, since = "3.5")
default boolean getIsFlying() {
return false;
}
/**
* Set if the player is flying.
*
* @deprecated Moved to its own data type.
* Use {@link FlightStatus#setFlying(boolean)} instead
*/
@Deprecated(forRemoval = true, since = "3.5")
default void setIsFlying(boolean isFlying) {
}
}
/**
* Data container holding data for the player's flight status
*
* @since 3.5
*/
interface FlightStatus extends Data {
boolean isAllowFlight();
void setAllowFlight(boolean allowFlight);
boolean isFlying();
void setFlying(boolean isFlying);
}
}

View File

@@ -1,40 +0,0 @@
package net.william278.husksync.data;
import org.jetbrains.annotations.NotNull;
/**
* An adapter that adapts {@link UserData} to and from a portable byte array.
*/
public interface DataAdapter {
/**
* Converts {@link UserData} to a byte array
*
* @param data The {@link UserData} to adapt
* @return The byte array.
* @throws DataAdaptionException If an error occurred during adaptation.
*/
byte[] toBytes(@NotNull UserData data) throws DataAdaptionException;
/**
* Serializes {@link UserData} to a JSON string.
*
* @param data The {@link UserData} to serialize
* @param pretty Whether to pretty print the JSON.
* @return The output json string.
* @throws DataAdaptionException If an error occurred during adaptation.
*/
@NotNull
String toJson(@NotNull UserData data, boolean pretty) throws DataAdaptionException;
/**
* Converts a byte array to {@link UserData}.
*
* @param data The byte array to adapt.
* @return The {@link UserData}.
* @throws DataAdaptionException If an error occurred during adaptation, such as if the byte array is invalid.
*/
@NotNull
UserData fromBytes(final byte[] data) throws DataAdaptionException;
}

View File

@@ -1,11 +0,0 @@
package net.william278.husksync.data;
/**
* Indicates an error occurred during {@link UserData} adaptation to and from (compressed) json.
*/
public class DataAdaptionException extends RuntimeException {
protected DataAdaptionException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.data;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.william278.husksync.HuskSync;
import org.jetbrains.annotations.NotNull;
import java.util.function.BiFunction;
/**
* An exception related to {@link DataSnapshot} formatting, thrown if an exception occurs when unpacking a snapshot
*/
@Getter
public class DataException extends IllegalStateException {
private final Reason reason;
private DataException(@NotNull DataException.Reason reason, @NotNull DataSnapshot data, @NotNull HuskSync plugin) {
super(reason.getMessage(plugin, data));
this.reason = reason;
}
/**
* Reasons why {@link DataException}s were thrown
*/
@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.",
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.",
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.",
snapshot.getPlatformType(), plugin.getPlatformType())),
NO_LEGACY_CONVERTER((plugin, snapshot) -> String.format("No legacy converter to convert format version: %s",
snapshot.getFormatVersion()));
private final BiFunction<HuskSync, DataSnapshot, String> exception;
@NotNull
String getMessage(@NotNull HuskSync plugin, @NotNull DataSnapshot data) {
return exception.apply(plugin, data);
}
@NotNull
DataException toException(@NotNull DataSnapshot data, @NotNull HuskSync plugin) {
return new DataException(this, data, plugin);
}
}
}

View File

@@ -0,0 +1,158 @@
/*
* 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.data;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.Optional;
@SuppressWarnings("unused")
public interface DataHolder {
@NotNull
Map<Identifier, Data> getData();
default Optional<? extends Data> getData(@NotNull Identifier id) {
return getData().entrySet().stream().filter(e -> e.getKey().equals(id)).map(Map.Entry::getValue).findFirst();
}
default void setData(@NotNull Identifier identifier, @NotNull Data data) {
getData().put(identifier, data);
}
@NotNull
default Optional<Data.Items.Inventory> getInventory() {
return getData(Identifier.INVENTORY).map(Data.Items.Inventory.class::cast);
}
default void setInventory(@NotNull Data.Items.Inventory inventory) {
setData(Identifier.INVENTORY, inventory);
}
@NotNull
default Optional<Data.Items.EnderChest> getEnderChest() {
return getData(Identifier.ENDER_CHEST).map(Data.Items.EnderChest.class::cast);
}
default void setEnderChest(@NotNull Data.Items.EnderChest enderChest) {
setData(Identifier.ENDER_CHEST, enderChest);
}
@NotNull
default Optional<Data.PotionEffects> getPotionEffects() {
return getData(Identifier.POTION_EFFECTS).map(Data.PotionEffects.class::cast);
}
default void setPotionEffects(@NotNull Data.PotionEffects potionEffects) {
setData(Identifier.POTION_EFFECTS, potionEffects);
}
@NotNull
default Optional<Data.Advancements> getAdvancements() {
return getData(Identifier.ADVANCEMENTS).map(Data.Advancements.class::cast);
}
default void setAdvancements(@NotNull Data.Advancements advancements) {
setData(Identifier.ADVANCEMENTS, advancements);
}
@NotNull
default Optional<Data.Location> getLocation() {
return Optional.ofNullable((Data.Location) getData().get(Identifier.LOCATION));
}
default void setLocation(@NotNull Data.Location location) {
getData().put(Identifier.LOCATION, location);
}
@NotNull
default Optional<Data.Statistics> getStatistics() {
return Optional.ofNullable((Data.Statistics) getData().get(Identifier.STATISTICS));
}
default void setStatistics(@NotNull Data.Statistics statistics) {
getData().put(Identifier.STATISTICS, statistics);
}
@NotNull
default Optional<Data.Health> getHealth() {
return Optional.ofNullable((Data.Health) getData().get(Identifier.HEALTH));
}
default void setHealth(@NotNull Data.Health health) {
getData().put(Identifier.HEALTH, health);
}
@NotNull
default Optional<Data.Hunger> getHunger() {
return Optional.ofNullable((Data.Hunger) getData().get(Identifier.HUNGER));
}
default void setHunger(@NotNull Data.Hunger hunger) {
getData().put(Identifier.HUNGER, hunger);
}
@NotNull
default Optional<Data.Attributes> getAttributes() {
return Optional.ofNullable((Data.Attributes) getData().get(Identifier.ATTRIBUTES));
}
default void setAttributes(@NotNull Data.Attributes attributes) {
getData().put(Identifier.ATTRIBUTES, attributes);
}
@NotNull
default Optional<Data.Experience> getExperience() {
return Optional.ofNullable((Data.Experience) getData().get(Identifier.EXPERIENCE));
}
default void setExperience(@NotNull Data.Experience experience) {
getData().put(Identifier.EXPERIENCE, experience);
}
@NotNull
default Optional<Data.GameMode> getGameMode() {
return Optional.ofNullable((Data.GameMode) getData().get(Identifier.GAME_MODE));
}
default void setGameMode(@NotNull Data.GameMode gameMode) {
getData().put(Identifier.GAME_MODE, gameMode);
}
@NotNull
default Optional<Data.FlightStatus> getFlightStatus() {
return Optional.ofNullable((Data.FlightStatus) getData().get(Identifier.FLIGHT_STATUS));
}
default void setFlightStatus(@NotNull Data.FlightStatus flightStatus) {
getData().put(Identifier.FLIGHT_STATUS, flightStatus);
}
@NotNull
default Optional<Data.PersistentData> getPersistentData() {
return Optional.ofNullable((Data.PersistentData) getData().get(Identifier.PERSISTENT_DATA));
}
default void setPersistentData(@NotNull Data.PersistentData persistentData) {
getData().put(Identifier.PERSISTENT_DATA, persistentData);
}
}

View File

@@ -1,97 +0,0 @@
package net.william278.husksync.data;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.api.BaseHuskSyncAPI;
import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull;
/**
* Identifies the cause of a player data save.
*
* @implNote This enum is saved in the database.
* </p>
* Cause names have a max length of 32 characters.
*/
public enum DataSaveCause {
/**
* Indicates data saved when a player disconnected from the server (either to change servers, or to log off)
*
* @since 2.0
*/
DISCONNECT,
/**
* Indicates data saved when the world saved
*
* @since 2.0
*/
WORLD_SAVE,
/**
* Indicates data saved when the server shut down
*
* @since 2.0
*/
SERVER_SHUTDOWN,
/**
* Indicates data was saved by editing inventory contents via the {@code /inventory} command
*
* @since 2.0
*/
INVENTORY_COMMAND,
/**
* Indicates data was saved by editing Ender Chest contents via the {@code /enderchest} command
*
* @since 2.0
*/
ENDERCHEST_COMMAND,
/**
* Indicates data was saved by restoring it from a previous version
*
* @since 2.0
*/
BACKUP_RESTORE,
/**
* Indicates data was saved by an API call
*
* @see BaseHuskSyncAPI#saveUserData(OnlineUser)
* @see BaseHuskSyncAPI#setUserData(User, UserData)
* @since 2.0
*/
API,
/**
* Indicates data was saved from being imported from MySQLPlayerDataBridge
*
* @since 2.0
*/
MPDB_MIGRATION,
/**
* Indicates data was saved from being imported from a legacy version (v1.x)
*
* @since 2.0
*/
LEGACY_MIGRATION,
/**
* Indicates data was saved by an unknown cause.
* </p>
* This should not be used and is only used for error handling purposes.
*
* @since 2.0
*/
UNKNOWN;
/**
* Returns a {@link DataSaveCause} by name.
*
* @return the {@link DataSaveCause} or {@link #UNKNOWN} if the name is not valid.
*/
@NotNull
public static DataSaveCause getCauseByName(@NotNull String name) {
for (DataSaveCause cause : values()) {
if (cause.name().equalsIgnoreCase(name)) {
return cause;
}
}
return UNKNOWN;
}
}

View File

@@ -1,15 +0,0 @@
package net.william278.husksync.data;
import org.jetbrains.annotations.NotNull;
/**
* Indicates an error occurred during Base-64 serialization and deserialization of data.
* </p>
* For example, an exception deserializing {@link ItemData} item stack or {@link PotionEffectData} potion effect arrays
*/
public class DataSerializationException extends RuntimeException {
protected DataSerializationException(@NotNull String message, @NotNull Throwable cause) {
super(message, cause);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,328 @@
/*
* 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.data;
import lombok.*;
import net.kyori.adventure.key.InvalidKeyException;
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;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
/**
* Identifiers of different types of {@link Data}s
*/
@Getter
public class Identifier {
// Built-in identifiers
public static final Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
public static final Identifier INVENTORY = huskSync("inventory", true);
public static final Identifier ENDER_CHEST = huskSync("ender_chest", true);
public static final Identifier ADVANCEMENTS = huskSync("advancements", true);
public static final Identifier STATISTICS = huskSync("statistics", true);
public static final Identifier POTION_EFFECTS = huskSync("potion_effects", true);
public static final Identifier GAME_MODE = huskSync("game_mode", true);
public static final Identifier FLIGHT_STATUS = huskSync("flight_status", true,
Dependency.optional("game_mode")
);
public static final Identifier ATTRIBUTES = huskSync("attributes", true,
Dependency.optional("inventory"),
Dependency.optional("potion_effects")
);
public static final Identifier HEALTH = huskSync("health", true,
Dependency.optional("attributes")
);
public static final Identifier HUNGER = huskSync("hunger", true,
Dependency.optional("attributes")
);
public static final Identifier EXPERIENCE = huskSync("experience", true,
Dependency.optional("advancements")
);
public static final Identifier LOCATION = huskSync("location", false,
Dependency.optional("flight_status"),
Dependency.optional("potion_effects")
);
private final Key key;
private final boolean enabledByDefault;
@Getter
private final Set<Dependency> dependencies;
@Setter
@Getter
public boolean enabled;
private Identifier(@NotNull Key key, boolean enabledByDefault, @NotNull Set<Dependency> dependencies) {
this.key = key;
this.enabledByDefault = enabledByDefault;
this.enabled = enabledByDefault;
this.dependencies = dependencies;
}
/**
* Create an identifier from a {@link Key}
*
* @param key the key
* @param dependencies the dependencies
* @return the identifier
* @since 3.5.4
*/
@NotNull
public static Identifier from(@NotNull Key key, @NotNull Set<Dependency> dependencies) {
if (key.namespace().equals("husksync")) {
throw new IllegalArgumentException("You cannot register a key with \"husksync\" as the namespace!");
}
return new Identifier(key, true, dependencies);
}
/**
* Create an identifier from a {@link Key}
*
* @param key the key
* @return the identifier
* @since 3.0
*/
@NotNull
public static Identifier from(@NotNull Key key) {
return from(key, Collections.emptySet());
}
/**
* Create an identifier from a namespace and value
*
* @param plugin the namespace
* @param name the value
* @return the identifier
* @since 3.0
*/
@NotNull
public static Identifier from(@Subst("plugin") @NotNull String plugin, @Subst("null") @NotNull String name) {
return from(Key.key(plugin, name));
}
/**
* Create an identifier from a namespace, value, and dependencies
*
* @param plugin the namespace
* @param name the value
* @param dependencies the dependencies
* @return the identifier
* @since 3.5.4
*/
@NotNull
public static Identifier from(@Subst("plugin") @NotNull String plugin, @Subst("null") @NotNull String name,
@NotNull Set<Dependency> dependencies) {
return from(Key.key(plugin, name), dependencies);
}
// Return an identifier with a HuskSync namespace
@NotNull
private static Identifier huskSync(@Subst("null") @NotNull String name,
boolean configDefault) throws InvalidKeyException {
return new Identifier(Key.key("husksync", name), configDefault, Collections.emptySet());
}
// Return an identifier with a HuskSync namespace
@NotNull
private static Identifier huskSync(@Subst("null") @NotNull String name,
@SuppressWarnings("SameParameterValue") boolean configDefault,
@NotNull Dependency... dependents) throws InvalidKeyException {
return new Identifier(Key.key("husksync", name), configDefault, Set.of(dependents));
}
/**
* <b>(Internal use only)</b> - Get a map of the default config entries for all HuskSync identifiers
*
* @return a map of all the config entries
* @since 3.0
*/
@NotNull
@ApiStatus.Internal
@SuppressWarnings("unchecked")
public static Map<String, Boolean> getConfigMap() {
return Map.ofEntries(Stream.of(
INVENTORY, ENDER_CHEST, POTION_EFFECTS, ADVANCEMENTS, LOCATION, STATISTICS,
HEALTH, HUNGER, ATTRIBUTES, EXPERIENCE, GAME_MODE, FLIGHT_STATUS, PERSISTENT_DATA
)
.map(Identifier::getConfigEntry)
.toArray(Map.Entry[]::new));
}
/**
* Returns {@code true} if the identifier depends on the given identifier
*
* @param identifier the identifier to check
* @return {@code true} if the identifier depends on the given identifier
* @since 3.5.4
*/
public boolean dependsOn(@NotNull Identifier identifier) {
return dependencies.contains(Dependency.required(identifier.key));
}
/**
* Get the namespace of the identifier
*
* @return the namespace
*/
@NotNull
public String getKeyNamespace() {
return key.namespace();
}
/**
* Get the value of the identifier
*
* @return the value
*/
@NotNull
public String getKeyValue() {
return key.value();
}
/**
* Returns {@code true} if the identifier is a custom (non-HuskSync) identifier
*
* @return {@code false} if {@link #getKeyNamespace()} returns "husksync"; {@code true} otherwise
*/
public boolean isCustom() {
return !getKeyNamespace().equals("husksync");
}
/**
* Returns the identifier as a string (the key)
*
* @return the identifier as a string
*/
@NotNull
@Override
public String toString() {
return key.asString();
}
/**
* Returns {@code true} if the given object is an identifier with the same key as this identifier
*
* @param obj the object to compare
* @return {@code true} if the given object is an identifier with the same key as this identifier
*/
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof Identifier other ? toString().equals(other.toString()) : super.equals(obj);
}
@Override
public int hashCode() {
return key.toString().hashCode();
}
// Get the config entry for the identifier
@NotNull
private Map.Entry<String, Boolean> getConfigEntry() {
return Map.entry(getKeyValue(), enabledByDefault);
}
/**
* Compares two identifiers based on their dependencies.
* <p>
* If this identifier contains a dependency on the other, it should come after & vice versa
*
* @since 3.5.4
*/
@NoArgsConstructor(access = AccessLevel.PACKAGE)
static class DependencyOrderComparator implements Comparator<Identifier> {
@Override
public int compare(@NotNull Identifier i1, @NotNull Identifier i2) {
if (i1.equals(i2)) {
return 0;
}
if (i1.dependsOn(i2)) {
if (i2.dependsOn(i1)) {
throw new IllegalArgumentException(
"Found circular dependency between %s and %s".formatted(i1.getKey(), i2.getKey())
);
}
return 1;
}
return -1;
}
}
/**
* Represents a data dependency of an identifier, used to determine the order in which data is applied to users
*
* @since 3.5.4
*/
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class Dependency {
/**
* Key of the data dependency see {@code Identifier#key()}
*/
private Key key;
/**
* Whether the data dependency is required to be present & enabled for the dependant data to enabled
*/
private boolean required;
@NotNull
protected static Dependency required(@NotNull Key identifier) {
return new Dependency(identifier, true);
}
@NotNull
public static Dependency optional(@NotNull Key identifier) {
return new Dependency(identifier, false);
}
@NotNull
@SuppressWarnings("SameParameterValue")
private static Dependency required(@Subst("null") @NotNull String identifier) {
return required(Key.key("husksync", identifier));
}
@NotNull
private static Dependency optional(@Subst("null") @NotNull String identifier) {
return optional(Key.key("husksync", identifier));
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Dependency other) {
return key.equals(other.key);
}
return false;
}
@Override
public int hashCode() {
return key.toString().hashCode();
}
}
}

View File

@@ -1,25 +0,0 @@
package net.william278.husksync.data;
import com.google.gson.annotations.SerializedName;
import org.jetbrains.annotations.NotNull;
/**
* Stores information about the contents of a player's inventory or Ender Chest.
*/
public class ItemData {
/**
* A Base-64 string of platform-serialized items
*/
@SerializedName("serialized_items")
public String serializedItems;
public ItemData(@NotNull final String serializedItems) {
this.serializedItems = serializedItems;
}
@SuppressWarnings("unused")
protected ItemData() {
}
}

View File

@@ -1,29 +0,0 @@
package net.william278.husksync.data;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import org.jetbrains.annotations.NotNull;
import java.nio.charset.StandardCharsets;
public class JsonDataAdapter implements DataAdapter {
@Override
public byte[] toBytes(@NotNull UserData data) throws DataAdaptionException {
return toJson(data, false).getBytes(StandardCharsets.UTF_8);
}
@Override
public @NotNull String toJson(@NotNull UserData data, boolean pretty) throws DataAdaptionException {
return (pretty ? new GsonBuilder().setPrettyPrinting() : new GsonBuilder()).create().toJson(data);
}
@Override
public @NotNull UserData fromBytes(byte[] data) throws DataAdaptionException {
try {
return new GsonBuilder().create().fromJson(new String(data, StandardCharsets.UTF_8), UserData.class);
} catch (JsonSyntaxException e) {
throw new DataAdaptionException("Failed to parse JSON data", e);
}
}
}

View File

@@ -1,73 +0,0 @@
package net.william278.husksync.data;
import com.google.gson.annotations.SerializedName;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
/**
* Stores information about a player's location
*/
public class LocationData {
/**
* Name of the world on the server
*/
@SerializedName("world_name")
public String worldName;
/**
* Unique id of the world
*/
@SerializedName("world_uuid")
public UUID worldUuid;
/**
* The environment type of the world (one of "NORMAL", "NETHER", "THE_END")
*/
@SerializedName("world_environment")
public String worldEnvironment;
/**
* The x coordinate of the location
*/
@SerializedName("x")
public double x;
/**
* The y coordinate of the location
*/
@SerializedName("y")
public double y;
/**
* The z coordinate of the location
*/
@SerializedName("z")
public double z;
/**
* The location's facing yaw angle
*/
@SerializedName("yaw")
public float yaw;
/**
* The location's facing pitch angle
*/
@SerializedName("pitch")
public float pitch;
public LocationData(@NotNull String worldName, @NotNull UUID worldUuid,
@NotNull String worldEnvironment,
double x, double y, double z,
float yaw, float pitch) {
this.worldName = worldName;
this.worldUuid = worldUuid;
this.worldEnvironment = worldEnvironment;
this.x = x;
this.y = y;
this.z = z;
this.yaw = yaw;
this.pitch = pitch;
}
@SuppressWarnings("unused")
protected LocationData() {
}
}

View File

@@ -1,27 +0,0 @@
package net.william278.husksync.data;
import com.google.gson.annotations.SerializedName;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
/**
* Store's a user's persistent data container, holding a map of plugin-set persistent values
*/
public class PersistentDataContainerData {
/**
* Map of namespaced key strings to a byte array representing the persistent data
*/
@SerializedName("persistent_data_map")
public Map<String, Byte[]> persistentDataMap;
public PersistentDataContainerData(@NotNull final Map<String, Byte[]> persistentDataMap) {
this.persistentDataMap = persistentDataMap;
}
@SuppressWarnings("unused")
protected PersistentDataContainerData() {
}
}

View File

@@ -1,22 +0,0 @@
package net.william278.husksync.data;
import com.google.gson.annotations.SerializedName;
import org.jetbrains.annotations.NotNull;
/**
* Stores potion effect data
*/
public class PotionEffectData {
@SerializedName("serialized_potion_effects")
public String serializedPotionEffects;
public PotionEffectData(@NotNull final String serializedPotionEffects) {
this.serializedPotionEffects = serializedPotionEffects;
}
@SuppressWarnings("unused")
protected PotionEffectData() {
}
}

View File

@@ -0,0 +1,73 @@
/*
* 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.data;
import net.william278.desertwell.util.Version;
import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.Adaptable;
import org.jetbrains.annotations.NotNull;
public interface Serializer<T extends Data> {
T deserialize(@NotNull String serialized);
default T deserialize(@NotNull String serialized, @NotNull Version dataMcVersion) throws DeserializationException {
return deserialize(serialized);
}
@NotNull
String serialize(@NotNull T element) throws SerializationException;
final class DeserializationException extends IllegalStateException {
DeserializationException(@NotNull String message, @NotNull Throwable cause) {
super(message, cause);
}
}
final class SerializationException extends IllegalStateException {
SerializationException(@NotNull String message, @NotNull Throwable cause) {
super(message, cause);
}
}
class Json<T extends Data & Adaptable> implements Serializer<T> {
private final HuskSync plugin;
private final Class<T> type;
public Json(@NotNull HuskSync plugin, @NotNull Class<T> type) {
this.type = type;
this.plugin = plugin;
}
@Override
public T deserialize(@NotNull String serialized) throws DeserializationException {
return plugin.getDataAdapter().fromJson(serialized, type);
}
@NotNull
@Override
public String serialize(@NotNull T element) throws SerializationException {
return plugin.getDataAdapter().toJson(element);
}
}
}

Some files were not shown because too many files have changed in this diff Show More