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

Compare commits

...

774 Commits

Author SHA1 Message Date
its.bread
0772f09e98 fix: hardcode map scale to CLOSEST instead of NORMAL (#632) 2025-12-13 19:24:02 +00:00
dependabot[bot]
e916673454 deps: bump org.jetbrains:annotations from 26.0.2 to 26.0.2-1 (#615) 2025-12-11 15:23:37 +00:00
dependabot[bot]
ac163d5130 deps: bump com.google.guava:guava from 33.4.8-jre to 33.5.0-jre (#618)
Bumps [com.google.guava:guava](https://github.com/google/guava) from 33.4.8-jre to 33.5.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-version: 33.5.0-jre
  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-12-10 23:22:30 +00:00
dependabot[bot]
d656b67570 deps: bump de.exlll:configlib-yaml from 4.6.3 to 4.6.4 (#627)
Bumps [de.exlll:configlib-yaml](https://github.com/Exlll/ConfigLib) from 4.6.3 to 4.6.4.
- [Release notes](https://github.com/Exlll/ConfigLib/releases)
- [Commits](https://github.com/Exlll/ConfigLib/compare/v4.6.3...v4.6.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-10 23:22:19 +00:00
AO
3c66b65ac6 add DataVersion 1.21.11 (#630) 2025-12-10 23:22:10 +00:00
its.bread
c227933b3b feat: add Paper 1.21.11 support (#629) 2025-12-10 16:43:24 +00:00
dependabot[bot]
5cf9cb8e50 deps: bump org.junit:junit-bom from 5.13.3 to 6.0.1 (#617)
Bumps [org.junit:junit-bom](https://github.com/junit-team/junit-framework) from 5.13.3 to 6.0.1.
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](https://github.com/junit-team/junit-framework/compare/r5.13.3...r6.0.1)

---
updated-dependencies:
- dependency-name: org.junit:junit-bom
  dependency-version: 6.0.1
  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>
2025-12-07 21:19:15 +00:00
dependabot[bot]
693cd6120f deps: bump org.projectlombok:lombok from 1.18.38 to 1.18.42 (#619)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.38 to 1.18.42.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.38...v1.18.42)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-version: 1.18.42
  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-12-07 21:19:05 +00:00
dependabot[bot]
6efd800481 build(deps): bump urllib3 from 2.5.0 to 2.6.0 in /test (#625)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.5.0 to 2.6.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.5.0...2.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-07 21:18:57 +00:00
its.bread
a723a7cba3 Fix MongoDB getMapBinding and clarify Javadoc (#624) 2025-12-04 17:45:38 +00:00
Johannes / EinJojo
b1a5eb5f44 Fix org.postgresql.util.PSQLException: ERROR: operator does not exist: uuid = character varying (#623) 2025-12-04 15:28:27 +00:00
its.bread
8232282d13 Fix locked maps not rendering after server restart (#620) 2025-12-04 15:28:06 +00:00
dependabot[bot]
404d359f89 deps: bump com.github.retrooper:packetevents-spigot from 2.9.4 to 2.10.1 (#608)
Bumps [com.github.retrooper:packetevents-spigot](https://github.com/retrooper/packetevents) from 2.9.4 to 2.10.1.
- [Release notes](https://github.com/retrooper/packetevents/releases)
- [Changelog](https://github.com/retrooper/packetevents/blob/2.0/CHANGELOG.md)
- [Commits](https://github.com/retrooper/packetevents/compare/v2.9.4...v2.10.1)

---
updated-dependencies:
- dependency-name: com.github.retrooper:packetevents-spigot
  dependency-version: 2.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-29 19:33:29 +00:00
dependabot[bot]
62e84d92fc deps: bump net.kyori:adventure-platform-bukkit from 4.4.0 to 4.4.1 (#607)
Bumps [net.kyori:adventure-platform-bukkit](https://github.com/KyoriPowered/adventure-platform) from 4.4.0 to 4.4.1.
- [Release notes](https://github.com/KyoriPowered/adventure-platform/releases)
- [Commits](https://github.com/KyoriPowered/adventure-platform/compare/v4.4.0...v4.4.1)

---
updated-dependencies:
- dependency-name: net.kyori:adventure-platform-bukkit
  dependency-version: 4.4.1
  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-11-29 19:33:12 +00:00
dependabot[bot]
9b2246eac2 deps: bump commons-io:commons-io from 2.20.0 to 2.21.0 (#606)
Bumps [commons-io:commons-io](https://github.com/apache/commons-io) from 2.20.0 to 2.21.0.
- [Changelog](https://github.com/apache/commons-io/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-io/compare/rel/commons-io-2.20.0...rel/commons-io-2.21.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-29 19:33:05 +00:00
dependabot[bot]
e6d3935246 ci: bump actions/checkout from 5 to 6 (#613)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  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>
2025-11-29 19:32:56 +00:00
its.bread
5c4111b6a7 fix: prevent race condition in CHECK_IN_PETITION handler (#614)
- Replace confusing 'online' boolean with direct state checks
- Only release DATA_CHECKOUT when user is truly offline AND unlocked
2025-11-29 19:32:28 +00:00
AO
51a700600a Update DataVersionSupplier.java (#609) 2025-11-12 16:54:03 +00:00
William278
23c3ee08e9 fix: actually add build files for 1.21.10 2025-11-10 17:35:56 +00:00
William278
c1d08f9c23 Merge remote-tracking branch 'origin/master' 2025-11-09 20:23:59 +00:00
William278
4cabdbe952 fix: restore wrapper 2025-11-09 20:23:51 +00:00
dependabot[bot]
abdebd960b ci: bump actions/checkout from 4 to 5 (#572)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  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>
Co-authored-by: William <will27528@gmail.com>
2025-11-09 20:20:52 +00:00
dependabot[bot]
a7aea51a45 deps: bump de.exlll:configlib-yaml from 4.6.1 to 4.6.3 (#598)
Bumps [de.exlll:configlib-yaml](https://github.com/Exlll/ConfigLib) from 4.6.1 to 4.6.3.
- [Release notes](https://github.com/Exlll/ConfigLib/releases)
- [Commits](https://github.com/Exlll/ConfigLib/compare/v4.6.1...v4.6.3)

---
updated-dependencies:
- dependency-name: de.exlll:configlib-yaml
  dependency-version: 4.6.3
  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-11-09 20:20:41 +00:00
dependabot[bot]
c8a4376208 deps: bump com.google.code.gson:gson from 2.13.1 to 2.13.2 (#599)
Bumps [com.google.code.gson:gson](https://github.com/google/gson) from 2.13.1 to 2.13.2.
- [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.13.1...gson-parent-2.13.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-09 20:20:33 +00:00
dependabot[bot]
315cd4ba6b ci: bump mikepenz/action-junit-report from 5 to 6 (#603)
Bumps [mikepenz/action-junit-report](https://github.com/mikepenz/action-junit-report) from 5 to 6.
- [Release notes](https://github.com/mikepenz/action-junit-report/releases)
- [Commits](https://github.com/mikepenz/action-junit-report/compare/v5...v6)

---
updated-dependencies:
- dependency-name: mikepenz/action-junit-report
  dependency-version: '6'
  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>
2025-11-09 20:20:24 +00:00
dependabot[bot]
6607ac5a6e deps: bump de.exlll:configlib-core from 4.6.1 to 4.6.3 (#602)
Bumps [de.exlll:configlib-core](https://github.com/Exlll/ConfigLib) from 4.6.1 to 4.6.3.
- [Release notes](https://github.com/Exlll/ConfigLib/releases)
- [Commits](https://github.com/Exlll/ConfigLib/compare/v4.6.1...v4.6.3)

---
updated-dependencies:
- dependency-name: de.exlll:configlib-core
  dependency-version: 4.6.3
  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-11-09 20:20:19 +00:00
William278
562939498a build: bump to 3.8.8 2025-11-09 20:19:55 +00:00
William278
e686d43ca8 build: fixup build script, update wrapper 2025-11-09 20:19:30 +00:00
William278
e9ac400215 feat: add Paper 1.21.10 support 2025-10-28 19:29:20 +00:00
William278
234870537a feat: remove 1.20.1 support 2025-10-28 19:14:17 +00:00
dependabot[bot]
b5f392a20f ci: bump actions/setup-java from 4 to 5 (#575)
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4 to 5.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '5'
  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>
2025-10-28 19:04:23 +00:00
dependabot[bot]
9ea8eb4101 deps: bump com.zaxxer:HikariCP from 7.0.1 to 7.0.2 (#590)
Bumps [com.zaxxer:HikariCP](https://github.com/brettwooldridge/HikariCP) from 7.0.1 to 7.0.2.
- [Changelog](https://github.com/brettwooldridge/HikariCP/blob/dev/CHANGES)
- [Commits](https://github.com/brettwooldridge/HikariCP/compare/HikariCP-7.0.1...HikariCP-7.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-28 19:04:15 +00:00
dependabot[bot]
dc7cde1c33 deps: bump net.kyori:adventure-api from 4.23.0 to 4.25.0 (#593)
Bumps [net.kyori:adventure-api](https://github.com/KyoriPowered/adventure) from 4.23.0 to 4.25.0.
- [Release notes](https://github.com/KyoriPowered/adventure/releases)
- [Commits](https://github.com/KyoriPowered/adventure/compare/v4.23.0...v4.25.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-28 19:04:05 +00:00
Heriptik
4e75b5ca1d Fix: added null-checks for Material and EntityType in applyStat to avoid NPE (#589) 2025-10-03 09:56:15 +03:00
dependabot[bot]
fe0bdccf40 deps: bump com.gradleup.shadow from 8.3.8 to 9.2.2 (#587)
Bumps [com.gradleup.shadow](https://github.com/GradleUp/shadow) from 8.3.8 to 9.2.2.
- [Release notes](https://github.com/GradleUp/shadow/releases)
- [Commits](https://github.com/GradleUp/shadow/compare/8.3.8...9.2.2)

---
updated-dependencies:
- dependency-name: com.gradleup.shadow
  dependency-version: 9.2.2
  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>
2025-10-01 18:03:55 +03:00
Tkach
c615ab592b feat: allow overriding server name via HUSKSYNC_SERVER_NAME env (#586) 2025-09-23 19:35:26 +01:00
kFor
16d4a8fd9b fix: handle ender chest size mismatch (#582) 2025-09-11 23:51:15 +01:00
mfnalex
96f34092f6 Ensure attributes are accessed on main thread only (#578) 2025-08-29 13:20:44 +01:00
William278
a31c3c48f7 refactor: move data version fetching to an interface 2025-08-12 23:41:38 +01:00
William278
0e96374a03 fix: stat syncing not checking material validity, close #537 2025-08-12 23:34:45 +01:00
William278
24545563fa feat: target MC 1.21.8 instead of .7 2025-08-12 23:27:03 +01:00
William278
a9ea4d34e5 fix: malformed yaml being written to compatibility.yml files, close #565 2025-08-12 23:24:23 +01:00
William278
11453393d4 fix: Postgres syntax issue, close #545 2025-08-12 23:23:46 +01:00
dependabot[bot]
1d850a9ddb deps: bump commons-io:commons-io from 2.19.0 to 2.20.0 (#552)
Bumps [commons-io:commons-io](https://github.com/apache/commons-io) from 2.19.0 to 2.20.0.
- [Changelog](https://github.com/apache/commons-io/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-io/compare/rel/commons-io-2.19.0...rel/commons-io-2.20.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 23:15:00 +01:00
dependabot[bot]
5b90b3d006 deps: bump org.snakeyaml:snakeyaml-engine from 2.9 to 2.10 (#551)
Bumps [org.snakeyaml:snakeyaml-engine](https://bitbucket.org/snakeyaml/snakeyaml-engine) from 2.9 to 2.10.
- [Commits](https://bitbucket.org/snakeyaml/snakeyaml-engine/branches/compare/snakeyaml-engine-2.10..snakeyaml-engine-2.9)

---
updated-dependencies:
- dependency-name: org.snakeyaml:snakeyaml-engine
  dependency-version: '2.10'
  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-08-12 23:14:20 +01:00
dependabot[bot]
d85ec65384 deps: bump org.apache.commons:commons-text from 1.13.1 to 1.14.0 (#557)
Bumps [org.apache.commons:commons-text](https://github.com/apache/commons-text) from 1.13.1 to 1.14.0.
- [Changelog](https://github.com/apache/commons-text/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-text/compare/rel/commons-text-1.13.1...rel/commons-text-1.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 23:14:03 +01:00
dependabot[bot]
9d681db030 deps: bump com.github.retrooper:packetevents-spigot from 2.9.1 to 2.9.4 (#558)
Bumps [com.github.retrooper:packetevents-spigot](https://github.com/retrooper/packetevents) from 2.9.1 to 2.9.4.
- [Release notes](https://github.com/retrooper/packetevents/releases)
- [Changelog](https://github.com/retrooper/packetevents/blob/2.0/CHANGELOG.md)
- [Commits](https://github.com/retrooper/packetevents/compare/v2.9.1...v2.9.4)

---
updated-dependencies:
- dependency-name: com.github.retrooper:packetevents-spigot
  dependency-version: 2.9.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 23:13:42 +01:00
dependabot[bot]
c5c2dde0bf deps: bump com.zaxxer:HikariCP from 6.3.0 to 7.0.1 (#566)
Bumps [com.zaxxer:HikariCP](https://github.com/brettwooldridge/HikariCP) from 6.3.0 to 7.0.1.
- [Changelog](https://github.com/brettwooldridge/HikariCP/blob/dev/CHANGES)
- [Commits](https://github.com/brettwooldridge/HikariCP/compare/HikariCP-6.3.0...HikariCP-7.0.1)

---
updated-dependencies:
- dependency-name: com.zaxxer:HikariCP
  dependency-version: 7.0.1
  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>
2025-08-12 23:13:18 +01:00
Sushant Pangeni
807bffe9aa feat: add more Redis configuration options (#564) 2025-08-05 14:45:23 +01:00
William278
a1956c6822 build: bump NBT-API to 2.15.2-SNAPSHOT 2025-07-22 16:48:38 +01:00
AO
321dccb0b5 fix: NBT-API.DataFixerUtil mapping versions incorrectly (#554) 2025-07-21 18:23:17 +01:00
William278
1015c50802 Merge remote-tracking branch 'origin/master' 2025-07-21 18:22:57 +01:00
William
c5e759390b fix: issues with compat checking 2025-07-21 18:22:47 +01:00
William
883695b0b0 fix: also update compatibility.yml on fabric 2025-07-20 17:54:10 +01:00
William278
879aef471a feat: support 1.21.8 alongside 1.21.7
adds support for version range expressions to the CompatibilityChecker. Also adds tests for this.
2025-07-20 14:37:18 +01:00
dependabot[bot]
64c81a9a5a deps: bump org.junit:junit-bom from 5.13.2 to 5.13.3 (#540)
Bumps [org.junit:junit-bom](https://github.com/junit-team/junit-framework) from 5.13.2 to 5.13.3.
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](https://github.com/junit-team/junit-framework/compare/r5.13.2...r5.13.3)

---
updated-dependencies:
- dependency-name: org.junit:junit-bom
  dependency-version: 5.13.3
  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-07-20 13:40:46 +01:00
dependabot[bot]
b61a9a7bc3 deps: bump com.gradleup.shadow from 8.3.7 to 8.3.8 (#541)
Bumps [com.gradleup.shadow](https://github.com/GradleUp/shadow) from 8.3.7 to 8.3.8.
- [Release notes](https://github.com/GradleUp/shadow/releases)
- [Commits](https://github.com/GradleUp/shadow/compare/8.3.7...8.3.8)

---
updated-dependencies:
- dependency-name: com.gradleup.shadow
  dependency-version: 8.3.8
  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-07-20 13:40:35 +01:00
dependabot[bot]
3f725eb40c ci: bump Andrew-Chen-Wang/github-wiki-action from 4 to 5 (#544)
Bumps [Andrew-Chen-Wang/github-wiki-action](https://github.com/andrew-chen-wang/github-wiki-action) from 4 to 5.
- [Release notes](https://github.com/andrew-chen-wang/github-wiki-action/releases)
- [Commits](https://github.com/andrew-chen-wang/github-wiki-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: Andrew-Chen-Wang/github-wiki-action
  dependency-version: '5'
  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>
2025-07-20 13:40:17 +01:00
dependabot[bot]
8f1e4a5198 deps: bump com.github.retrooper:packetevents-spigot from 2.8.0 to 2.9.1 (#546)
Bumps [com.github.retrooper:packetevents-spigot](https://github.com/retrooper/packetevents) from 2.8.0 to 2.9.1.
- [Release notes](https://github.com/retrooper/packetevents/releases)
- [Changelog](https://github.com/retrooper/packetevents/blob/2.0/CHANGELOG.md)
- [Commits](https://github.com/retrooper/packetevents/compare/v2.8.0...v2.9.1)

---
updated-dependencies:
- dependency-name: com.github.retrooper:packetevents-spigot
  dependency-version: 2.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-20 13:40:08 +01:00
William278
3875447430 build: bump uniform to 1.3.8, fix mixin issue on fabric 1.21.7 2025-07-04 21:44:26 +01:00
William278
dce84f285d feat: support Minecraft 1.21.7 2025-07-03 20:17:53 +01:00
dependabot[bot]
1314683eea deps: bump com.gradleup.shadow from 8.3.6 to 8.3.7 (#535)
Bumps [com.gradleup.shadow](https://github.com/GradleUp/shadow) from 8.3.6 to 8.3.7.
- [Release notes](https://github.com/GradleUp/shadow/releases)
- [Commits](https://github.com/GradleUp/shadow/compare/8.3.6...8.3.7)

---
updated-dependencies:
- dependency-name: com.gradleup.shadow
  dependency-version: 8.3.7
  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-07-03 13:08:00 +01:00
dependabot[bot]
6b1f89aab0 deps: bump org.junit:junit-bom from 5.13.1 to 5.13.2 (#536)
Bumps [org.junit:junit-bom](https://github.com/junit-team/junit-framework) from 5.13.1 to 5.13.2.
- [Release notes](https://github.com/junit-team/junit-framework/releases)
- [Commits](https://github.com/junit-team/junit-framework/compare/r5.13.1...r5.13.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 13:07:52 +01:00
lolplay123
27e958a474 locales: update Japanese locales ja-jp.yml (#534) 2025-06-28 00:58:04 +01:00
William
39ebd0dc4f docs: document steps to reset a server 2025-06-25 20:31:48 +01:00
William278
2ada0497ec docs: update compatibility 2025-06-25 18:55:44 +01:00
William278
e9f2856040 feat: add support for Minecraft 1.21.6 2025-06-23 00:15:11 +01:00
William278
6050c584c0 refactor(paper): improve handling of locked maps in item frames 2025-06-22 14:23:52 +01:00
William278
7ebf91bfae refactor(paper): further refactors to locked maps 2025-06-22 00:24:36 +01:00
William278
2d7799628a refactor(paper): avoid use of default maven central URL 2025-06-21 15:32:02 +01:00
William278
1627de732b refactor: enable check-in petitions by default 2025-06-21 15:09:17 +01:00
William278
fea882c642 fix(paper): locked maps losing data on restart, close #498 2025-06-21 15:08:16 +01:00
dependabot[bot]
8b749357f7 build(deps): bump urllib3 from 2.2.2 to 2.5.0 in /test (#528)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.2 to 2.5.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.2.2...2.5.0)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,96 @@
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@v6
- name: 'Set up JDK 21 📦'
uses: actions/setup-java@v5
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@v6
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.21.1
paper-1.21.4
paper-1.21.5
paper-1.21.8
paper-1.21.10
paper-1.21.11
fabric-1.21.1
fabric-1.21.4
fabric-1.21.5
fabric-1.21.8
distro-groups: |
paper
paper
paper
paper
paper
paper
fabric
fabric
fabric
fabric
distro-descriptions: |
Paper 1.21.1
Paper 1.21.4
Paper 1.21.5
Paper 1.21.8
Paper 1.21.10
Paper 1.21.11
Fabric 1.21.1
Fabric 1.21.4
Fabric 1.21.5
Fabric 1.21.8
files: |
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.1.jar
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.4.jar
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.5.jar
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.8.jar
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.10.jar
target/HuskSync-Bukkit-${{ env.version_name }}+mc.1.21.11.jar
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.1.jar
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.4.jar
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.5.jar
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.21.8.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@v6
- name: 'Set up JDK 21 📦'
uses: actions/setup-java@v5
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@v6
if: success() || failure() # Continue on failure
with:
report_paths: '**/build/test-results/test/TEST-*.xml'

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

@@ -0,0 +1,81 @@
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@v6
- name: 'Set up JDK 21 📦'
uses: actions/setup-java@v5
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@v6
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.21.1
paper-1.21.4
paper-1.21.5
paper-1.21.8
paper-1.21.10
fabric-1.21.1
fabric-1.21.4
fabric-1.21.5
fabric-1.21.8
distro-groups: |
paper
paper
paper
paper
paper
fabric
fabric
fabric
fabric
distro-descriptions: |
Paper 1.21.1
Paper 1.21.4
Paper 1.21.5
Paper 1.21.8
Paper 1.21.10
Fabric 1.21.1
Fabric 1.21.4
Fabric 1.21.5
Fabric 1.21.8
files: |
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.1.jar
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.4.jar
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.5.jar
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.8.jar
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.10.jar
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.1.jar
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.4.jar
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.5.jar
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.8.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@v6
- name: 'Push Docs to Github Wiki 📄️'
uses: Andrew-Chen-Wang/github-wiki-action@v5
with:
path: 'docs'

4
.gitignore vendored
View File

@@ -118,3 +118,7 @@ run/
!gradle-wrapper.jar !gradle-wrapper.jar
/build-output-final/ /build-output-final/
/target/ /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 Apache License
This source code is provided as reference to licensed individuals that have purchased the HuskSync Version 2.0, January 2004
plugin once from any of the official sources it is provided. The availability of this code does http://www.apache.org/licenses/
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.
CONTRIBUTOR AGREEMENT TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
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
DEFINITIONS 1. Definitions.
"plugin"; the jar file compiled from this source code
"source code"; the java source code and gradle configurations provided in this repository, however "License" shall mean the terms and conditions for use, reproduction,
excludes libraries and distribution as defined by Sections 1 through 9 of this document.
"copyright holder"; William278
"contributor(s)"; person(s) who submit (contribute) code through a pull request to this repository "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.

129
README.md
View File

@@ -1,61 +1,112 @@
# [![HuskSync Banner](images/banner-graphic.png)](https://github.com/WiIIiam278/HuskSync) <!--suppress ALL -->
![Github Actions](https://github.com/WiIIiam278/HuskSync/workflows/Java%20CI/badge.svg) <p align="center">
[![Discord](https://img.shields.io/discord/818135932103557162?color=7289da&logo=discord)](https://discord.gg/tVYhJfyDWG) <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 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.
**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.
## Features ## 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. **⭐ Seamless synchronization** &mdash; Utilises optimised Redis caching when players change server to sync player data super quickly for a seamless experience.
- 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.
## Requirements **⭐ 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!
* A MySQL Database (v8.0+)
* A Redis Database (v5.0+) **⭐ Backup, restore & rotate** &mdash; Something gone wrong? Restore players back to a previous data state. Rotate and manage data snapshots in-game!
* Any number of proxied Spigot servers (Minecraft v1.16.5+)
**⭐ 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.10 | _latest_ | 21 | Paper | ✅ **Active Release** |
| 1.21.7/8 | _latest_ | 21 | Paper, Fabric | ✅ **August 2026** |
| 1.21.6 | 3.8.5 | 21 | Paper | 🗃️ Archived (July 2025) |
| 1.21.5 | _latest_ | 21 | Paper | ✅ **February 2026** (Non-LTS) |
| 1.21.4 | _latest_ | 21 | Paper, Fabric | ✅ **February 2026** (Non-LTS) |
| 1.21.3 | 3.7.1 | 21 | Paper, Fabric | 🗃️ Archived (December 2024) |
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **May 2026** (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 | 3.8.7 | 17 | Paper, Fabric | 🗃️ Archived (November 2024) |
| 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 ## 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. 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).
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.
## Building 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.
To build HuskSync, simply run the following in the root of the repository: 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 ./gradlew clean build
``` ```
## License 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.
HuskSync is a premium resource. This source code is provided as reference only for those who have purchased the resource from an official source.
### License
HuskSync is licensed under the Apache 2.0 license.
- [License](https://github.com/WiIIiam278/HuskSync/blob/master/LICENSE) - [License](https://github.com/WiIIiam278/HuskSync/blob/master/LICENSE)
## Contributing Contributions to the project are welcome&mdash;feel free to open a pull request with new features, improvements and/or fixes!
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.
## 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. 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) - [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) - [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 ## Links
- [Documentation, Guides & API](https://william278.net/docs/husksync/Home) - [Docs](https://william278.net/docs/husksync/) &mdash; Read the plugin documentation!
- [Resource Page](https://www.spigotmc.org/resources/husksync.97144/) - [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/))
- [Bug Reports](https://github.com/WiIIiam278/HuskSync/issues) - [Issues](https://github.com/WiIIiam278/HuskSync/issues) &mdash; File a bug report or feature request
- [Discord Support](https://discord.gg/tVYhJfyDWG) (Proof of purchase required) - [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,41 +1,104 @@
import org.apache.tools.ant.filters.ReplaceTokens
plugins { plugins {
id 'com.github.johnrengelman.shadow' version '7.1.2' id 'com.gradleup.shadow' version '9.2.2'
id 'org.ajoberstar.grgit' version '5.0.0' id 'org.cadixdev.licenser' version '0.6.1' apply false
id 'java' id 'dev.architectury.loom' version '1.9-SNAPSHOT' apply false
id 'gg.essential.multi-version.root' apply false
id 'org.ajoberstar.grgit' version '5.3.2'
id 'maven-publish' id 'maven-publish'
id 'java'
} }
group 'net.william278' group 'net.william278'
version "$ext.plugin_version+${versionMetadata()}" version "$ext.plugin_version${versionMetadata()}"
description "$ext.plugin_description"
defaultTasks 'licenseFormat', 'build'
ext { ext {
set 'version', version.toString() 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 { 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' apply plugin: 'java'
compileJava.options.encoding = 'UTF-8' compileJava.options.encoding = 'UTF-8'
compileJava.options.release.set 17
javadoc.options.encoding = 'UTF-8' javadoc.options.encoding = 'UTF-8'
javadoc.options.addStringOption('Xdoclint:none', '-quiet') javadoc.options.addStringOption('Xdoclint:none', '-quiet')
compileJava.options.release.set 16
repositories { repositories {
mavenLocal() mavenLocal()
mavenCentral() 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://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.minebench.de/' }
maven { url 'https://repo.alessiodp.com/releases/' } maven { url 'https://repo.alessiodp.com/releases/' }
maven { url 'https://jitpack.io' } maven { url 'https://jitpack.io' }
maven { url 'https://mvn-repo.arim.space/lesser-gpl3/' }
maven { url 'https://libraries.minecraft.net/' }
} }
dependencies { dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' testImplementation(platform("org.junit:junit-bom:6.0.1"))
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.8.2' testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testCompileOnly 'org.jetbrains:annotations:26.0.2-1'
}
license {
header = rootProject.file('HEADER')
include '**/*.java'
newLine = true
} }
test { test {
@@ -43,60 +106,128 @@ allprojects {
} }
processResources { processResources {
filter ReplaceTokens as Class, beginToken: '${', endToken: '}', def tokenMap = rootProject.ext.properties
tokens: rootProject.ext.properties tokenMap.merge("grgit", '', (s, s2) -> s)
filesMatching(['**/*.json', '**/*.yml']) {
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
tokens: tokenMap
}
} }
} }
subprojects { subprojects {
// Ignore parent projects (no jars)
if (['fabric', 'bukkit'].contains(project.name)) {
return
}
// Project naming
version rootProject.version 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)) { // Version-specific configuration
shadowJar { if (['fabric', 'bukkit'].contains(project.parent?.name)) {
destinationDirectory.set(file("$rootDir/target")) compileJava.options.release.set 21
archiveClassifier.set('') version += "+mc.${project.name}"
if (project.parent?.name?.equals('fabric')) {
apply plugin: 'dev.architectury.loom'
} }
}
// API publishing jar {
if ('api'.contains(project.name)) { from '../LICENSE'
java { }
withSourcesJar()
withJavadocJar()
}
sourcesJar {
destinationDirectory.set(file("$rootDir/target"))
}
javadocJar {
destinationDirectory.set(file("$rootDir/target"))
}
shadowJar.dependsOn(sourcesJar, javadocJar)
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 { publications {
mavenJava(MavenPublication) { mavenJavaCommon(MavenPublication) {
groupId = 'net.william278' groupId = 'net.william278.husksync'
artifactId = 'husksync' artifactId = 'husksync-common'
version = "$rootProject.version" version = "$rootProject.version"
artifact shadowJar artifact shadowJar
artifact javadocJar
artifact sourcesJar 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") logger.lifecycle("Building HuskSync ${version} by William278")
@SuppressWarnings('GrMethodMayBeStatic') @SuppressWarnings('GrMethodMayBeStatic')
def versionMetadata() { def versionMetadata() {
// Require grgit
if (grgit == null) { 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,4 @@
minecraft_version_range=1.21.1
minecraft_version_numeric=12101
minecraft_api_version=1.21
paper_api_version=1.21.1-R0.1-SNAPSHOT

View File

@@ -0,0 +1,4 @@
minecraft_version_range=>=1.21.9 <=1.21.10
minecraft_version_numeric=12110
minecraft_api_version=1.21
paper_api_version=1.21.10-R0.1-SNAPSHOT

View File

@@ -0,0 +1,4 @@
minecraft_version_range=>=1.21.11 <=1.21.11
minecraft_version_numeric=12111
minecraft_api_version=1.21
paper_api_version=1.21.11-R0.1-SNAPSHOT

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
minecraft_version_range=>=1.21.7 <=1.21.8
minecraft_version_numeric=12108
minecraft_api_version=1.21
paper_api_version=1.21.8-R0.1-SNAPSHOT

View File

@@ -1,29 +1,102 @@
plugins {
id 'java'
id 'net.william278.preprocessor' version '1.0'
id 'xyz.jpenilla.run-paper' version '2.3.1'
id 'maven-publish'
}
dependencies { dependencies {
implementation project(path: ':common') implementation project(path: ':common')
implementation 'org.bstats:bstats-bukkit:3.0.0'
implementation 'net.william278.uniform:uniform-bukkit:1.3.9'
implementation 'net.william278.uniform:uniform-paper:1.3.9'
implementation 'net.william278.toilet:toilet-bukkit:1.0.16'
implementation 'net.william278:mpdbdataconverter:1.0.1' implementation 'net.william278:mpdbdataconverter:1.0.1'
implementation 'net.william278:hsldataconverter:1.0' implementation 'net.william278:hsldataconverter:1.0'
implementation 'net.william278:mapdataapi:2.0'
implementation 'org.bstats:bstats-bukkit:3.1.0'
implementation 'net.kyori:adventure-platform-bukkit:4.4.1'
implementation 'dev.triumphteam:triumph-gui:3.1.12'
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
implementation 'de.tr7zw:item-nbt-api:2.15.5'
compileOnly 'redis.clients:jedis:4.2.3' compileOnly "io.papermc.paper:paper-api:${paper_api_version}"
compileOnly 'commons-io:commons-io:2.11.0' compileOnly 'com.github.retrooper:packetevents-spigot:2.10.1'
compileOnly 'de.themoep:minedown:1.7.1-SNAPSHOT' compileOnly 'com.github.dmulloy2:ProtocolLib:5.3.0'
compileOnly 'dev.dejvokep:boosted-yaml:1.2' compileOnly 'org.projectlombok:lombok:1.18.42'
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT' compileOnly 'commons-io:commons-io:2.21.0'
compileOnly 'com.zaxxer:HikariCP:5.0.1' compileOnly 'org.json:json:20250517'
compileOnly 'net.william278:minedown:1.8.2'
compileOnly 'de.exlll:configlib-yaml:4.6.4'
compileOnly 'com.zaxxer:HikariCP:7.0.2'
compileOnly 'net.william278:DesertWell:2.0.4'
compileOnly 'net.william278:AdvancementAPI:97a9583413'
compileOnly "redis.clients:jedis:$jedis_version"
annotationProcessor 'org.projectlombok:lombok:1.18.42'
}
processResources {
filesMatching(['**/*.json', '**/*.yml']) {
expand([
version: version,
paper_api_version: paper_api_version,
minecraft_version: project.name,
minecraft_version_range: minecraft_version_range,
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 { 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 'de.themoep', 'net.william278.husksync.libraries'
relocate 'org.jetbrains', 'net.william278.husksync.libraries' relocate 'org.jetbrains', 'net.william278.husksync.libraries'
relocate 'org.intellij', 'net.william278.husksync.libraries' relocate 'org.intellij', 'net.william278.husksync.libraries'
relocate 'com.zaxxer', 'net.william278.husksync.libraries' relocate 'com.zaxxer', 'net.william278.husksync.libraries'
relocate 'com.google', 'net.william278.husksync.libraries' relocate 'de.exlll', 'net.william278.husksync.libraries'
relocate 'redis.clients', 'net.william278.husksync.libraries' relocate 'net.william278.uniform', 'net.william278.husksync.libraries.uniform'
relocate 'org.json', 'net.william278.husksync.libraries.json' relocate 'net.william278.toilet', 'net.william278.husksync.libraries.toilet'
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby' relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats' relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter' relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter' relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
relocate 'org.json', 'net.william278.husksync.libraries.json'
relocate '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)
downloadPlugins {
github("plan-player-analytics", "Plan", "5.6.2965", "Plan-5.6-build-2965.jar")
}
}
} }

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,403 @@
/*
* 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; package net.william278.husksync;
import dev.dejvokep.boostedyaml.YamlDocument; import com.google.common.collect.Lists;
import dev.dejvokep.boostedyaml.dvs.versioning.BasicVersioning; import com.google.common.collect.Maps;
import dev.dejvokep.boostedyaml.settings.dumper.DumperSettings; import com.google.common.collect.Sets;
import dev.dejvokep.boostedyaml.settings.general.GeneralSettings; import com.google.gson.Gson;
import dev.dejvokep.boostedyaml.settings.loader.LoaderSettings; import de.tr7zw.changeme.nbtapi.NBT;
import dev.dejvokep.boostedyaml.settings.updater.UpdaterSettings; import lombok.AccessLevel;
import net.william278.husksync.command.BukkitCommand; import lombok.Getter;
import net.william278.husksync.command.BukkitCommandType; import lombok.NoArgsConstructor;
import net.william278.husksync.command.Permission; 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.Locales;
import net.william278.husksync.config.Server;
import net.william278.husksync.config.Settings; import net.william278.husksync.config.Settings;
import net.william278.husksync.data.CompressedDataAdapter; import net.william278.husksync.data.*;
import net.william278.husksync.data.DataAdapter;
import net.william278.husksync.data.JsonDataAdapter;
import net.william278.husksync.database.Database; import net.william278.husksync.database.Database;
import net.william278.husksync.database.MongoDbDatabase;
import net.william278.husksync.database.MySqlDatabase; import net.william278.husksync.database.MySqlDatabase;
import net.william278.husksync.editor.DataEditor; import net.william278.husksync.database.PostgresDatabase;
import net.william278.husksync.event.BukkitEventCannon; import net.william278.husksync.event.BukkitEventDispatcher;
import net.william278.husksync.event.EventCannon;
import net.william278.husksync.hook.PlanHook; import net.william278.husksync.hook.PlanHook;
import net.william278.husksync.listener.BukkitEventListener; import net.william278.husksync.listener.BukkitEventListener;
import net.william278.husksync.listener.EventListener; import net.william278.husksync.listener.LockedHandler;
import net.william278.husksync.maps.BukkitMapHandler;
import net.william278.husksync.migrator.LegacyMigrator; import net.william278.husksync.migrator.LegacyMigrator;
import net.william278.husksync.migrator.Migrator; import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.migrator.MpdbMigrator; 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.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.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.bstats.bukkit.Metrics;
import org.bukkit.Bukkit;
import org.bukkit.command.PluginCommand;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.permissions.PermissionDefault; import org.bukkit.map.MapView;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull; 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.nio.file.Path;
import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; 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>. * 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 int METRICS_ID = 13140;
private static final String PLATFORM_TYPE_ID = "bukkit";
private final HashMap<Identifier, Serializer<? extends Data>> serializers = Maps.newHashMap();
private final Map<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap();
private final Map<Integer, MapView> mapViews = Maps.newConcurrentMap();
private final List<Migrator> availableMigrators = Lists.newArrayList();
private final Set<UUID> lockedPlayers = Sets.newConcurrentHashSet();
private final Set<UUID> disconnectingPlayers = Sets.newConcurrentHashSet();
private boolean disabling;
private Gson gson;
private AudienceProvider audiences;
private Toilet toilet;
private MorePaperLib paperLib;
private Database database; private Database database;
private RedisManager redisManager; private RedisManager redisManager;
private Logger logger; private BukkitEventListener eventListener;
private ResourceReader resourceReader;
private EventListener eventListener;
private DataAdapter dataAdapter; private DataAdapter dataAdapter;
private DataEditor dataEditor; private DataSyncer dataSyncer;
private EventCannon eventCannon; private LegacyConverter legacyConverter;
private AsynchronousScheduler asyncScheduler;
private RegionalScheduler regionalScheduler;
@Setter
private Settings settings; private Settings settings;
@Setter
private Locales locales; private Locales locales;
private List<Migrator> availableMigrators; @Setter
private static BukkitHuskSync instance; @Getter(AccessLevel.NONE)
private Server serverName;
/**
* (<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;
}
@Override @Override
public void onLoad() { 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 @Override
public void onEnable() { public void onEnable() {
// Initialize HuskSync this.audiences = BukkitAudiences.create(this);
final AtomicBoolean initialized = new AtomicBoolean(true); this.toilet = BukkitToilet.create(getDumpOptions());
try {
// Set the logging adapter and resource reader
this.logger = new BukkitLogger(this.getLogger());
this.resourceReader = new BukkitResourceReader(this);
// Load settings and locales // Check compatibility
getLoggingAdapter().log(Level.INFO, "Loading plugin configuration settings & locales..."); checkCompatibility();
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");
} else {
throw new HuskSyncInitializationException("Failed to load plugin configuration settings and/or locales");
}
// Prepare data adapter // Preload NBT-API
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_COMPRESS_DATA)) { if (!NBT.preloadApi()) {
dataAdapter = new CompressedDataAdapter(); log(Level.SEVERE, "Failed to load NBT API. HuskSync will not be initialized!");
} else { return;
dataAdapter = new JsonDataAdapter();
}
// Prepare event cannon
eventCannon = new BukkitEventCannon();
// Prepare data editor
dataEditor = new DataEditor(locales);
// Prepare migrators
availableMigrators = new ArrayList<>();
availableMigrators.add(new LegacyMigrator(this));
final Plugin mySqlPlayerDataBridge = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
if (mySqlPlayerDataBridge != null) {
availableMigrators.add(new MpdbMigrator(this, mySqlPlayerDataBridge));
}
// 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");
}
// Prepare redis connection
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");
}
// Register events
getLoggingAdapter().log(Level.INFO, "Registering events...");
this.eventListener = new BukkitEventListener(this);
getLoggingAdapter().log(Level.INFO, "Successfully registered events listener");
// 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 commands
for (final BukkitCommandType bukkitCommandType : BukkitCommandType.values()) {
final PluginCommand pluginCommand = getCommand(bukkitCommandType.commandBase.command);
if (pluginCommand != null) {
new BukkitCommand(bukkitCommandType.commandBase, this).register(pluginCommand);
}
}
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!");
}
// 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);
}
} }
// 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 {
dataAdapter = new GsonAdapter(this);
}
});
// 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();
});
// Setup available migrators
initialize("data migrators/converters", (plugin) -> {
availableMigrators.add(new LegacyMigrator(this));
if (isDependencyLoaded("MySqlPlayerDataBridge")) {
availableMigrators.add(new MpdbMigrator(this));
}
legacyConverter = new BukkitLegacyConverter(this);
});
// 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
initialize("Redis server connection", (plugin) -> {
this.redisManager = new RedisManager(this);
this.redisManager.initialize();
});
// Prepare data syncer
initialize("data syncer", (plugin) -> {
dataSyncer = getSettings().getSynchronization().getMode().create(this);
dataSyncer.initialize();
});
// Register events
initialize("events", (plugin) -> eventListener.onEnable());
// Register plugin hooks
initialize("hooks", (plugin) -> {
if (isDependencyLoaded("Plan") && getSettings().isEnablePlanHook()) {
new PlanHook(this).hookIntoPlan();
}
});
// Register API
initialize("api", (plugin) -> BukkitHuskSyncAPI.register(this));
// Hook into bStats and check for updates
initialize("metrics", (plugin) -> this.registerMetrics(METRICS_ID));
this.checkForUpdates();
} }
@Override @Override
public void onDisable() { 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) { if (this.eventListener != null) {
this.eventListener.handlePluginDisable(); 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 @Override
public @NotNull Set<OnlineUser> getOnlineUsers() { @NotNull
return Bukkit.getOnlinePlayers().stream().map(BukkitPlayer::adapt).collect(Collectors.toSet()); public Set<OnlineUser> getOnlineUsers() {
return getServer().getOnlinePlayers().stream()
.map(player -> BukkitUser.adapt(player, this))
.collect(Collectors.toSet());
} }
@Override @Override
public @NotNull Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) { @NotNull
final Player player = Bukkit.getPlayer(uuid); public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
final Player player = getServer().getPlayer(uuid);
if (player == null) { if (player == null) {
return Optional.empty(); return Optional.empty();
} }
return Optional.of(BukkitPlayer.adapt(player)); return Optional.of(BukkitUser.adapt(player, this));
} }
@Override @Override
public @NotNull Database getDatabase() { public void setDataSyncer(@NotNull DataSyncer dataSyncer) {
return database; log(Level.INFO, String.format("Switching data syncer to %s", dataSyncer.getClass().getSimpleName()));
this.dataSyncer = dataSyncer;
} }
@Override @Override
public @NotNull RedisManager getRedisManager() { @NotNull
return redisManager; public Uniform getUniform() {
} return BukkitUniform.getInstance(this);
@Override
public @NotNull DataAdapter getDataAdapter() {
return dataAdapter;
}
@Override
public @NotNull DataEditor getDataEditor() {
return dataEditor;
}
@Override
public @NotNull EventCannon getEventCannon() {
return eventCannon;
} }
@NotNull @NotNull
@Override @Override
public List<Migrator> getAvailableMigrators() { public Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
return availableMigrators; return playerCustomDataStore.compute(
user.getUuid(),
(uuid, data) -> data == null ? Maps.newHashMap() : data
);
} }
@Override @Override
public @NotNull Settings getSettings() { @NotNull
return settings; public String getServerName() {
return serverName == null ? "server" : serverName.getName();
} }
@Override @Override
public @NotNull Locales getLocales() { public boolean isDependencyLoaded(@NotNull String name) {
return locales; 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 @Override
public @NotNull Logger getLoggingAdapter() { public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable) {
return logger; 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());
}
@NotNull
@Override
public String getPlatformType() {
return PLATFORM_TYPE_ID;
} }
@Override @Override
public @NotNull Version getPluginVersion() { @NotNull
return Version.pluginVersion(getDescription().getVersion()); public String getServerVersion() {
return String.format("%s/%s", getServer().getName(), getServer().getVersion());
} }
@Override @Override
public @NotNull Version getMinecraftVersion() { public Optional<LegacyConverter> getLegacyConverter() {
return Version.minecraftVersion(Bukkit.getBukkitVersion()); return Optional.of(legacyConverter);
} }
@Override @Override
public CompletableFuture<Boolean> reload() { @NotNull
return CompletableFuture.supplyAsync(() -> { public LockedHandler getLockedHandler() {
try { return eventListener.getLockedHandler();
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 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,90 @@
/*
* 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;
import java.util.stream.Stream;
@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", getMavenUrl()).build());
classpathBuilder.addLibrary(resolver);
}
@NotNull
private static String getMavenUrl() {
return Stream.of(
System.getenv("PAPER_DEFAULT_CENTRAL_REPOSITORY"),
System.getProperty("org.bukkit.plugin.java.LibraryLoader.centralURL"),
"https://maven-central.storage-download.googleapis.com/maven2"
).filter(Objects::nonNull).findFirst().orElseThrow(IllegalStateException::new);
}
@NotNull
private static List<String> resolveLibraries(@NotNull PluginClasspathBuilder classpathBuilder) {
try (InputStream input = getLibraryListFile()) {
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,880 @@
/*
* 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;
import org.bukkit.inventory.EquipmentSlotGroup;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.plugin.Plugin;
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 {
ItemStack[] fullContents = plugin.setMapViews(getContents());
ItemStack[] enderChestContents = Arrays.copyOf(fullContents, Math.min(fullContents.length, user.getPlayer().getEnderChest().getSize()));
user.getPlayer().getEnderChest().setContents(enderChestContents);
}
}
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) {
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 {
int stat = 0;
if (i instanceof Material mat && ((id.getType() == Statistic.Type.BLOCK && mat.isBlock())
|| (id.getType() == Statistic.Type.ITEM && mat.isItem()))) {
stat = p.getStatistic(id, mat);
} else if (i instanceof EntityType ent && id.getType() == Statistic.Type.ENTITY) {
stat = p.getStatistic(id, ent);
}
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 -> {
Material material = matchMaterial(key.length > 0 ? key[0] : null);
if (material != null) {
player.setStatistic(stat, material, value);
}
}
case ENTITY -> {
EntityType entity = matchEntityType(key.length > 0 ? key[0] : null);
if (entity != null) {
player.setStatistic(stat, entity, 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) {
if (!Bukkit.isPrimaryThread()) {
try {
return Bukkit.getScheduler().callSyncMethod((Plugin) plugin, () -> adapt(player, plugin)).get();
} catch (Exception e) {
throw new IllegalStateException("Failed to adapt attributes on main thread", e);
}
}
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()))
.filter(modifier -> modifier.getSlotGroup() != EquipmentSlotGroup.ANY)
.map(BukkitData.Attributes::adapt).collect(Collectors.toSet())
);
}
@NotNull
private static Modifier adapt(@NotNull AttributeModifier modifier) {
return new Modifier(
modifier.getKey().toString(),
modifier.getAmount(),
modifier.getOperation().ordinal(),
modifier.getSlotGroup().toString()
);
}
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) {
return new AttributeModifier(
Objects.requireNonNull(NamespacedKey.fromString(modifier.name())),
modifier.amount(),
AttributeModifier.Operation.values()[modifier.operation()],
Optional.ofNullable(EquipmentSlotGroup.getByName(modifier.slotGroup())).orElse(EquipmentSlotGroup.ANY)
);
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
if (!Bukkit.isPrimaryThread()) {
try {
Bukkit.getScheduler().callSyncMethod(plugin, () -> { this.apply(user, plugin); return null; }).get();
return;
} catch (Exception e) {
throw new IllegalStateException("Failed to apply attributes on main thread", e);
}
}
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; 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.inventory.ItemStack;
import org.bukkit.potion.PotionEffect; import org.jetbrains.annotations.ApiStatus;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
import java.io.ByteArrayInputStream; import java.util.List;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
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 { public class BukkitSerializer {
/** protected final HuskSync plugin;
* Returns a serialized array of {@link ItemStack}s
* @SuppressWarnings("unused")
* @param inventoryContents The contents of the inventory public BukkitSerializer(@NotNull HuskSyncAPI api) {
* @return The serialized inventory contents this.plugin = api.getPlugin();
*/ }
public static CompletableFuture<String> serializeItemStackArray(@NotNull ItemStack[] inventoryContents)
throws DataSerializationException { @ApiStatus.Internal
return CompletableFuture.supplyAsync(() -> { @NotNull
// Return an empty string if there is no inventory item data to serialize public HuskSync getPlugin() {
if (inventoryContents.length == 0) { return plugin;
return ""; }
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 @NotNull
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); private ItemStack @NotNull [] upgradeItemStacks(@NotNull NBTCompound itemsNbt, @NotNull Version mcVersion) {
final ReadWriteNBTCompoundList items = itemsNbt.getCompoundList("items");
try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) { final ItemStack[] itemStacks = new ItemStack[itemsNbt.getInteger("size")];
// Define the length of the inventory array to serialize for (int i = 0; i < items.size(); i++) {
bukkitOutputStream.writeInt(inventoryContents.length); if (items.get(i) == null) {
itemStacks[i] = new ItemStack(Material.AIR);
// Write each serialize each ItemStack to the output stream continue;
for (ItemStack inventoryItem : inventoryContents) {
bukkitOutputStream.writeObject(serializeItemStack(inventoryItem));
} }
try {
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion itemStacks[i] = NBT.itemStackFromNBT(upgradeItemData(items.get(i), mcVersion));
return Base64Coder.encodeLines(byteOutputStream.toByteArray()); } catch (Throwable e) {
} catch (IOException e) { itemStacks[i] = new ItemStack(Material.AIR);
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;
} }
} 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 * @deprecated Use {@link Serializer.Json} in the common module instead
*
* @param item The {@link ItemStack} to serialize
* @return The serialized {@link ItemStack}
*/ */
@Nullable @Deprecated(since = "2.6")
private static Map<String, Object> serializeItemStack(@Nullable ItemStack item) { public class Json<T extends Data & Adaptable> extends Serializer.Json<T> {
return item != null ? item.serialize() : null;
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,177 @@
/*
* 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 Optional.ofNullable(getCustomDataStore().get(id));
}
try {
return switch (id.getKeyValue()) {
case "inventory" -> getInventory();
case "ender_chest" -> getEnderChest();
case "potion_effects" -> getPotionEffects();
case "advancements" -> getAdvancements();
case "location" -> getLocation();
case "statistics" -> getStatistics();
case "health" -> getHealth();
case "hunger" -> getHunger();
case "attributes" -> getAttributes();
case "experience" -> getExperience();
case "game_mode" -> getGameMode();
case "flight_status" -> getFlightStatus();
case "persistent_data" -> getPersistentData();
default -> throw new IllegalStateException(String.format("Unexpected data type: %s", id));
};
} catch (Throwable e) {
getPlugin().debug("Failed to get data for key: " + id.asMinimalString(), e);
return Optional.empty();
}
}
@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; package net.william278.husksync.event;
import net.william278.husksync.data.DataSaveCause; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.UserData; import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.player.User; import net.william278.husksync.user.User;
import org.bukkit.event.Cancellable; import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList; import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@SuppressWarnings("unused")
public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, Cancellable { public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList(); private static final HandlerList HANDLER_LIST = new HandlerList();
private boolean cancelled = false; private final HuskSync plugin;
private UserData userData; private final DataSnapshot.Packed snapshot;
private final User user; private final User user;
private final DataSaveCause saveCause; private boolean cancelled = false;
protected BukkitDataSaveEvent(@NotNull User user, @NotNull UserData userData, protected BukkitDataSaveEvent(@NotNull User user, @NotNull DataSnapshot.Packed snapshot, @NotNull HuskSync plugin) {
@NotNull DataSaveCause saveCause) {
this.user = user; this.user = user;
this.userData = userData; this.snapshot = snapshot;
this.saveCause = saveCause; this.plugin = plugin;
} }
@Override @Override
@@ -38,18 +57,15 @@ public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, C
} }
@Override @Override
public @NotNull UserData getUserData() { @NotNull
return userData; public DataSnapshot.Packed getData() {
return snapshot;
} }
@NotNull
@Override @Override
public void setUserData(@NotNull UserData userData) { public HuskSync getPlugin() {
this.userData = userData; return plugin;
}
@Override
public @NotNull DataSaveCause getSaveCause() {
return saveCause;
} }
@NotNull @NotNull
@@ -57,4 +73,8 @@ public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, C
public HandlerList getHandlers() { public HandlerList getHandlers() {
return HANDLER_LIST; 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; package net.william278.husksync.event;
import net.william278.husksync.BukkitHuskSync;
import org.bukkit.Bukkit;
import org.bukkit.event.Event; 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 { public abstract class BukkitEvent extends Event implements net.william278.husksync.event.Event {
private static final HandlerList HANDLER_LIST = new HandlerList();
protected BukkitEvent() { protected BukkitEvent() {
} }
@NotNull
@Override @Override
public CompletableFuture<net.william278.husksync.event.Event> fire() { public HandlerList getHandlers() {
final CompletableFuture<net.william278.husksync.event.Event> eventFireFuture = new CompletableFuture<>(); return HANDLER_LIST;
// Don't fire events while the server is shutting down }
if (!BukkitHuskSync.getInstance().isEnabled()) {
eventFireFuture.complete(this); public static HandlerList getHandlerList() {
} else { return HANDLER_LIST;
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
Bukkit.getServer().getPluginManager().callEvent(this);
eventFireFuture.complete(this);
});
}
return eventFireFuture;
} }
} }

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; package net.william278.husksync.event;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.player.BukkitPlayer; import org.bukkit.event.HandlerList;
import net.william278.husksync.player.OnlineUser;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture; @SuppressWarnings("unused")
public abstract class BukkitPlayerEvent extends BukkitEvent implements PlayerEvent { 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; this.player = player;
} }
@Override @Override
@NotNull
public OnlineUser getUser() { public OnlineUser getUser() {
return BukkitPlayer.adapt(player); return player;
} }
@NotNull
@Override @Override
public CompletableFuture<Event> fire() { public HandlerList getHandlers() {
final CompletableFuture<Event> eventFireFuture = new CompletableFuture<>(); return HANDLER_LIST;
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
Bukkit.getServer().getPluginManager().callEvent(this);
eventFireFuture.complete(this);
});
return eventFireFuture;
} }
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; package net.william278.husksync.event;
import net.william278.husksync.data.UserData; import net.william278.husksync.HuskSync;
import org.bukkit.entity.Player; import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.OnlineUser;
import org.bukkit.event.Cancellable; import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList; import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@SuppressWarnings("unused")
public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEvent, Cancellable { public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEvent, Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList(); private static final HandlerList HANDLER_LIST = new HandlerList();
private final HuskSync plugin;
private final DataSnapshot.Packed data;
private boolean cancelled = false; 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); super(player);
this.userData = userData; this.data = data;
this.plugin = plugin;
} }
@Override @Override
@@ -27,13 +50,15 @@ public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEven
} }
@Override @Override
public @NotNull UserData getUserData() { @NotNull
return userData; public DataSnapshot.Packed getData() {
return data;
} }
@NotNull
@Override @Override
public void setUserData(@NotNull UserData userData) { public HuskSync getPlugin() {
this.userData = userData; return plugin;
} }
@NotNull @NotNull
@@ -41,4 +66,8 @@ public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEven
public HandlerList getHandlers() { public HandlerList getHandlers() {
return HANDLER_LIST; 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; 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.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@SuppressWarnings("unused")
public class BukkitSyncCompleteEvent extends BukkitPlayerEvent implements SyncCompleteEvent { public class BukkitSyncCompleteEvent extends BukkitPlayerEvent implements SyncCompleteEvent {
private static final HandlerList HANDLER_LIST = new HandlerList(); private static final HandlerList HANDLER_LIST = new HandlerList();
protected BukkitSyncCompleteEvent(@NotNull Player player) { protected BukkitSyncCompleteEvent(@NotNull OnlineUser player, @NotNull HuskSync plugin) {
super(player); super(player);
} }
@@ -16,4 +37,8 @@ public class BukkitSyncCompleteEvent extends BukkitPlayerEvent implements SyncCo
public HandlerList getHandlers() { public HandlerList getHandlers() {
return HANDLER_LIST; 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; package net.william278.husksync.listener;
import lombok.Getter;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.data.BukkitSerializer; import net.william278.husksync.data.BukkitData;
import net.william278.husksync.data.DataSerializationException; import net.william278.husksync.user.BukkitUser;
import net.william278.husksync.data.ItemData; import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.editor.ItemEditorMenuType;
import net.william278.husksync.player.BukkitPlayer;
import net.william278.husksync.player.OnlineUser;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; 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.entity.PlayerDeathEvent;
import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.server.MapInitializeEvent;
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.world.WorldSaveEvent; import org.bukkit.event.world.WorldSaveEvent;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.stream.Collectors; 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) { protected LockedHandler lockedHandler;
super(huskSync);
Bukkit.getServer().getPluginManager().registerEvents(this, huskSync); public BukkitEventListener(@NotNull BukkitHuskSync plugin) {
super(plugin);
} }
@EventHandler(priority = EventPriority.LOWEST) public void onLoad() {
public void onPlayerJoin(@NotNull PlayerJoinEvent event) { this.lockedHandler = createLockedHandler((BukkitHuskSync) plugin);
super.handlePlayerJoin(BukkitPlayer.adapt(event.getPlayer()));
} }
@EventHandler(priority = EventPriority.LOWEST) public void onEnable() {
public void onPlayerQuit(@NotNull PlayerQuitEvent event) { getPlugin().getServer().getPluginManager().registerEvents(this, getPlugin());
super.handlePlayerQuit(BukkitPlayer.adapt(event.getPlayer())); 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) @EventHandler(ignoreCancelled = true)
public void onWorldSave(@NotNull WorldSaveEvent event) { public void onWorldSave(@NotNull WorldSaveEvent event) {
CompletableFuture.runAsync(() -> super.handleAsyncWorldSave(event.getWorld().getPlayers().stream() if (!plugin.getSettings().getSynchronization().isSaveOnWorldSave()) {
.map(BukkitPlayer::adapt).collect(Collectors.toList()))); 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) @EventHandler(ignoreCancelled = true)
public void onInventoryClose(@NotNull InventoryCloseEvent event) { public void onMapInitialize(@NotNull MapInitializeEvent event) {
CompletableFuture.runAsync(() -> { if (plugin.getSettings().getSynchronization().isPersistLockedMaps() && event.getMap().isLocked()) {
if (event.getPlayer() instanceof Player player) { getPlugin().runAsync(() -> ((BukkitHuskSync) plugin).renderInitializingLockedMap(event.getMap()));
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)));
} }
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) // We handle commands here to allow specific command handling on ProtocolLib servers
public void onInventoryOpen(@NotNull InventoryOpenEvent event) { @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
if (event.getPlayer() instanceof Player player) { public void onCommandProcessed(@NotNull PlayerCommandPreprocessEvent event) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(player))); if (!lockedHandler.isCommandDisabled(event.getMessage().substring(1).split(" ")[0])) {
return;
}
if (lockedHandler.cancelPlayerEvent(event.getPlayer().getUniqueId())) {
event.setCancelled(true);
} }
} }
@EventHandler(ignoreCancelled = true) @NotNull
public void onPlayerDeath(PlayerDeathEvent event) { @Override
if (cancelPlayerEvent(BukkitPlayer.adapt(event.getEntity()))) { public BukkitHuskSync getPlugin() {
event.getDrops().clear(); 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,129 @@
/*
* 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.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);
}
}
}

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,106 @@
/*
* 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
@SuppressWarnings("RedundantMethodOverride")
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,620 @@
/*
* 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.ReadableItemNBT;
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.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.List;
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";
// The legacy map key used to store pixel data (3.7.3 and below)
String MAP_LEGACY_PIXEL_DATA_KEY = "husksync:canvas_data";
// Name of server the map originates from
String MAP_ORIGIN_KEY = "origin";
// Original map id
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
&& !box.getInventory().isEmpty()) {
forEachMap(box.getInventory().getContents(), function);
b.setBlockState(box);
item.setItemMeta(b);
} else if (item.getItemMeta() instanceof BundleMeta bundle && bundle.hasItems()) {
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 MapData readMapData(@NotNull String serverName, int mapId) {
final byte[] readData = fetchMapData(serverName, mapId);
if (readData == null) {
return null;
}
return deserializeMapData(readData);
}
@Nullable
@Blocking
private byte[] fetchMapData(@NotNull String serverName, int mapId) {
return fetchMapData(serverName, mapId, true);
}
@Nullable
@Blocking
private byte[] fetchMapData(@NotNull String serverName, int mapId, boolean doReverseLookup) {
// Read from Redis cache
final byte[] redisData = getRedisManager().getMapData(serverName, mapId);
if (redisData != null) {
return redisData;
}
// Read from database and set to Redis
final byte[] databaseData = getPlugin().getDatabase().getMapData(serverName, mapId);
if (databaseData != null) {
getRedisManager().setMapData(serverName, mapId, databaseData);
return databaseData;
}
// Otherwise, lookup a reverse map binding
if (doReverseLookup) {
return fetchReversedMapData(serverName, mapId);
}
return null;
}
@Nullable
private byte[] 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 MapData deserializeMapData(byte @NotNull [] data) {
try {
return getPlugin().getDataAdapter().fromBytes(data, AdaptableMapData.class)
.getData(getPlugin().getDataVersion(getPlugin().getMinecraftVersion()));
} 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;
}
// Server the map was originally created on, and the current server. If they match, isOrigin is true.
final String originServer = mapData.getString(MAP_ORIGIN_KEY);
final String currentServer = getPlugin().getServerName();
final boolean isOrigin = currentServer.equals(originServer);
// Determine the map's ID on its origin server, and the new ID it should be bound to here.
// Then, update the map item / data accordingly (re-rendering and caching the map if needed)
final int originalId = mapData.getInteger(MAP_ID_KEY);
int newId = isOrigin ? originalId : getBoundMapId(originServer, originalId, currentServer);
if (newId != -1) {
handleBoundMap(meta, nbt, originServer, originalId, newId, isOrigin);
} else {
handleUnboundMap(meta, nbt, originServer, originalId, currentServer);
}
map.setItemMeta(meta);
});
return map;
}
private void handleBoundMap(@NotNull MapMeta meta, @NotNull ReadableItemNBT nbt, @NotNull String originServer,
int originalId, int newId, boolean isOrigin) {
MapView view = Bukkit.getMap(newId);
if (isOrigin && view != null) {
meta.setMapView(view);
getPlugin().debug("Map ID set to original ID #%s".formatted(newId));
return;
}
Optional<MapView> optionalView = getMapView(newId);
if (optionalView.isPresent()) {
meta.setMapView(optionalView.get());
getPlugin().debug("Map ID set to #%s".formatted(newId));
return;
}
getPlugin().debug("Deserializing map data from NBT and generating view...");
MapData mapData = readMapData(originServer, originalId);
if (mapData == null && nbt.hasTag(MAP_LEGACY_PIXEL_DATA_KEY)) {
mapData = readLegacyMapItemData(nbt);
}
if (mapData == null) {
getPlugin().debug("Read pixel data was not found in database, skipping...");
return;
}
MapView newView = view != null ? view : Bukkit.createMap(getDefaultMapWorld());
generateRenderedMap(mapData, newView);
meta.setMapView(newView);
}
private void handleUnboundMap(@NotNull MapMeta meta, @NotNull ReadableItemNBT nbt, @NotNull String originServer,
int originalId, @NotNull String currentServer) {
getPlugin().debug("Deserializing map data from NBT and generating view...");
MapData mapData = readMapData(originServer, originalId);
if (mapData == null && nbt.hasTag(MAP_LEGACY_PIXEL_DATA_KEY)) {
mapData = readLegacyMapItemData(nbt);
}
if (mapData == null) {
getPlugin().debug("Read pixel data was not found in database, skipping...");
return;
}
final MapView view = generateRenderedMap(Objects.requireNonNull(mapData, "Pixel data null!"));
meta.setMapView(view);
final int id = view.getId();
getRedisManager().bindMapIds(originServer, originalId, currentServer, id);
getPlugin().getDatabase().setMapBinding(originServer, originalId, currentServer, id);
getPlugin().debug("Bound map to view (#%s) on server %s".formatted(id, currentServer));
}
// Render a persisted locked map that is initializing (i.e. in an item frame)
default void renderInitializingLockedMap(@NotNull MapView view) {
if (view.isVirtual()) {
return;
}
final Optional<MapView> optionalView = getMapView(view.getId());
if (optionalView.isPresent()) {
view.getRenderers().clear();
view.getRenderers().addAll(optionalView.get().getRenderers());
view.setLocked(true);
view.setScale(MapView.Scale.CLOSEST);
view.setTrackingPosition(false);
view.setUnlimitedTracking(false);
return;
}
MapData data = readMapData(getPlugin().getServerName(), view.getId());
if (data == null) {
data = readLegacyMapFileData(view.getId());
}
if (data == null) {
World world = view.getWorld() == null ? getDefaultMapWorld() : view.getWorld();
getPlugin().debug("Not rendering map: no data in DB for world %s, map #%s."
.formatted(world.getName(), view.getId()));
return;
}
renderMapView(view, data);
}
@NotNull
private MapView generateRenderedMap(@NotNull MapData canvasData) {
return generateRenderedMap(canvasData, Bukkit.createMap(getDefaultMapWorld()));
}
@NotNull
private MapView generateRenderedMap(@NotNull MapData canvasData, @NotNull MapView view) {
renderMapView(view, canvasData);
return view;
}
private void renderMapView(@NotNull MapView view, @NotNull MapData canvasData) {
view.getRenderers().clear();
view.addRenderer(new PersistentMapRenderer(canvasData));
view.setLocked(true);
view.setScale(MapView.Scale.CLOSEST);
view.setTrackingPosition(false);
view.setUnlimitedTracking(false);
setMapView(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()
);
}
// Legacy - read maps from item stacks
@Nullable
@Blocking
private MapData readLegacyMapItemData(@NotNull ReadableItemNBT nbt) {
final int dataVer = getPlugin().getDataVersion(getPlugin().getMinecraftVersion());
try {
return MapData.fromByteArray(dataVer,
Objects.requireNonNull(nbt.getByteArray(MAP_LEGACY_PIXEL_DATA_KEY)));
} catch (IOException e) {
getPlugin().log(Level.WARNING, "Failed to read legacy map data", e);
return null;
}
}
// Legacy - read maps from files
@Nullable
private MapData readLegacyMapFileData(int mapId) {
final Path path = getPlugin().getDataFolder().toPath().resolve("maps").resolve(mapId + ".dat");
final File file = path.toFile();
if (!file.exists()) {
return null;
}
try {
return MapData.fromNbt(file);
} catch (IOException e) {
getPlugin().log(Level.WARNING, "Failed to read legacy map file", e);
return null;
}
}
/**
* A {@link MapCanvas} implementation used for pre-rendering maps to be converted into {@link MapData}
*/
@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);
final String type = cursor.getType().getKey().getKey();
if (type.startsWith(BANNER_PREFIX)) {
banners.add(new MapBanner(
type.replaceAll(BANNER_PREFIX, ""),
cursor.getCaption() == null ? "" : cursor.getCaption(),
cursor.getX(),
mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128,
cursor.getY()
));
}
}
return MapData.fromPixels(mapDataVersion, pixels, getDimension(), (byte) 0, 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; package net.william278.husksync.migrator;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import me.william278.husksync.bukkit.data.DataSerializer; import me.william278.husksync.bukkit.data.DataSerializer;
import net.william278.hslmigrator.HSLConverter; import net.william278.hslmigrator.HSLConverter;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings; import net.william278.husksync.data.BukkitData;
import net.william278.husksync.data.*; import net.william278.husksync.data.Data;
import net.william278.husksync.player.User; 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.Material;
import org.bukkit.Statistic; import org.bukkit.Statistic;
import org.bukkit.entity.EntityType; import org.bukkit.entity.EntityType;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.sql.Connection; import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static net.william278.husksync.config.Settings.DatabaseSettings;
public class LegacyMigrator extends Migrator { public class LegacyMigrator extends Migrator {
private final HSLConverter hslConverter; private final HSLConverter hslConverter;
@@ -32,50 +57,50 @@ public class LegacyMigrator extends Migrator {
private String sourcePlayersTable; private String sourcePlayersTable;
private String sourceDataTable; private String sourceDataTable;
private final String minecraftVersion;
public LegacyMigrator(@NotNull HuskSync plugin) { public LegacyMigrator(@NotNull HuskSync plugin) {
super(plugin); super(plugin);
this.hslConverter = HSLConverter.getInstance(); this.hslConverter = HSLConverter.getInstance();
this.sourceHost = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_HOST);
this.sourcePort = plugin.getSettings().getIntegerValue(Settings.ConfigOption.DATABASE_PORT); final DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
this.sourceUsername = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_USERNAME); this.sourceHost = credentials.getHost();
this.sourcePassword = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_PASSWORD); this.sourcePort = credentials.getPort();
this.sourceDatabase = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_NAME); this.sourceUsername = credentials.getUsername();
this.sourcePassword = credentials.getPassword();
this.sourceDatabase = credentials.getDatabase();
this.sourcePlayersTable = "husksync_players"; this.sourcePlayersTable = "husksync_players";
this.sourceDataTable = "husksync_data"; this.sourceDataTable = "husksync_data";
this.minecraftVersion = plugin.getMinecraftVersion().toString();
} }
@Override @Override
public CompletableFuture<Boolean> start() { 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(); final long startTime = System.currentTimeMillis();
return CompletableFuture.supplyAsync(() -> { return plugin.supplyAsync(() -> {
// Wipe the existing database, preparing it for data import // Wipe the existing database, preparing it for data import
plugin.getLoggingAdapter().log(Level.INFO, "Preparing existing database (wiping)..."); plugin.log(Level.INFO, "Preparing existing database (wiping)...");
plugin.getDatabase().wipeDatabase().join(); plugin.getDatabase().wipeDatabase();
plugin.getLoggingAdapter().log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)"); plugin.log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
// Create jdbc driver connection url // Create jdbc driver connection url
final String jdbcUrl = "jdbc:mysql://" + sourceHost + ":" + sourcePort + "/" + sourceDatabase; final String jdbcUrl = "jdbc:mysql://" + sourceHost + ":" + sourcePort + "/" + sourceDatabase;
// Create a new data source for the mpdb converter // Create a new data source for the mpdb converter
try (final HikariDataSource connectionPool = new HikariDataSource()) { 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.setJdbcUrl(jdbcUrl);
connectionPool.setUsername(sourceUsername); connectionPool.setUsername(sourceUsername);
connectionPool.setPassword(sourcePassword); 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..."); plugin.log(Level.INFO, "Downloading raw data from the legacy database (this might take a while)...");
final List<LegacyData> dataToMigrate = new ArrayList<>(); final List<LegacyData> dataToMigrate = Lists.newArrayList();
try (final Connection connection = connectionPool.getConnection()) { try (final Connection connection = connectionPool.getConnection()) {
try (final PreparedStatement statement = connection.prepareStatement(""" 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` 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%` FROM `%source_players_table%`
INNER JOIN `%source_data_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_players_table%"), sourcePlayersTable)
.replaceAll(Pattern.quote("%source_data_table%"), sourceDataTable))) { .replaceAll(Pattern.quote("%source_data_table%"), sourceDataTable))) {
try (final ResultSet resultSet = statement.executeQuery()) { try (final ResultSet resultSet = statement.executeQuery()) {
@@ -104,26 +129,36 @@ public class LegacyMigrator extends Migrator {
resultSet.getString("location") resultSet.getString("location")
)); ));
playersMigrated++; playersMigrated++;
if (playersMigrated % 25 == 0) { if (playersMigrated % 50 == 0) {
plugin.getLoggingAdapter().log(Level.INFO, "Downloaded legacy data for " + playersMigrated + " players..."); 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.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..."); plugin.log(Level.INFO, "Converting HuskSync 1.x data to the new user data format (this might take a while)...");
dataToMigrate.forEach(data -> data.toUserData(hslConverter, minecraftVersion).thenAccept(convertedData ->
plugin.getDatabase().ensureUser(data.user()).thenRun(() -> final AtomicInteger playersConverted = new AtomicInteger();
plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.LEGACY_MIGRATION) dataToMigrate.forEach(data -> {
.exceptionally(exception -> { final DataSnapshot.Packed convertedData = data.toUserData(hslConverter, plugin);
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().username + ": " + exception.getMessage()); plugin.getDatabase().ensureUser(data.user());
return null; try {
})))); plugin.getDatabase().addSnapshot(data.user(), convertedData);
plugin.getLoggingAdapter().log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!"); } 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; return true;
} catch (Exception e) { } catch (Throwable e) {
plugin.getLoggingAdapter().log(Level.SEVERE, "Error while migrating legacy data: " + e.getMessage() + " - are your source database credentials correct?"); plugin.log(Level.SEVERE, "Error while migrating legacy data: " + e.getMessage() + " - are your source database credentials correct?", e);
return false; return false;
} }
}); });
@@ -132,7 +167,7 @@ public class LegacyMigrator extends Migrator {
@Override @Override
public void handleConfigurationCommand(@NotNull String[] args) { public void handleConfigurationCommand(@NotNull String[] args) {
if (args.length == 2) { if (args.length == 2) {
if (switch (args[0].toLowerCase()) { if (switch (args[0].toLowerCase(Locale.ENGLISH)) {
case "host" -> { case "host" -> {
this.sourceHost = args[1]; this.sourceHost = args[1];
yield true; yield true;
@@ -167,15 +202,15 @@ public class LegacyMigrator extends Migrator {
} }
default -> false; default -> false;
}) { }) {
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu()); plugin.log(Level.INFO, getHelpMenu());
plugin.getLoggingAdapter().log(Level.INFO, "Successfully set " + args[0] + " to " + plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1])); obfuscateDataString(args[1]));
} else { } else {
plugin.getLoggingAdapter().log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " + plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
obfuscateDataString(args[1]) + " (is it a valid option?)"); obfuscateDataString(args[1]) + " (is it a valid option?)");
} }
} else { } else {
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu()); plugin.log(Level.INFO, getHelpMenu());
} }
} }
@@ -188,22 +223,22 @@ public class LegacyMigrator extends Migrator {
@NotNull @NotNull
@Override @Override
public String getName() { public String getName() {
return "HuskSync v1.x --> v2.x Migrator"; return "HuskSync v1.x --> v3.x Migrator";
} }
@NotNull @NotNull
@Override @Override
public String getHelpMenu() { public String getHelpMenu() {
return """ 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 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. please follow the steps below carefully.
[!] Existing data in the database will be wiped. [!] [!] Existing data in the database will be wiped. [!]
STEP 1] Please ensure no players are on any servers. STEP 1] Please ensure no players are on any servers.
STEP 2] HuskSync will need to connect to the database STEP 2] HuskSync will need to connect to the database
used to hold the existing, legacy HuskSync data. used to hold the existing, legacy HuskSync data.
If this is the same database as the one you are If this is the same database as the one you are
@@ -223,12 +258,12 @@ public class LegacyMigrator extends Migrator {
using the command: using the command:
"husksync migrate legacy set <parameter> <value>" "husksync migrate legacy set <parameter> <value>"
(e.g.: "husksync migrate legacy set host 1.2.3.4") (e.g.: "husksync migrate legacy set host 1.2.3.4")
STEP 3] HuskSync will migrate data into the database STEP 3] HuskSync will migrate data into the database
tables configures in the config.yml file of this tables configures in the config.yml file of this
server. Please make sure you're happy with this server. Please make sure you're happy with this
before proceeding. before proceeding.
STEP 4] To start the migration, please run: STEP 4] To start the migration, please run:
"husksync migrate legacy start" "husksync migrate legacy start"
""".replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost)) """.replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost))
@@ -249,72 +284,90 @@ public class LegacyMigrator extends Migrator {
@NotNull String serializedAdvancements, @NotNull String serializedLocation) { @NotNull String serializedAdvancements, @NotNull String serializedLocation) {
@NotNull @NotNull
public CompletableFuture<UserData> toUserData(@NotNull HSLConverter converter, public DataSnapshot.Packed toUserData(@NotNull HSLConverter converter, @NotNull HuskSync plugin) {
@NotNull String minecraftVersion) { try {
return CompletableFuture.supplyAsync(() -> { final DataSerializer.StatisticData stats = converter.deserializeStatisticData(serializedStatistics);
try { final DataSerializer.PlayerLocation loc = converter.deserializePlayerLocationData(serializedLocation);
final DataSerializer.StatisticData legacyStatisticData = converter final BukkitLegacyConverter adapter = (BukkitLegacyConverter) plugin.getLegacyConverter()
.deserializeStatisticData(serializedStatistics); .orElseThrow(() -> new IllegalStateException("Legacy converter not present"));
final StatisticsData convertedStatisticData = new StatisticsData(
convertStatisticMap(legacyStatisticData.untypedStatisticValues()),
convertMaterialStatisticMap(legacyStatisticData.blockStatisticValues()),
convertMaterialStatisticMap(legacyStatisticData.itemStatisticValues()),
convertEntityStatisticMap(legacyStatisticData.entityStatisticValues()));
final List<AdvancementData> convertedAdvancements = converter return DataSnapshot.builder(plugin)
.deserializeAdvancementData(serializedAdvancements) // Inventory
.stream().map(data -> new AdvancementData(data.key(), data.criteriaMap())).toList(); .inventory(BukkitData.Items.Inventory.from(
adapter.deserializeLegacyItemStacks(serializedInventory),
selectedSlot
))
final DataSerializer.PlayerLocation legacyLocationData = converter // Ender chest
.deserializePlayerLocationData(serializedLocation); .enderChest(BukkitData.Items.EnderChest.adapt(
final LocationData convertedLocationData = new LocationData( adapter.deserializeLegacyItemStacks(serializedEnderChest)
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());
return new UserData(new StatusData(health, maxHealth, healthScale, hunger, saturation, // Location
saturationExhaustion, selectedSlot, totalExp, expLevel, expProgress, gameMode, isFlying), .location(BukkitData.Location.from(
new ItemData(serializedInventory), new ItemData(serializedEnderChest), loc == null ? 0d : loc.x(),
new PotionEffectData(serializedPotionEffects), convertedAdvancements, loc == null ? 64d : loc.y(),
convertedStatisticData, convertedLocationData, loc == null ? 0d : loc.z(),
new PersistentDataContainerData(new HashMap<>()), loc == null ? 90f : loc.yaw(),
minecraftVersion); loc == null ? 180f : loc.pitch(),
} catch (IOException e) { new Data.Location.World(
throw new RuntimeException(e); 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) { 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()) { 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; return convertedMap;
} }
private Map<String, Map<String, Integer>> convertMaterialStatisticMap(@NotNull HashMap<Statistic, HashMap<Material, Integer>> rawMap) { 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<Statistic, HashMap<Material, Integer>> entry : rawMap.entrySet()) {
for (Map.Entry<Material, Integer> materialEntry : entry.getValue().entrySet()) { for (Map.Entry<Material, Integer> materialEntry : entry.getValue().entrySet()) {
convertedMap.computeIfAbsent(entry.getKey().toString(), k -> new HashMap<>()) convertedMap.computeIfAbsent(entry.getKey().getKey().toString(), k -> new HashMap<>())
.put(materialEntry.getKey().toString(), materialEntry.getValue()); .put(materialEntry.getKey().getKey().toString(), materialEntry.getValue());
} }
} }
return convertedMap; return convertedMap;
} }
private Map<String, Map<String, Integer>> convertEntityStatisticMap(@NotNull HashMap<Statistic, HashMap<EntityType, Integer>> rawMap) { 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<Statistic, HashMap<EntityType, Integer>> entry : rawMap.entrySet()) {
for (Map.Entry<EntityType, Integer> materialEntry : entry.getValue().entrySet()) { for (Map.Entry<EntityType, Integer> materialEntry : entry.getValue().entrySet()) {
convertedMap.computeIfAbsent(entry.getKey().toString(), k -> new HashMap<>()) convertedMap.computeIfAbsent(entry.getKey().getKey().toString(), k -> new HashMap<>())
.put(materialEntry.getKey().toString(), materialEntry.getValue()); .put(materialEntry.getKey().getKey().toString(), materialEntry.getValue());
} }
} }
return convertedMap; 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; package net.william278.husksync.migrator;
import com.google.common.collect.Lists;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.config.Settings; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.*; import net.william278.husksync.data.BukkitData;
import net.william278.husksync.player.User; import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import net.william278.mpdbconverter.MPDBConverter; import net.william278.mpdbconverter.MPDBConverter;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory; import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.sql.Connection; import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; 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.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.regex.Pattern; 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 { public class MpdbMigrator extends Migrator {
@@ -35,46 +61,48 @@ public class MpdbMigrator extends Migrator {
private String sourceInventoryTable; private String sourceInventoryTable;
private String sourceEnderChestTable; private String sourceEnderChestTable;
private String sourceExperienceTable; private String sourceExperienceTable;
private final String minecraftVersion;
public MpdbMigrator(@NotNull BukkitHuskSync plugin, @NotNull Plugin mySqlPlayerDataBridge) { public MpdbMigrator(@NotNull BukkitHuskSync plugin) {
super(plugin); super(plugin);
this.mpdbConverter = MPDBConverter.getInstance(mySqlPlayerDataBridge); this.mpdbConverter = MPDBConverter.getInstance(Objects.requireNonNull(
this.sourceHost = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_HOST); Bukkit.getPluginManager().getPlugin("MySQLPlayerDataBridge"),
this.sourcePort = plugin.getSettings().getIntegerValue(Settings.ConfigOption.DATABASE_PORT); "MySQLPlayerDataBridge dependency not found!"
this.sourceUsername = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_USERNAME); ));
this.sourcePassword = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_PASSWORD); final DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
this.sourceDatabase = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_NAME); 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.sourceInventoryTable = "mpdb_inventory";
this.sourceEnderChestTable = "mpdb_enderchest"; this.sourceEnderChestTable = "mpdb_enderchest";
this.sourceExperienceTable = "mpdb_experience"; this.sourceExperienceTable = "mpdb_experience";
this.minecraftVersion = plugin.getMinecraftVersion().toString();
} }
@Override @Override
public CompletableFuture<Boolean> start() { 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(); final long startTime = System.currentTimeMillis();
return CompletableFuture.supplyAsync(() -> { return plugin.supplyAsync(() -> {
// Wipe the existing database, preparing it for data import // Wipe the existing database, preparing it for data import
plugin.getLoggingAdapter().log(Level.INFO, "Preparing existing database (wiping)..."); plugin.log(Level.INFO, "Preparing existing database (wiping)...");
plugin.getDatabase().wipeDatabase().join(); plugin.getDatabase().wipeDatabase();
plugin.getLoggingAdapter().log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)"); plugin.log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
// Create jdbc driver connection url // Create jdbc driver connection url
final String jdbcUrl = "jdbc:mysql://" + sourceHost + ":" + sourcePort + "/" + sourceDatabase; final String jdbcUrl = "jdbc:mysql://" + sourceHost + ":" + sourcePort + "/" + sourceDatabase;
// Create a new data source for the mpdb converter // Create a new data source for the mpdb converter
try (final HikariDataSource connectionPool = new HikariDataSource()) { 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.setJdbcUrl(jdbcUrl);
connectionPool.setUsername(sourceUsername); connectionPool.setUsername(sourceUsername);
connectionPool.setPassword(sourcePassword); 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..."); plugin.log(Level.INFO, "Downloading raw data from the MySQLPlayerDataBridge database (this might take a while)...");
final List<MpdbData> dataToMigrate = new ArrayList<>(); final List<MpdbData> dataToMigrate = Lists.newArrayList();
try (final Connection connection = connectionPool.getConnection()) { try (final Connection connection = connectionPool.getConnection()) {
try (final PreparedStatement statement = connection.prepareStatement(""" 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` 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++; playersMigrated++;
if (playersMigrated % 25 == 0) { 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.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..."); plugin.log(Level.INFO, "Converting raw MySQLPlayerDataBridge data to HuskSync user data (this might take a while)...");
dataToMigrate.forEach(data -> data.toUserData(mpdbConverter, minecraftVersion).thenAccept(convertedData ->
plugin.getDatabase().ensureUser(data.user()).thenRun(() -> final AtomicInteger playersConverted = new AtomicInteger();
plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.MPDB_MIGRATION)) dataToMigrate.forEach(data -> {
.exceptionally(exception -> { final DataSnapshot.Packed convertedData = data.toUserData(mpdbConverter, plugin);
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate MySQLPlayerDataBridge data for " + data.user().username + ": " + exception.getMessage()); plugin.getDatabase().ensureUser(data.user());
return null; plugin.getDatabase().addSnapshot(data.user(), convertedData);
}))); playersConverted.getAndIncrement();
plugin.getLoggingAdapter().log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!"); 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; return true;
} catch (Exception e) { } catch (Throwable e) {
plugin.getLoggingAdapter().log(Level.SEVERE, "Error while migrating data: " + e.getMessage() + " - are your source database credentials correct?"); plugin.log(Level.SEVERE, "Error while migrating data: " + e.getMessage() + " - are your source database credentials correct?");
return false; return false;
} }
}); });
@@ -128,7 +160,7 @@ public class MpdbMigrator extends Migrator {
@Override @Override
public void handleConfigurationCommand(@NotNull String[] args) { public void handleConfigurationCommand(@NotNull String[] args) {
if (args.length == 2) { if (args.length == 2) {
if (switch (args[0].toLowerCase()) { if (switch (args[0].toLowerCase(Locale.ENGLISH)) {
case "host" -> { case "host" -> {
this.sourceHost = args[1]; this.sourceHost = args[1];
yield true; yield true;
@@ -167,15 +199,15 @@ public class MpdbMigrator extends Migrator {
} }
default -> false; default -> false;
}) { }) {
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu()); plugin.log(Level.INFO, getHelpMenu());
plugin.getLoggingAdapter().log(Level.INFO, "Successfully set " + args[0] + " to " + plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1])); obfuscateDataString(args[1]));
} else { } else {
plugin.getLoggingAdapter().log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " + plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
obfuscateDataString(args[1]) + " (is it a valid option?)"); obfuscateDataString(args[1]) + " (is it a valid option?)");
} }
} else { } else {
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu()); plugin.log(Level.INFO, getHelpMenu());
} }
} }
@@ -196,16 +228,19 @@ public class MpdbMigrator extends Migrator {
public String getHelpMenu() { public String getHelpMenu() {
return """ return """
=== MySQLPlayerDataBridge Migration Wizard ========== === MySQLPlayerDataBridge Migration Wizard ==========
NOTE: This migrator currently WORKS WITH MPDB version
v4.9.2 and below!
This will migrate inventories, ender chests and XP This will migrate inventories, ender chests and XP
from the MySQLPlayerDataBridge plugin to HuskSync. from the MySQLPlayerDataBridge plugin to HuskSync.
To prevent excessive migration times, other non-vital To prevent excessive migration times, other non-vital
data will not be transferred. data will not be transferred.
[!] Existing data in the database will be wiped. [!] [!] Existing data in the database will be wiped. [!]
STEP 1] Please ensure no players are on any servers. STEP 1] Please ensure no players are on any servers.
STEP 2] HuskSync will need to connect to the database STEP 2] HuskSync will need to connect to the database
used to hold the source MySQLPlayerDataBridge data. used to hold the source MySQLPlayerDataBridge data.
Please check these database parameters are OK: 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 If any of these are not correct, please correct them
using the command: using the command:
"husksync migrate mpdb set <parameter> <value>" "husksync migrate mpdb set <parameter> <value>"
(e.g.: "husksync migrate mpdb set host 1.2.3.4") (e.g.: "husksync migrate set mpdb host 1.2.3.4")
STEP 3] HuskSync will migrate data into the database STEP 3] HuskSync will migrate data into the database
tables configures in the config.yml file of this tables configures in the config.yml file of this
server. Please make sure you're happy with this server. Please make sure you're happy with this
before proceeding. before proceeding.
STEP 4] To start the migration, please run: STEP 4] To start the migration, please run:
"husksync migrate mpdb start" "husksync migrate start mpdb"
NOTE: This migrator currently WORKS WITH MPDB version
v4.9.2 and below!
""".replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost)) """.replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost))
.replaceAll(Pattern.quote("%source_port%"), Integer.toString(sourcePort)) .replaceAll(Pattern.quote("%source_port%"), Integer.toString(sourcePort))
.replaceAll(Pattern.quote("%source_username%"), obfuscateDataString(sourceUsername)) .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 expProgress The player's current XP progress
* @param totalExp The player's total XP score * @param totalExp The player's total XP score
*/ */
private record MpdbData(@NotNull User user, @NotNull String serializedInventory, private record MpdbData(
@NotNull String serializedArmor, @NotNull String serializedEnderChest, @NotNull User user,
int expLevel, float expProgress, int totalExp) { @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 * @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 @NotNull
public CompletableFuture<UserData> toUserData(@NotNull MPDBConverter converter, public DataSnapshot.Packed toUserData(@NotNull MPDBConverter converter, @NotNull HuskSync plugin) {
@NotNull String minecraftVersion) { // Combine inventory and armor
return CompletableFuture.supplyAsync(() -> { final Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
// Combine inventory and armour inventory.setContents(converter.getItemStackFromSerializedData(serializedInventory));
final Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER); final ItemStack[] armor = converter.getItemStackFromSerializedData(serializedArmor).clone();
inventory.setContents(converter.getItemStackFromSerializedData(serializedInventory)); for (int i = 36; i < 36 + armor.length; i++) {
final ItemStack[] armor = converter.getItemStackFromSerializedData(serializedArmor).clone(); inventory.setItem(i, armor[i - 36]);
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 // Create user data record
return new UserData(new StatusData(20, 20, 0, 20, 10, return DataSnapshot.builder(plugin)
1, 0, totalExp, expLevel, expProgress, "SURVIVAL", .inventory(BukkitData.Items.Inventory.from(inventory.getContents(), 0))
false), .enderChest(BukkitData.Items.EnderChest.adapt(enderChest))
new ItemData(BukkitSerializer.serializeItemStackArray(inventory.getContents()).join()), .experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
new ItemData(BukkitSerializer.serializeItemStackArray(converter .gameMode(BukkitData.GameMode.from("SURVIVAL"))
.getItemStackFromSerializedData(serializedEnderChest)).join()), .saveCause(DataSnapshot.SaveCause.MPDB_MIGRATION)
new PotionEffectData(""), new ArrayList<>(), .buildAndPack();
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);
});
} }
} }
} }

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,128 @@
/*
* 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 hasDisconnected() {
return getPlugin().getDisconnectingPlayers().contains(getUuid())
|| 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,62 @@
/*
* 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) {
return getRegistryValue(Registry.EFFECT, key);
}
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_range: '${minecraft_version_range}'

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 name: 'HuskSync'
version: ${version} version: '${version}'
main: net.william278.husksync.BukkitHuskSync main: 'net.william278.husksync.BukkitHuskSync'
api-version: 1.16 api-version: '${minecraft_api_version}'
author: William278 author: 'William278'
description: 'A modern, cross-server player data synchronization system' description: '${description}'
website: 'https://william278.net' website: 'https://william278.net'
folia-supported: true
softdepend: softdepend:
- MysqlPlayerDataBridge - 'packetevents'
- Plan - 'ProtocolLib'
- 'MysqlPlayerDataBridge'
- 'Plan'
libraries: libraries:
- 'mysql:mysql-connector-java:8.0.29' - 'redis.clients:jedis:${jedis_version}'
- 'org.xerial.snappy:snappy-java:1.1.8.4' - 'com.mysql:mysql-connector-j:${mysql_driver_version}'
- 'dev.dejvokep:boosted-yaml:1.2' - 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}'
commands: - 'org.postgresql:postgresql:${postgres_driver_version}'
husksync: - 'org.mongodb:mongodb-driver-sync:${mongodb_driver_version}'
usage: '/husksync <update/info/reload/migrate>' - 'org.xerial.snappy:snappy-java:${snappy_version}'
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'

View File

@@ -1,31 +1,46 @@
dependencies { plugins {
implementation 'commons-io:commons-io:2.11.0' id 'java-library'
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'
} }
shadowJar { dependencies {
relocate 'org.apache', 'net.william278.husksync.libraries' api 'commons-io:commons-io:2.21.0'
relocate 'de.themoep', 'net.william278.husksync.libraries' api 'org.apache.commons:commons-text:1.14.0'
relocate 'org.jetbrains', 'net.william278.husksync.libraries' api 'net.william278:minedown:1.8.2'
relocate 'org.intellij', 'net.william278.husksync.libraries' api 'net.william278:mapdataapi:2.0'
relocate 'com.zaxxer', 'net.william278.husksync.libraries' api 'org.json:json:20250517'
relocate 'com.google', 'net.william278.husksync.libraries' api 'com.google.code.gson:gson:2.13.2'
relocate 'redis.clients', 'net.william278.husksync.libraries' api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
relocate 'org.json', 'net.william278.husksync.libraries.json' api 'de.exlll:configlib-yaml:4.6.4'
api 'net.william278:paginedown:1.1.2'
api 'net.william278:DesertWell:2.0.4'
api('com.zaxxer:HikariCP:7.0.2') {
exclude module: 'slf4j-api'
}
compileOnlyApi 'net.william278.toilet:toilet-common:1.0.16'
compileOnly 'net.william278.uniform:uniform-common:1.3.9'
compileOnly 'com.mojang:brigadier:1.1.8'
compileOnly 'org.projectlombok:lombok:1.18.42'
compileOnly 'org.jetbrains:annotations:26.0.2-1'
compileOnly 'net.kyori:adventure-api:4.25.0'
compileOnly 'net.kyori:adventure-platform-api:4.4.0'
compileOnly "net.kyori:adventure-text-serializer-plain:4.25.0"
compileOnly 'com.google.guava:guava:33.5.0-jre'
compileOnly 'com.github.plan-player-analytics:Plan:5.6.2965'
compileOnly "redis.clients:jedis:$jedis_version"
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
compileOnly "org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version"
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.5.0-jre'
testImplementation 'com.github.plan-player-analytics:Plan:5.6.2965'
testCompileOnly 'de.exlll:configlib-yaml:4.6.4'
testCompileOnly 'org.jetbrains:annotations:26.0.2-1'
annotationProcessor 'org.projectlombok:lombok:1.18.42'
} }

View File

@@ -1,28 +1,62 @@
/*
* 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; package net.william278.husksync;
import net.william278.husksync.config.Locales; import com.fatboyindustrial.gsonjavatime.Converters;
import net.william278.husksync.config.Settings; import com.google.common.collect.Maps;
import net.william278.husksync.data.DataAdapter; import com.google.gson.Gson;
import net.william278.husksync.editor.DataEditor; 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.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.migrator.Migrator;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.redis.RedisManager; import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.util.Logger; import net.william278.husksync.sync.DataSyncer;
import net.william278.husksync.util.Version; import net.william278.husksync.user.ConsoleUser;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.util.*;
import net.william278.uniform.Uniform;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.List; import java.io.InputStream;
import java.util.Optional; import java.text.SimpleDateFormat;
import java.util.Set; import java.util.*;
import java.util.UUID; import java.util.logging.Level;
import java.util.concurrent.CompletableFuture;
/** /**
* Abstract implementation of the HuskSync plugin. * Abstract implementation of the HuskSync plugin.
*/ */
public interface HuskSync { public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider, SerializerRegistry,
CompatibilityChecker, DumpProvider, DataVersionSupplier {
int SPIGOT_RESOURCE_ID = 97144;
/** /**
* Returns a set of online players. * Returns a set of online players.
@@ -54,33 +88,39 @@ public interface HuskSync {
* *
* @return the {@link RedisManager} implementation * @return the {@link RedisManager} implementation
*/ */
@NotNull @NotNull
RedisManager getRedisManager(); RedisManager getRedisManager();
/** /**
* Returns the data adapter implementation * Returns the implementing adapter for serializing data
* *
* @return the {@link DataAdapter} implementation * @return the {@link DataAdapter}
*/ */
@NotNull @NotNull
DataAdapter getDataAdapter(); DataAdapter getDataAdapter();
/** /**
* Returns the data editor implementation * Returns the data syncer implementation
* *
* @return the {@link DataEditor} implementation * @return the {@link DataSyncer} implementation
*/ */
@NotNull @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 @NotNull
EventCannon getEventCannon(); Uniform getUniform();
/** /**
* Returns a list of available data {@link Migrator}s * Returns a list of available data {@link Migrator}s
@@ -90,29 +130,108 @@ public interface HuskSync {
@NotNull @NotNull
List<Migrator> getAvailableMigrators(); List<Migrator> getAvailableMigrators();
/**
* Returns the plugin {@link Settings}
*
* @return the {@link Settings}
*/
@NotNull @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 = Maps.newHashMap();
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 default void initialize(@NotNull String name, @NotNull ThrowingConsumer<HuskSync> runner) {
Locales getLocales(); 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 @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 * Returns the plugin version
@@ -131,10 +250,113 @@ public interface HuskSync {
Version getMinecraftVersion(); Version getMinecraftVersion();
/** /**
* Reloads the {@link Settings} and {@link Locales} from their respective config files * Returns the platform type
* *
* @return a {@link CompletableFuture} that will be completed when the plugin reload is complete and if it was successful * @return the platform type
*/ */
CompletableFuture<Boolean> reload(); @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();
/**
* Get the set of UUIDs of players who are currently marked as disconnecting or disconnected
*/
@NotNull
Set<UUID> getDisconnectingPlayers();
default boolean isLocked(@NotNull UUID uuid) {
return getLockedPlayers().contains(uuid);
}
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; package net.william278.husksync.command;
import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSaveCause; import net.william278.husksync.data.Data;
import net.william278.husksync.data.UserData; import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.data.UserDataSnapshot; import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.editor.ItemEditorMenu; import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.player.OnlineUser; import net.william278.husksync.user.User;
import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull; 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.List;
import java.util.Locale; import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class EnderChestCommand extends CommandBase implements TabCompletable { public class EnderChestCommand extends ItemsCommand {
public EnderChestCommand(@NotNull HuskSync implementor) { public EnderChestCommand(@NotNull HuskSync plugin) {
super("enderchest", Permission.COMMAND_ENDER_CHEST, implementor, "echest", "openechest"); super("enderchest", List.of("echest", "openechest"), DataSnapshot.SaveCause.ENDERCHEST_COMMAND, plugin);
} }
@Override @Override
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) { protected void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
if (args.length == 0 || args.length > 2) { @NotNull User user, boolean allowEdit) {
plugin.getLocales().getLocale("error_invalid_syntax", "/enderchest <player>") final Optional<Data.Items.EnderChest> optionalEnderChest = snapshot.getEnderChest();
.ifPresent(player::sendMessage); if (optionalEnderChest.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
return; return;
} }
plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAccept(optionalUser ->
optionalUser.ifPresentOrElse(user -> { // Display opening message
if (args.length == 2) { plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getName(),
// View user data by specified UUID snapshot.getTimestamp().format(DateTimeFormatter
try { .ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
final UUID versionUuid = UUID.fromString(args[1]); .ifPresent(viewer::sendMessage);
plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> data.ifPresentOrElse(
userData -> showEnderChestMenu(player, userData, user, false), // Show GUI
() -> plugin.getLocales().getLocale("error_invalid_version_uuid") final Data.Items.EnderChest enderChest = optionalEnderChest.get();
.ifPresent(player::sendMessage))); viewer.showGui(
} catch (IllegalArgumentException e) { enderChest,
plugin.getLocales().getLocale("error_invalid_syntax", plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getName())
"/enderchest <player> [version_uuid]").ifPresent(player::sendMessage); .orElse(new MineDown(String.format("%s's Ender Chest", user.getName()))),
} allowEdit,
} else { enderChest.getSlotCount(),
// View latest user data (itemsOnClose) -> {
plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse( if (allowEdit && !enderChest.equals(itemsOnClose)) {
versionedUserData -> showEnderChestMenu(player, versionedUserData, user, true), plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
() -> plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(player::sendMessage)));
} }
}, () -> plugin.getLocales().getLocale("error_invalid_player") }
.ifPresent(player::sendMessage))); );
} }
private void showEnderChestMenu(@NotNull OnlineUser player, @NotNull UserDataSnapshot userDataSnapshot, // Creates a new snapshot with the updated enderChest
@NotNull User dataOwner, final boolean allowEdit) { @SuppressWarnings("DuplicatedCode")
CompletableFuture.runAsync(() -> { private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
final UserData data = userDataSnapshot.userData(); final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
final ItemEditorMenu menu = ItemEditorMenu.createEnderChestMenu(data.getEnderChestData(), if (latestData.isEmpty()) {
dataOwner, player, plugin.getLocales(), allowEdit); plugin.getLocales().getLocale("error_no_data_to_display")
plugin.getLocales().getLocale("viewing_ender_chest_of", dataOwner.username, .ifPresent(viewer::sendMessage);
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault()) return;
.format(userDataSnapshot.versionTimestamp())) }
.ifPresent(player::sendMessage);
plugin.getDataEditor().openItemEditorMenu(player, menu).thenAccept(enderChestDataOnClose -> { // Create and pack the snapshot with the updated enderChest
if (!menu.canEdit) { final DataSnapshot.Packed snapshot = latestData.get().copy();
return; boolean pin = plugin.getSettings().getSynchronization().doAutoPin(saveCause);
} snapshot.edit(plugin, (data) -> {
final UserData updatedUserData = new UserData(data.getStatusData(), data.getInventoryData(), data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items));
enderChestDataOnClose, data.getPotionEffectsData(), data.getAdvancementData(), data.setSaveCause(saveCause);
data.getStatisticsData(), data.getLocationData(), data.setPinned(pin);
data.getPersistentDataContainerData(),
plugin.getMinecraftVersion().toString());
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.ENDERCHEST_COMMAND).join();
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
});
}); });
} // Save data
final RedisManager redis = plugin.getRedisManager();
@Override plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
public List<String> onTabComplete(@NotNull String[] args) { redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot));
return plugin.getOnlineUsers().stream().map(user -> user.username) redis.sendUserDataUpdate(user, data);
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : "")) });
.sorted().collect(Collectors.toList());
} }
} }

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; 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.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.migrator.Migrator;
import net.william278.husksync.player.OnlineUser; import net.william278.husksync.user.CommandUser;
import net.william278.husksync.util.UpdateChecker; 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 org.jetbrains.annotations.NotNull;
import java.time.OffsetDateTime;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.UUID;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; 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) { public HuskSyncCommand(@NotNull HuskSync plugin) {
super("husksync", Permission.COMMAND_HUSKSYNC, implementor); 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 @Override
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) { public void provide(@NotNull BaseCommand<?> command) {
if (args.length < 1) { command.setDefaultExecutor((ctx) -> about(command, ctx));
displayPluginInformation(player); command.addSubCommand("about", (sub) -> sub.setDefaultExecutor((ctx) -> about(command, ctx)));
return; command.addSubCommand("status", needsOp("status"), status());
} command.addSubCommand("dump", needsOp("dump"), dump());
switch (args[0].toLowerCase()) { command.addSubCommand("reload", needsOp("reload"), reload());
case "update", "version" -> { command.addSubCommand("update", needsOp("update"), update());
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_UPDATE.node)) { command.addSubCommand("forceupgrade", forceUpgrade());
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage); command.addSubCommand("migrate", migrate());
return; }
}
final UpdateChecker updateChecker = new UpdateChecker(plugin.getPluginVersion(), plugin.getLoggingAdapter()); private void about(@NotNull BaseCommand<?> c, @NotNull CommandContext<?> ctx) {
updateChecker.fetchLatestVersion().thenAccept(latestVersion -> { user(c, ctx).getAudience().sendMessage(aboutMenu.toComponent());
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)" + @NotNull
"[•](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)")); 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 { } else {
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date, running version " + updateChecker.getCurrentVersion() + "](#00fb9a)")); plugin.log(Level.WARNING, "Migration failed!");
} }
}); });
} }, migrator()));
case "info", "about" -> displayPluginInformation(player); sub.addSubCommand("set", (set) -> set.addSyntax((cmd) -> {
case "reload" -> { final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_RELOAD.node)) { final String[] args = cmd.getArgument("args", String.class).split(" ");
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage); migrator.handleConfigurationCommand(args);
return; }, migrator(), BaseCommand.greedyString("args")));
} };
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);
}
} }
@Override @NotNull
public void onConsoleExecute(@NotNull String[] args) { private CommandProvider forceUpgrade() {
if (args.length < 1) { return (sub) -> {
plugin.getLoggingAdapter().log(Level.INFO, "Console usage: \"husksync <update/about/reload/migrate>\""); sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
return; sub.setDefaultExecutor((ctx) -> {
} final LegacyConverter converter = plugin.getLegacyConverter().orElse(null);
switch (args[0].toLowerCase()) { if (converter == null) {
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();
return; return;
} }
final Optional<Migrator> selectedMigrator = plugin.getAvailableMigrators().stream().filter(availableMigrator ->
availableMigrator.getIdentifier().equalsIgnoreCase(args[1])).findFirst(); plugin.runAsync(() -> {
selectedMigrator.ifPresentOrElse(migrator -> { final Database database = plugin.getDatabase();
if (args.length < 3) { plugin.log(Level.INFO, "Beginning forced legacy data upgrade for all users...");
plugin.getLoggingAdapter().log(Level.INFO, migrator.getHelpMenu()); database.getAllUsers().forEach(user -> database.getLatestSnapshot(user).ifPresent(snapshot -> {
return; final DataSnapshot.Packed upgraded = converter.convert(
} snapshot.asBytes(plugin),
switch (args[2]) { UUID.randomUUID(),
case "start" -> migrator.start().thenAccept(succeeded -> { OffsetDateTime.now()
if (succeeded) { );
plugin.getLoggingAdapter().log(Level.INFO, "Migration completed successfully!"); upgraded.setSaveCause(DataSnapshot.SaveCause.CONVERTED_FROM_V2);
} else { plugin.getDatabase().addSnapshot(user, upgraded);
plugin.getLoggingAdapter().log(Level.WARNING, "Migration failed!"); plugin.getRedisManager().clearUserData(user);
} }));
}); plugin.log(Level.INFO, "Legacy data upgrade complete!");
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();
}); });
});
};
}
@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, return migrator;
"Invalid syntax. Console usage: \"husksync <update/about/reload/migrate>\""); }, (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,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; package net.william278.husksync.command;
import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSaveCause; import net.william278.husksync.data.Data;
import net.william278.husksync.data.UserData; import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.data.UserDataSnapshot; import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.editor.ItemEditorMenu; import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.player.OnlineUser; import net.william278.husksync.user.User;
import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull; 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.List;
import java.util.Locale; import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class InventoryCommand extends CommandBase implements TabCompletable { public class InventoryCommand extends ItemsCommand {
public InventoryCommand(@NotNull HuskSync implementor) { public InventoryCommand(@NotNull HuskSync plugin) {
super("inventory", Permission.COMMAND_INVENTORY, implementor, "invsee", "openinv"); super("inventory", List.of("invsee", "openinv"), DataSnapshot.SaveCause.INVENTORY_COMMAND, plugin);
} }
@Override @Override
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) { protected void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
if (args.length == 0 || args.length > 2) { @NotNull User user, boolean allowEdit) {
plugin.getLocales().getLocale("error_invalid_syntax", "/inventory <player>") final Optional<Data.Items.Inventory> optionalInventory = snapshot.getInventory();
.ifPresent(player::sendMessage); if (optionalInventory.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
return; return;
} }
plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAccept(optionalUser ->
optionalUser.ifPresentOrElse(user -> { // Display opening message
if (args.length == 2) { plugin.getLocales().getLocale("inventory_viewer_opened", user.getName(),
// View user data by specified UUID snapshot.getTimestamp().format(DateTimeFormatter
try { .ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
final UUID versionUuid = UUID.fromString(args[1]); .ifPresent(viewer::sendMessage);
plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> data.ifPresentOrElse(
userData -> showInventoryMenu(player, userData, user, false), // Show GUI
() -> plugin.getLocales().getLocale("error_invalid_version_uuid") final Data.Items.Inventory inventory = optionalInventory.get();
.ifPresent(player::sendMessage))); viewer.showGui(
} catch (IllegalArgumentException e) { inventory,
plugin.getLocales().getLocale("error_invalid_syntax", plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getName())
"/inventory <player> [version_uuid]").ifPresent(player::sendMessage); .orElse(new MineDown(String.format("%s's Inventory", user.getName()))),
} allowEdit,
} else { inventory.getSlotCount(),
// View latest user data (itemsOnClose) -> {
plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse( if (allowEdit && !inventory.equals(itemsOnClose)) {
versionedUserData -> showInventoryMenu(player, versionedUserData, user, true), plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
() -> plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(player::sendMessage)));
} }
}, () -> plugin.getLocales().getLocale("error_invalid_player") }
.ifPresent(player::sendMessage))); );
} }
private void showInventoryMenu(@NotNull OnlineUser player, @NotNull UserDataSnapshot userDataSnapshot, // Creates a new snapshot with the updated inventory
@NotNull User dataOwner, boolean allowEdit) { @SuppressWarnings("DuplicatedCode")
CompletableFuture.runAsync(() -> { private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
final UserData data = userDataSnapshot.userData(); final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
final ItemEditorMenu menu = ItemEditorMenu.createInventoryMenu(data.getInventoryData(), if (latestData.isEmpty()) {
dataOwner, player, plugin.getLocales(), allowEdit); plugin.getLocales().getLocale("error_no_data_to_display")
plugin.getLocales().getLocale("viewing_inventory_of", dataOwner.username, .ifPresent(viewer::sendMessage);
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault()) return;
.format(userDataSnapshot.versionTimestamp())) }
.ifPresent(player::sendMessage);
plugin.getDataEditor().openItemEditorMenu(player, menu).thenAccept(inventoryDataOnClose -> { // Create and pack the snapshot with the updated inventory
if (!menu.canEdit) { final DataSnapshot.Packed snapshot = latestData.get().copy();
return; boolean pin = plugin.getSettings().getSynchronization().doAutoPin(saveCause);
} snapshot.edit(plugin, (data) -> {
final UserData updatedUserData = new UserData(data.getStatusData(), inventoryDataOnClose, data.getInventory().ifPresent(inventory -> inventory.setContents(items));
data.getEnderChestData(), data.getPotionEffectsData(), data.getAdvancementData(), data.setSaveCause(saveCause);
data.getStatisticsData(), data.getLocationData(), data.setPinned(pin);
data.getPersistentDataContainerData(), });
plugin.getMinecraftVersion().toString());
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND).join(); // Save data
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join(); 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,327 @@
/*
* 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; 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.HuskSync;
import net.william278.husksync.data.DataSaveCause; import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.player.OnlineUser; 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 org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.logging.Level;
import java.util.stream.Collectors;
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 plugin) {
super("userdata", List.of("playerdata"), Permission.Default.IF_OP, ExecutionScope.ALL, plugin);
public UserDataCommand(@NotNull HuskSync implementor) {
super("userdata", Permission.COMMAND_USER_DATA, implementor, "playerdata");
} }
@Override @Override
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) { public void provide(@NotNull BaseCommand<?> command) {
if (args.length < 1) { command.addSubCommand("view", needsOp("view"), view());
plugin.getLocales().getLocale("error_invalid_syntax", command.addSubCommand("list", needsOp("list"), list());
"/userdata <view/list/delete/restore/pin> <username> [version_uuid]") command.addSubCommand("delete", needsOp("delete"), delete());
.ifPresent(player::sendMessage); 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; return;
} }
switch (args[0].toLowerCase()) { // Restore users with a minimum of one health (prevent restoring players with <= 0 health)
case "view" -> { final DataSnapshot.Packed data = optionalData.get().copy();
if (args.length < 2) { if (data.isInvalid()) {
plugin.getLocales().getLocale("error_invalid_syntax", plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
"/userdata view <username> [version_uuid]") .ifPresent(executor::sendMessage);
.ifPresent(player::sendMessage); return;
return; }
} data.edit(plugin, (unpacked -> {
final String username = args[1]; unpacked.getHealth().ifPresent(status -> status.setHealth(Math.max(1, status.getHealth())));
if (args.length >= 3) { unpacked.setSaveCause(DataSnapshot.SaveCause.BACKUP_RESTORE);
try { unpacked.setPinned(
final UUID versionUuid = UUID.fromString(args[2]); plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.BACKUP_RESTORE)
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept( );
optionalUser -> optionalUser.ifPresentOrElse( }));
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data ->
data.ifPresentOrElse(userData -> plugin.getDataEditor() // Save data
.displayDataOverview(player, userData, user), final RedisManager redis = plugin.getRedisManager();
() -> plugin.getLocales().getLocale("error_invalid_version_uuid") plugin.getDataSyncer().saveData(user, data, (u, s) -> {
.ifPresent(player::sendMessage))), redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s));
() -> plugin.getLocales().getLocale("error_invalid_player") redis.sendUserDataUpdate(u, s);
.ifPresent(player::sendMessage)))); plugin.getLocales().getLocale("data_restored", u.getName(), u.getUuid().toString(),
} catch (IllegalArgumentException e) { s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage);
plugin.getLocales().getLocale("error_invalid_syntax", });
"/userdata view <username> [version_uuid]") }
.ifPresent(player::sendMessage);
} // Pin a snapshot
} else { private void pinSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept( final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, version);
optionalUser -> optionalUser.ifPresentOrElse( if (optionalData.isEmpty()) {
user -> plugin.getDatabase().getCurrentUserData(user).thenAccept( plugin.getLocales().getLocale("error_invalid_version_uuid")
latestData -> latestData.ifPresentOrElse( .ifPresent(executor::sendMessage);
userData -> plugin.getDataEditor() return;
.displayDataOverview(player, userData, user), }
() -> plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(player::sendMessage))), // Pin or unpin the data
() -> plugin.getLocales().getLocale("error_invalid_player") final DataSnapshot.Packed data = optionalData.get();
.ifPresent(player::sendMessage)))); if (data.isPinned()) {
} plugin.getDatabase().unpinSnapshot(user, data.getId());
} } else {
case "list" -> { plugin.getDatabase().pinSnapshot(user, data.getId());
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) { }
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage); plugin.getLocales().getLocale(data.isPinned() ? "data_unpinned" : "data_pinned", data.getShortId(),
return; data.getId().toString(), user.getName(), user.getUuid().toString())
} .ifPresent(executor::sendMessage);
if (args.length < 2) { }
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata list <username>") // Lookup a snapshot by UUID and dump
.ifPresent(player::sendMessage); private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version,
return; @NotNull DumpType type) {
} final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, version);
final String username = args[1]; if (data.isEmpty()) {
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept( plugin.getLocales().getLocale("error_invalid_version_uuid")
optionalUser -> optionalUser.ifPresentOrElse( .ifPresent(executor::sendMessage);
user -> plugin.getDatabase().getUserData(user).thenAccept(dataList -> { return;
if (dataList.isEmpty()) { }
plugin.getLocales().getLocale("error_no_data_to_display") this.dumpSnapshot(executor, user, data.get(), type);
.ifPresent(player::sendMessage); }
return;
} // Dump a snapshot
plugin.getDataEditor().displayDataList(player, dataList, user); private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user,
}), @NotNull DataSnapshot.Packed userData, @NotNull DumpType type) {
() -> plugin.getLocales().getLocale("error_invalid_player") final UserDataDumper dumper = UserDataDumper.create(userData, user, plugin);
.ifPresent(player::sendMessage)))); try {
} final String url = type == DumpType.WEB ? dumper.toWeb() : dumper.toFile();
case "delete" -> { plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getName())
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) { .ifPresent(executor::sendMessage);
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage); executor.sendMessage(Component.text(url)
return; .clickEvent(type == DumpType.WEB ? ClickEvent.openUrl(url) : ClickEvent.copyToClipboard(url))
} .decorate(TextDecoration.UNDERLINED).color(NamedTextColor.GRAY));
// Delete user data by specified UUID } catch (Throwable e) {
if (args.length < 3) { plugin.log(Level.SEVERE, "Failed to dump user data", e);
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);
}
}
} }
} }
@Override @NotNull
public List<String> onTabComplete(@NotNull String[] args) { private CommandProvider view() {
switch (args.length) { return (sub) -> {
case 0, 1 -> { sub.addSyntax((ctx) -> {
return Arrays.stream(COMMAND_ARGUMENTS) final User user = ctx.getArgument("username", User.class);
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : "")) final UUID version = ctx.getArgument("version", UUID.class);
.sorted().collect(Collectors.toList()); viewSnapshot(user(sub, ctx), user, version);
} }, user("username"), versionUuid());
case 2 -> { sub.addSyntax((ctx) -> {
return plugin.getOnlineUsers().stream().map(user -> user.username) final User user = ctx.getArgument("username", User.class);
.filter(argument -> argument.startsWith(args[1])) viewLatestSnapshot(user(sub, ctx), user);
.sorted().collect(Collectors.toList()); }, user("username"));
} };
}
return Collections.emptyList();
} }
@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; package net.william278.husksync.config;
import de.themoep.minedown.MineDown; import com.google.common.collect.Maps;
import dev.dejvokep.boostedyaml.YamlDocument; 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 org.jetbrains.annotations.NotNull;
import java.util.HashMap; import java.util.Arrays;
import java.util.Map;
import java.util.Optional; 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 class Locales {
public static final String PLUGIN_INFORMATION = """ static final String CONFIG_HEADER = """
[HuskSync](#00fb9a bold) [| Version %version%](#00fb9a) ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
[A modern, cross-server player data synchronization system](gray) ┃ HuskSync - Locales ┃
[• Author:](white) [William278](gray show_text=&7Click to visit website open_url=https://william278.net) ┃ Developed by William278
[• 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) ┣╸ See plugin about menu for international locale credits
[• Documentation:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://william278.net/docs/husksync/Home/) ┣╸ Formatted in MineDown: https://github.com/Phoenix616/MineDown
[• Bug reporting:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/issues) ┗╸ Translate HuskSync: https://william278.net/docs/husksync/translations""";
[• Discord support:](white) [[Link]](#00fb9a show_text=&7Click to join open_url=https://discord.gg/tVYhJfyDWG)""";
@NotNull protected static final String DEFAULT_LOCALE = "en-gb";
private final HashMap<String, String> rawLocales;
private Locales(@NotNull YamlDocument localesConfig) { // The raw set of locales loaded from yaml
this.rawLocales = new HashMap<>(); Map<String, String> locales = Maps.newTreeMap();
for (String localeId : localesConfig.getRoutesAsStrings(false)) {
rawLocales.put(localeId, localesConfig.getString(localeId));
}
}
/** /**
* 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 * @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 * @return An {@link Optional} containing the locale corresponding to the id, if it exists
*/ */
public Optional<String> getRawLocale(@NotNull String localeId) { public Optional<String> getRawLocale(@NotNull String localeId) {
if (rawLocales.containsKey(localeId)) { return Optional.ofNullable(locales.get(localeId)).map(StringEscapeUtils::unescapeJava);
return Optional.of(rawLocales.get(localeId).replaceAll(Pattern.quote("\\n"), "\n"));
}
return Optional.empty();
} }
/** /**
* 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 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 * @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 * @return An {@link Optional} containing the formatted locale corresponding to the id, if it exists
*/ */
public Optional<MineDown> getLocale(@NotNull String localeId) { 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 * 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 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 * @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 * @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) { 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 * @param replacements Ordered array of replacement strings to fill in placeholders with
* @return the raw locale, with inserted placeholders * @return the raw locale, with inserted placeholders
*/ */
@NotNull
private String applyReplacements(@NotNull String rawLocale, @NotNull String... replacements) { private String applyReplacements(@NotNull String rawLocale, @NotNull String... replacements) {
int replacementIndexer = 1; int replacementIndexer = 1;
for (String replacement : replacements) { for (String replacement : replacements) {
String replacementString = "%" + replacementIndexer + "%"; String replacementString = "%" + replacementIndexer + "%";
rawLocale = rawLocale.replace(replacementString, replacement); rawLocale = rawLocale.replace(replacementString, replacement);
replacementIndexer = replacementIndexer + 1; replacementIndexer += 1;
} }
return rawLocale; 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 * @param string The string to escape
* @return the loaded {@link Locales} * @return The escaped string
*/ */
public static Locales load(@NotNull YamlDocument localesConfig) { @NotNull
return new Locales(localesConfig); 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 * @param itemsPerPage The number of items to display per page
* @return The MineDown-stripped string * @return The list options
*/ */
public String stripMineDown(@NotNull String string) { @NotNull
final String[] in = string.split("\n"); public ListOptions.Builder getBaseChatList(int itemsPerPage) {
final StringBuilder out = new StringBuilder(); return new ListOptions.Builder()
String regex = "[^\\[\\]() ]*\\[([^()]+)]\\([^()]+open_url=(\\S+).*\\)"; .setFooterFormat(getRawLocale("list_footer",
"%previous_page_button%", "%current_page%",
for (int i = 0; i < in.length; i++) { "%total_pages%", "%next_page_button%", "%page_jumpers%").orElse(""))
Pattern pattern = Pattern.compile(regex); .setNextButtonFormat(getRawLocale("list_next_page_button",
Matcher m = pattern.matcher(in[i]); "%next_page_index%", "%command%").orElse(""))
.setPreviousButtonFormat(getRawLocale("list_previous_page_button",
if (m.find()) { "%previous_page_index%", "%command%").orElse(""))
out.append(in[i].replace(m.group(0), "")); .setPageJumpersFormat(getRawLocale("list_page_jumpers",
out.append(m.group(2)); "%page_jump_buttons%").orElse(""))
} else { .setPageJumperPageFormat(getRawLocale("list_page_jumper_button",
out.append(in[i]); "%target_page_index%", "%command%").orElse(""))
} .setPageJumperCurrentPageFormat(getRawLocale("list_page_jumper_current_page",
"%current_page%").orElse(""))
if (i + 1 != in.length) { .setPageJumperPageSeparator(getRawLocale("list_page_jumper_separator").orElse(""))
out.append("\n"); .setPageJumperGroupSeparator(getRawLocale("list_page_jumper_group_separator").orElse(""))
} .setItemsPerPage(itemsPerPage)
} .setEscapeItemsMineDown(false)
.setSpaceAfterHeader(false)
return out.toString(); .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,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.config;
import de.exlll.configlib.Configuration;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.nio.file.Path;
@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);
}
public String getName() {
final String envServerName = System.getenv("HUSKSYNC_SERVER_NAME");
return envServerName == null ? name : envServerName;
}
}

View File

@@ -1,273 +1,394 @@
/*
* 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; 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.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List; 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 { public class Settings {
/** protected static final String CONFIG_HEADER = """
* Map of {@link ConfigOption}s read from the config file ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
*/ ┃ HuskSync Config
private final HashMap<ConfigOption, Object> configOptions; ┃ 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 // Top-level settings
private Settings(@NotNull YamlDocument config) { @Comment({"Locale of the default language file to use.", "Docs: https://william278.net/docs/husksync/translations"})
this.configOptions = new HashMap<>(); private String language = Locales.DEFAULT_LOCALE;
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);
}));
}
/** @Comment("Whether to automatically check for plugin updates on startup")
* Get the value of the specified {@link ConfigOption} private boolean checkForUpdates = true;
*
* @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("Specify a common ID for grouping servers running HuskSync. "
* Get the value of the specified {@link ConfigOption} + "Don't modify this unless you know what you're doing!")
* private String clusterId = "";
* @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("Enable development debug logging")
* Get the value of the specified {@link ConfigOption} private boolean debugLogging = false;
*
* @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({"Whether to enable the Player Analytics hook.", "Docs: https://william278.net/docs/husksync/plan-hook"})
* Get the value of the specified {@link ConfigOption} private boolean enablePlanHook = true;
*
* @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 cancel game event packets directly when handling locked players if ProtocolLib or PacketEvents is installed")
* Get the value of the specified {@link ConfigOption} private boolean cancelPackets = true;
*
* @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("Add HuskSync commands to this list to prevent them from being registered (e.g. ['userdata'])")
* Get the value of the specified {@link ConfigOption} @Getter(AccessLevel.NONE)
* private List<String> disabledCommands = Lists.newArrayList();
* @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);
}
// Database settings
@Comment("Database settings")
private DatabaseSettings database = new DatabaseSettings();
/** @Getter
* Load the settings from a BoostedYaml {@link YamlDocument} config file @Configuration
* @NoArgsConstructor(access = AccessLevel.PRIVATE)
* @param config The loaded {@link YamlDocument} config.yml file public static class DatabaseSettings {
* @return the loaded {@link Settings}
*/
public static Settings load(@NotNull YamlDocument config) {
return new Settings(config);
}
/** @Comment("Type of database to use (MYSQL, MARIADB, POSTGRES, MONGO)")
* Represents an option stored by a path in config.yml private Database.Type type = Database.Type.MYSQL;
*/
public enum ConfigOption {
LANGUAGE("language", OptionType.STRING, "en-gb"),
CHECK_FOR_UPDATES("check_for_updates", OptionType.BOOLEAN, true),
CLUSTER_ID("cluster_id", OptionType.STRING, ""), @Comment("Specify credentials here for your MYSQL, MARIADB, POSTGRES OR MONGO database")
DEBUG_LOGGING("debug_logging", OptionType.BOOLEAN, false), private DatabaseCredentials credentials = new DatabaseCredentials();
DATABASE_HOST("database.credentials.host", OptionType.STRING, "localhost"), @Getter
DATABASE_PORT("database.credentials.port", OptionType.INTEGER, 3306), @Configuration
DATABASE_NAME("database.credentials.database", OptionType.STRING, "HuskSync"), @NoArgsConstructor(access = AccessLevel.PRIVATE)
DATABASE_USERNAME("database.credentials.username", OptionType.STRING, "root"), public static class DatabaseCredentials {
DATABASE_PASSWORD("database.credentials.password", OptionType.STRING, "pa55w0rd"), private String host = "localhost";
DATABASE_CONNECTION_PARAMS("database.credentials.params", OptionType.STRING, "?autoReconnect=true&useSSL=false"), private int port = 3306;
DATABASE_CONNECTION_POOL_MAX_SIZE("database.connection_pool.maximum_pool_size", OptionType.INTEGER, 10), private String database = "HuskSync";
DATABASE_CONNECTION_POOL_MIN_IDLE("database.connection_pool.minimum_idle", OptionType.INTEGER, 10), private String username = "root";
DATABASE_CONNECTION_POOL_MAX_LIFETIME("database.connection_pool.maximum_lifetime", OptionType.INTEGER, 1800000), private String password = "pa55w0rd";
DATABASE_CONNECTION_POOL_KEEPALIVE("database.connection_pool.keepalive_time", OptionType.INTEGER, 0), @Comment("Only change this if you're using MARIADB or POSTGRES")
DATABASE_CONNECTION_POOL_TIMEOUT("database.connection_pool.connection_timeout", OptionType.INTEGER, 5000), private String parameters = String.join("&",
DATABASE_USERS_TABLE_NAME("database.table_names.users_table", OptionType.STRING, "husksync_users"), "?autoReconnect=true", "useSSL=false",
DATABASE_USER_DATA_TABLE_NAME("database.table_names.user_data_table", OptionType.STRING, "husksync_user_data"), "useUnicode=true", "characterEncoding=UTF-8");
}
REDIS_HOST("redis.credentials.host", OptionType.STRING, "localhost"), @Comment("MYSQL, MARIADB, POSTGRES database Hikari connection pool properties. Don't modify this unless you know what you're doing!")
REDIS_PORT("redis.credentials.port", OptionType.INTEGER, 6379), private PoolSettings connectionPool = new PoolSettings();
REDIS_PASSWORD("redis.credentials.password", OptionType.STRING, ""),
REDIS_USE_SSL("redis.use_ssl", OptionType.BOOLEAN, false),
SYNCHRONIZATION_MAX_USER_DATA_SNAPSHOTS("synchronization.max_user_data_snapshots", OptionType.INTEGER, 5), @Getter
SYNCHRONIZATION_SAVE_ON_WORLD_SAVE("synchronization.save_on_world_save", OptionType.BOOLEAN, true), @Configuration
SYNCHRONIZATION_COMPRESS_DATA("synchronization.compress_data", OptionType.BOOLEAN, true), @NoArgsConstructor(access = AccessLevel.PRIVATE)
SYNCHRONIZATION_NETWORK_LATENCY_MILLISECONDS("synchronization.network_latency_milliseconds", OptionType.INTEGER, 500), public static class PoolSettings {
SYNCHRONIZATION_SYNC_INVENTORIES("synchronization.features.inventories", OptionType.BOOLEAN, true), private int maximumPoolSize = 10;
SYNCHRONIZATION_SYNC_ENDER_CHESTS("synchronization.features.ender_chests", OptionType.BOOLEAN, true), private int minimumIdle = 10;
SYNCHRONIZATION_SYNC_HEALTH("synchronization.features.health", OptionType.BOOLEAN, true), private long maximumLifetime = 1800000;
SYNCHRONIZATION_SYNC_MAX_HEALTH("synchronization.features.max_health", OptionType.BOOLEAN, true), private long keepaliveTime = 0;
SYNCHRONIZATION_SYNC_HUNGER("synchronization.features.hunger", OptionType.BOOLEAN, true), private long connectionTimeout = 5000;
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), @Comment("Advanced MongoDB settings. Don't modify unless you know what you're doing!")
SYNCHRONIZATION_SYNC_GAME_MODE("synchronization.features.game_mode", OptionType.BOOLEAN, true), private MongoSettings mongoSettings = new MongoSettings();
SYNCHRONIZATION_SYNC_STATISTICS("synchronization.features.statistics", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER("synchronization.features.persistent_data_container", OptionType.BOOLEAN, true), @Getter
SYNCHRONIZATION_SYNC_LOCATION("synchronization.features.location", OptionType.BOOLEAN, true); @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 @NotNull
public final String configPath; public String getTableName(@NotNull Database.TableName tableName) {
return tableNames.getOrDefault(tableName.name().toLowerCase(Locale.ENGLISH), tableName.getDefaultName());
/**
* 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
} }
} }
// Redis 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 \"user\" to '' if you don't have one or would like to use the default user.",
"Set \"password\" to '' if you don't have one."})
private RedisCredentials credentials = new RedisCredentials();
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class RedisCredentials {
private String host = "localhost";
private int port = 6379;
@Comment("Only change the database if you know what you are doing. The default is 0.")
private int database = 0;
private String user = "";
private String password = "";
@Comment("Use SSL/TLS for encrypted connections.")
private boolean useSsl = false;
@Comment("Connection timeout in milliseconds.")
private int connectionTimeout = 2000;
@Comment("Socket (read/write) timeout in milliseconds.")
private int socketTimeout = 2000;
@Comment("Max number of connections in the pool.")
private int maxTotalConnections = 50;
@Comment("Max number of idle connections in the pool.")
private int maxIdleConnections = 8;
@Comment("Min number of idle connections in the pool.")
private int minIdleConnections = 2;
@Comment("Enable health checks when borrowing connections from the pool.")
private boolean testOnBorrow = true;
@Comment("Enable health checks when returning connections to the pool.")
private boolean testOnReturn = true;
@Comment("Enable periodic idle connection health checks.")
private boolean testWhileIdle = true;
@Comment("Min evictable idle time (ms) before a connection is eligible for eviction.")
private long minEvictableIdleTimeMillis = 60000;
@Comment("Time (ms) between eviction runs.")
private long timeBetweenEvictionRunsMillis = 30000;
@Comment("Number of retries for commands when connection fails.")
private int maxRetries = 3;
@Comment("Base backoff time in ms for retries (exponential backoff multiplier).")
private int retryBackoffMillis = 200;
}
@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();
@Comment("Enable check-in petitions for data syncing (don't change this unless you know what you're doing)")
private boolean checkinPetitions = false;
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,161 @@
/*
* 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) {
if (getData().containsKey(id)) {
return Optional.of(getData().get(id));
}
return Optional.empty();
}
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,367 @@
/*
* 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 net.kyori.adventure.key.KeyPattern;
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 implements Comparable<Identifier> {
// Namespace for built-in identifiers
private static final @KeyPattern String DEFAULT_NAMESPACE = "husksync";
// Built-in identifiers
public static final Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
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(DEFAULT_NAMESPACE)) {
throw new IllegalArgumentException("Cannot register with %s as key namespace!".formatted(key.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(DEFAULT_NAMESPACE, 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(DEFAULT_NAMESPACE, 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(DEFAULT_NAMESPACE);
}
/**
* Get the minimal string representation of this key.
* <p>
* If the namespace of the key is {@link #DEFAULT_NAMESPACE}, only the key value will be returned.
*
* @return the minimal string key representation
* @since 3.8
*/
@NotNull
public String asMinimalString() {
if (getKey().namespace().equals(DEFAULT_NAMESPACE)) {
return getKey().value();
}
return getKey().asString();
}
/**
* Returns the identifier as a string (the key)
*
* @return the identifier as a string
* @since 3.0
*/
@NotNull
@Override
public String toString() {
return key.asString();
}
/**
* Return whether this Identifier is equal to another Identifier
*
* @param obj another object
* @return {@code true} if this identifier matches the identifier of {@code obj}
* @since 3.8
*/
@Override
public boolean equals(@Nullable Object obj) {
if (obj instanceof Identifier other) {
return asMinimalString().equals(other.asMinimalString());
}
return false;
}
/**
* Get the hash code of the Identifier (equivalent to {@link #asMinimalString()}->{@code #hashCode()}
*
* @return the hash code
* @since 3.8
*/
@Override
public int hashCode() {
return asMinimalString().hashCode();
}
// Get the config entry for the identifier
@NotNull
private Map.Entry<String, Boolean> getConfigEntry() {
return Map.entry(getKeyValue(), enabledByDefault);
}
// Comparable; always sort this Identifier after any dependencies
@Override
public int compareTo(@NotNull Identifier o) {
if (this.dependsOn(o)) return 1;
if (o.dependsOn(this)) return -1;
return this.key.compareTo(o.key);
}
/**
* Compares two identifiers based on their dependencies.
* <p>
* 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);
}
}
}

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