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

Compare commits

..

178 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
142 changed files with 3939 additions and 1633 deletions

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

View File

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

View File

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

View File

@@ -13,9 +13,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout for CI 🛎'
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: 'Set up JDK 21 📦'
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
@@ -24,7 +24,7 @@ jobs:
with:
arguments: test
- name: 'Publish Test Report 📊'
uses: mikepenz/action-junit-report@v5
uses: mikepenz/action-junit-report@v6
if: success() || failure() # Continue on failure
with:
report_paths: '**/build/test-results/test/TEST-*.xml'

View File

@@ -1,4 +1,4 @@
name: Release Tests
name: Release Tests & Publish
on:
release:
@@ -8,65 +8,26 @@ permissions:
contents: read
checks: write
jobs:
build:
name: 'Publish Release'
runs-on: ubuntu-latest
steps:
- name: 'Setup JDK 21 📦'
uses: actions/setup-java@v4
- 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: 'Setup Gradle 8.8 🏗️'
uses: gradle/actions/setup-gradle@v4
- name: 'Build with Gradle 🏗️'
uses: gradle/gradle-build-action@v3
with:
gradle-version: '8.8'
- name: '[Current - 1.21.3] Checkout for CI 🛎️'
uses: actions/checkout@v4
with:
path: '1_21_3'
- name: '[Non-LTS - 1.21.1] Checkout for CI 🛎️'
uses: actions/checkout@v4
with:
ref: 'minecraft/1.21.1'
path: '1_21_1'
- name: '[LTS - 1.20.1] Checkout for CI 🛎️'
uses: actions/checkout@v4
with:
ref: 'minecraft/1.20.1'
path: '1_20_1'
- name: '[Current - 1.21.3] Build 🛎️'
run: |
mkdir target
cd 1_21_3
./gradlew clean build publish -Dforce-hide-version-meta=1
cp -rf target/* ../target/
cd ..
env:
RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
RELEASES_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
- name: '[Non-LTS - 1.21.1] Build 🛎️'
run: |
cd 1_21_1
./gradlew clean build publish -Dforce-hide-version-meta=1
cp -rf target/* ../target/
cd ..
env:
RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
RELEASES_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
- name: '[LTS - 1.20.1] Build 🛎️'
run: |
cd 1_20_1
./gradlew clean build publish -Dforce-hide-version-meta=1
cp -rf target/* ../target/
cd ..
arguments: build test publish
env:
RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
RELEASES_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
- name: 'Publish Test Report 📊'
uses: mikepenz/action-junit-report@v5
uses: mikepenz/action-junit-report@v6
if: success() || failure() # Continue on failure
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
@@ -79,30 +40,42 @@ jobs:
version: ${{ github.event.release.tag_name }}
changelog: ${{ github.event.release.body }}
distro-names: |
paper-1.21.3
fabric-1.21.3
paper-1.21.1
paper-1.21.4
paper-1.21.5
paper-1.21.8
paper-1.21.10
fabric-1.21.1
paper-1.20.1
fabric-1.20.1
fabric-1.21.4
fabric-1.21.5
fabric-1.21.8
distro-groups: |
paper
fabric
paper
paper
paper
paper
fabric
paper
fabric
fabric
fabric
distro-descriptions: |
Paper 1.21.3
Fabric 1.21.3
Paper 1.21.1
Paper 1.21.4
Paper 1.21.5
Paper 1.21.8
Paper 1.21.10
Fabric 1.21.1
Paper 1.20.1
Fabric 1.20.1
Fabric 1.21.4
Fabric 1.21.5
Fabric 1.21.8
files: |
target/HuskSync-Paper-${{ github.event.release.tag_name }}+mc.1.21.3.jar
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.21.3.jar
target/HuskSync-Paper-${{ github.event.release.tag_name }}+mc.1.21.1.jar
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.1.jar
target/HuskSync-Bukkit-${{ github.event.release.tag_name }}+mc.1.21.4.jar
target/HuskSync-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-Paper-${{ github.event.release.tag_name }}+mc.1.20.1.jar
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.20.1.jar
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.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

View File

@@ -18,8 +18,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout for CI 🛎️'
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: 'Push Docs to Github Wiki 📄️'
uses: Andrew-Chen-Wang/github-wiki-action@v4
uses: Andrew-Chen-Wang/github-wiki-action@v5
with:
path: 'docs'

1
.gitignore vendored
View File

@@ -122,4 +122,3 @@ run/
# Don't include generated test suite files
/test/servers/
/test/HuskSync
/test/config.yml

View File

@@ -1,8 +1,8 @@
<!--suppress ALL -->
<p align="center">
<img src="images/banner.png" alt="HuskSync" />
<a href="https://github.com/WiIIiam278/HuskSync/actions/workflows/ci_master.yml">
<img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/ci_master.yml?branch=master&logo=github"/>
<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" />
@@ -46,25 +46,30 @@
## Compatibility
HuskSync supports the following [compatible versions](https://william278.net/docs/husksync/compatibility) of Minecraft. Since v3.7, you must download the correct version of HuskSync for your server:
| Minecraft | Latest HuskSync | Java Version | Platforms | Support Ends |
| Minecraft | Latest HuskSync | Java Version | Platforms | Support Status |
|:---------------:|:---------------:|:------------:|:--------------|:------------------------------|
| 1.21.3 | _latest_ | 21 | Paper, Fabric | ✅ **Active Release** |
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **December 2024** (Non-LTS) |
| 1.20.6 | 3.6.8 | 17 | Paper | _October 2024_ |
| 1.20.4 | 3.6.8 | 17 | Paper | _July 2024_ |
| 1.20.1 | _latest_ | 17 | Paper, Fabric | ✅ **November 2025** (LTS) |
| 1.17.1 - 1.19.4 | 3.6.8 | 17 | Paper | ❌ _Support ended_ |
| 1.16.5 | 3.2.1 | 16 | Paper | ❌ _Support ended_ |
| 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.
Verify your purchase on Discord and [Download HuskSync](https://william278.net/project/husksync/download) for your server.
## Setup
Requires a MySQL/Mongo/PostgreSQL database, a Redis (v5.0+) server and a network of Spigot or Fabric Minecraft servers (see [Compatibility](#compatibility)).
Requires a [MySQL/MariaDB/Mongo/PostgreSQL database](https://william278.net/docs/husksync/database), a [Redis (v5.0+) server]((https://william278.net/docs/husksync/redis)) and a network of [compatible Spigot or Fabric Minecraft servers](https://william278.net/docs/husksync/compatibility).
1. Place the plugin jar file in the `/plugins` or `/mods` directory of each Spigot/Fabric server. You do not need to install HuskSync as a proxy plugin.
2. Start, then stop every server to let HuskSync generate the config file.
@@ -78,6 +83,8 @@ To build HuskSync, simply run the following in the root of the repository (build
./gradlew clean build
```
HuskSync uses `essential-multi-version` (Fabric) and `preprocessor` (Bukkit) to target multiple versions of Minecraft in one codebase - [check here](https://github.com/WiIIiam278/PreProcessor?tab=readme-ov-file#code-example) for a preprocessor comment logic reference.
### License
HuskSync is licensed under the Apache 2.0 license.
@@ -102,4 +109,4 @@ Translations of the plugin locales are welcome to help make the plugin more acce
- [bStats](https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140) &mdash; View plugin metrics
---
&copy; [William278](https://william278.net/), 2023. Licensed under the Apache-2.0 License.
&copy; [William278](https://william278.net/), 2025. Licensed under the Apache-2.0 License.

View File

@@ -1,10 +1,11 @@
import org.apache.tools.ant.filters.ReplaceTokens
plugins {
id 'com.gradleup.shadow' version '8.3.4'
id 'com.gradleup.shadow' version '9.2.2'
id 'org.cadixdev.licenser' version '0.6.1' apply false
id 'fabric-loom' version "$fabric_loom_version" apply false
id 'org.ajoberstar.grgit' version '5.3.0'
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 'java'
}
@@ -18,7 +19,6 @@ ext {
set 'version', version.toString()
set 'description', description.toString()
set 'minecraft_version', minecraft_version.toString()
set 'jedis_version', jedis_version.toString()
set 'mysql_driver_version', mysql_driver_version.toString()
set 'mariadb_driver_version', mariadb_driver_version.toString()
@@ -59,13 +59,17 @@ publishing {
}
allprojects {
// Ignore parent projects (no jars)
if (project.name == 'fabric' || project.name == 'bukkit') {
return
}
apply plugin: 'com.gradleup.shadow'
apply plugin: 'org.cadixdev.licenser'
apply plugin: 'java'
compileJava.options.encoding = 'UTF-8'
compileJava.options.compilerArgs += ['-Xlint:unchecked', '-Xlint:deprecation']
compileJava.options.release.set Integer.parseInt(rootProject.ext.javaVersion)
compileJava.options.release.set 17
javadoc.options.encoding = 'UTF-8'
javadoc.options.addStringOption('Xdoclint:none', '-quiet')
@@ -76,7 +80,6 @@ allprojects {
maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
maven { url 'https://repo.papermc.io/repository/maven-public/' }
maven { url "https://repo.dmulloy2.net/repository/public/" }
maven { url 'https://repo.codemc.io/repository/maven-public/' }
maven { url 'https://repo.minebench.de/' }
maven { url 'https://repo.alessiodp.com/releases/' }
@@ -86,13 +89,10 @@ allprojects {
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.3'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.3'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.3'
}
test {
useJUnitPlatform()
testImplementation(platform("org.junit:junit-bom:6.0.1"))
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testCompileOnly 'org.jetbrains:annotations:26.0.2-1'
}
license {
@@ -101,9 +101,13 @@ allprojects {
newLine = true
}
test {
useJUnitPlatform()
}
processResources {
def tokenMap = rootProject.ext.properties
tokenMap.merge("grgit",'',(s, s2) -> s)
tokenMap.merge("grgit", '', (s, s2) -> s)
filesMatching(['**/*.json', '**/*.yml']) {
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
tokens: tokenMap
@@ -112,12 +116,30 @@ allprojects {
}
subprojects {
if (['fabric'].contains(project.name)) {
apply plugin: 'fabric-loom'
// Ignore parent projects (no jars)
if (['fabric', 'bukkit'].contains(project.name)) {
return
}
// Project naming
version rootProject.version
archivesBaseName = "${rootProject.name}-${project.name.capitalize()}"
def name = "$rootProject.name"
if (rootProject != project.parent) {
name += "-${project.parent.name.capitalize()}"
} else {
name += "-${project.name.capitalize()}"
}
archivesBaseName = name
// Version-specific configuration
if (['fabric', 'bukkit'].contains(project.parent?.name)) {
compileJava.options.release.set 21
version += "+mc.${project.name}"
if (project.parent?.name?.equals('fabric')) {
apply plugin: 'dev.architectury.loom'
}
}
jar {
from '../LICENSE'
@@ -128,13 +150,8 @@ subprojects {
archiveClassifier.set('')
}
// Append the compatible Minecraft version to the version
if (['bukkit', 'paper', 'fabric'].contains(project.name)) {
version += "+mc.${minecraft_version}"
}
// API publishing
if (['common', 'bukkit', 'fabric'].contains(project.name)) {
if (project.name == 'common' || ['fabric', 'bukkit'].contains(project.parent?.name)) {
java {
withSourcesJar()
withJavadocJar()
@@ -161,12 +178,12 @@ subprojects {
}
}
if (['bukkit'].contains(project.name)) {
if (project.parent?.name?.equals('bukkit')) {
publications {
mavenJavaBukkit(MavenPublication) {
"mavenJavaBukkit_${project.name.replace('.', '_')}"(MavenPublication) {
groupId = 'net.william278.husksync'
artifactId = 'husksync-bukkit'
version = "$rootProject.version+${minecraft_version}"
version = "$rootProject.version+$project.name"
artifact shadowJar
artifact sourcesJar
artifact javadocJar
@@ -174,12 +191,12 @@ subprojects {
}
}
if (['fabric'].contains(project.name)) {
if (project.parent?.name?.equals('fabric')) {
publications {
mavenJavaFabric(MavenPublication) {
"mavenJavaFabric_${project.name.replace('.', '_')}"(MavenPublication) {
groupId = 'net.william278.husksync'
artifactId = 'husksync-fabric'
version = "$rootProject.version+${minecraft_version}"
version = "$rootProject.version+$project.name"
artifact remapJar
artifact sourcesJar
artifact javadocJar
@@ -189,19 +206,14 @@ subprojects {
}
}
jar.dependsOn(shadowJar)
jar.dependsOn shadowJar
clean.delete "$rootDir/target"
}
logger.lifecycle("Building HuskSync ${version} by William278 for Minecraft ${minecraft_version}")
logger.lifecycle("Building HuskSync ${version} by William278")
@SuppressWarnings('GrMethodMayBeStatic')
def versionMetadata() {
// If the force-hide-version-meta environment variable is set, return ''
if (System.getProperty('force-hide-version-meta') != null) {
return ''
}
// Require grgit
if (grgit == null) {
return '-unknown'

View File

@@ -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,36 +1,68 @@
plugins {
id 'java'
id 'net.william278.preprocessor' version '1.0'
id 'xyz.jpenilla.run-paper' version '2.3.1'
id 'maven-publish'
}
dependencies {
implementation project(path: ':common')
implementation 'net.william278.uniform:uniform-bukkit:1.2.2'
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:hsldataconverter:1.0'
implementation 'net.william278:mapdataapi:2.0'
implementation 'org.bstats:bstats-bukkit:3.1.0'
implementation 'net.kyori:adventure-platform-bukkit:4.3.4'
implementation 'dev.triumphteam:triumph-gui:3.1.10'
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.14.0'
implementation 'de.tr7zw:item-nbt-api:2.15.5'
compileOnly "org.spigotmc:spigot-api:${bukkit_spigot_api}"
compileOnly 'com.github.retrooper.packetevents:spigot:2.3.0'
compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0'
compileOnly 'org.projectlombok:lombok:1.18.34'
compileOnly 'commons-io:commons-io:2.17.0'
compileOnly 'org.json:json:20240303'
compileOnly "io.papermc.paper:paper-api:${paper_api_version}"
compileOnly 'com.github.retrooper:packetevents-spigot:2.10.1'
compileOnly 'com.github.dmulloy2:ProtocolLib:5.3.0'
compileOnly 'org.projectlombok:lombok:1.18.42'
compileOnly 'commons-io:commons-io:2.21.0'
compileOnly 'org.json:json:20250517'
compileOnly 'net.william278:minedown:1.8.2'
compileOnly 'de.exlll:configlib-yaml:4.5.0'
compileOnly 'com.zaxxer:HikariCP:6.1.0'
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.34'
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 {
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'
@@ -42,6 +74,7 @@ shadowJar {
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
relocate 'de.exlll', 'net.william278.husksync.libraries'
relocate 'net.william278.uniform', 'net.william278.husksync.libraries.uniform'
relocate 'net.william278.toilet', 'net.william278.husksync.libraries.toilet'
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
@@ -57,3 +90,13 @@ shadowJar {
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

@@ -23,7 +23,7 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import de.tr7zw.changeme.nbtapi.utils.DataFixerUtil;
import de.tr7zw.changeme.nbtapi.NBT;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -47,6 +47,8 @@ import net.william278.husksync.database.PostgresDatabase;
import net.william278.husksync.event.BukkitEventDispatcher;
import net.william278.husksync.hook.PlanHook;
import net.william278.husksync.listener.BukkitEventListener;
import net.william278.husksync.listener.LockedHandler;
import net.william278.husksync.maps.BukkitMapHandler;
import net.william278.husksync.migrator.LegacyMigrator;
import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.migrator.MpdbMigrator;
@@ -55,9 +57,10 @@ 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.BukkitMapPersister;
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;
@@ -81,7 +84,7 @@ import java.util.stream.Collectors;
@NoArgsConstructor
@SuppressWarnings("unchecked")
public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier,
BukkitEventDispatcher, BukkitMapPersister {
BukkitEventDispatcher, BukkitMapHandler {
/**
* Metrics ID for <a href="https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140">HuskSync on Bukkit</a>.
@@ -89,17 +92,17 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
private static final int METRICS_ID = 13140;
private static final String PLATFORM_TYPE_ID = "bukkit";
private final TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap(
SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR
);
private final 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 RedisManager redisManager;
@@ -139,10 +142,17 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@Override
public void onEnable() {
this.audiences = BukkitAudiences.create(this);
this.toilet = BukkitToilet.create(getDumpOptions());
// Check compatibility
checkCompatibility();
// Preload NBT-API
if (!NBT.preloadApi()) {
log(Level.SEVERE, "Failed to load NBT API. HuskSync will not be initialized!");
return;
}
// Register commands
initialize("commands", (plugin) -> getUniform().register(PluginCommand.Type.create(this)));
@@ -333,21 +343,6 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
return Version.fromString(getServer().getBukkitVersion());
}
public int getDataVersion(@NotNull Version mcVersion) {
return switch (mcVersion.toStringWithoutMetadata()) {
case "1.16", "1.16.1", "1.16.2", "1.16.3", "1.16.4", "1.16.5" -> DataFixerUtil.VERSION1_16_5;
case "1.17", "1.17.1" -> DataFixerUtil.VERSION1_17_1;
case "1.18", "1.18.1", "1.18.2" -> DataFixerUtil.VERSION1_18_2;
case "1.19", "1.19.1", "1.19.2" -> DataFixerUtil.VERSION1_19_2;
case "1.20", "1.20.1", "1.20.2" -> DataFixerUtil.VERSION1_20_2;
case "1.20.3", "1.20.4" -> DataFixerUtil.VERSION1_20_4;
case "1.20.5", "1.20.6" -> DataFixerUtil.VERSION1_20_5;
case "1.21", "1.21.1" -> DataFixerUtil.VERSION1_21;
case "1.21.2", "1.21.3" -> DataFixerUtil.VERSION1_21_2;
default -> DataFixerUtil.getCurrentVersion();
};
}
@NotNull
@Override
public String getPlatformType() {
@@ -365,6 +360,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
return Optional.of(legacyConverter);
}
@Override
@NotNull
public LockedHandler getLockedHandler() {
return eventListener.getLockedHandler();
}
@NotNull
public GracefulScheduling getScheduler() {
return paperLib.scheduling();

View File

@@ -30,7 +30,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.UUID;
@SuppressWarnings({"unchecked", "unused"})
@SuppressWarnings({"unused"})
public class PaperHuskSync extends BukkitHuskSync {
@NotNull

View File

@@ -34,6 +34,7 @@ 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")
@@ -46,13 +47,20 @@ public class PaperHuskSyncLoader implements PluginLoader {
resolveLibraries(classpathBuilder).stream()
.map(DefaultArtifact::new)
.forEach(artifact -> resolver.addDependency(new Dependency(artifact, null)));
resolver.addRepository(new RemoteRepository.Builder(
"maven", "default", "https://repo.maven.apache.org/maven2/"
).build());
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()) {

View File

@@ -41,6 +41,7 @@ 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;
@@ -194,7 +195,9 @@ public abstract class BukkitData implements Data {
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
user.getPlayer().getEnderChest().setContents(plugin.setMapViews(getContents()));
ItemStack[] fullContents = plugin.setMapViews(getContents());
ItemStack[] enderChestContents = Arrays.copyOf(fullContents, Math.min(fullContents.length, user.getPlayer().getEnderChest().getSize()));
user.getPlayer().getEnderChest().setContents(enderChestContents);
}
}
@@ -482,8 +485,13 @@ public abstract class BukkitData implements Data {
@NotNull Map<String, Map<String, Integer>> map) {
registry.forEach(i -> {
try {
final int stat = i instanceof Material m ? p.getStatistic(id, m) :
(i instanceof EntityType e ? p.getStatistic(id, e) : -1);
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);
@@ -512,8 +520,18 @@ public abstract class BukkitData implements Data {
try {
switch (type) {
case UNTYPED -> player.setStatistic(stat, value);
case BLOCK, ITEM -> player.setStatistic(stat, Objects.requireNonNull(matchMaterial(key[0])), value);
case ENTITY -> player.setStatistic(stat, Objects.requireNonNull(matchEntityType(key[0])), value);
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);
@@ -558,6 +576,14 @@ public abstract class BukkitData implements Data {
@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 -> {
@@ -632,6 +658,15 @@ public abstract class BukkitData implements Data {
@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())) {

View File

@@ -20,7 +20,7 @@
package net.william278.husksync.data;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.util.BukkitMapPersister;
import net.william278.husksync.maps.BukkitMapHandler;
import org.bukkit.entity.Player;
import org.bukkit.inventory.PlayerInventory;
import org.jetbrains.annotations.NotNull;
@@ -31,7 +31,11 @@ public interface BukkitUserDataHolder extends UserDataHolder {
@Override
default Optional<? extends Data> getData(@NotNull Identifier id) {
if (!id.isCustom()) {
if (id.isCustom()) {
return Optional.ofNullable(getCustomDataStore().get(id));
}
try {
return switch (id.getKeyValue()) {
case "inventory" -> getInventory();
case "ender_chest" -> getEnderChest();
@@ -48,8 +52,10 @@ public interface BukkitUserDataHolder extends UserDataHolder {
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();
}
return Optional.ofNullable(getCustomDataStore().get(id));
}
@Override
@@ -163,7 +169,7 @@ public interface BukkitUserDataHolder extends UserDataHolder {
}
@NotNull
default BukkitMapPersister getMapPersister() {
default BukkitMapHandler getMapPersister() {
return (BukkitHuskSync) getPlugin();
}

View File

@@ -19,6 +19,7 @@
package net.william278.husksync.listener;
import lombok.Getter;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.data.BukkitData;
import net.william278.husksync.user.BukkitUser;
@@ -36,6 +37,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.stream.Collectors;
@Getter
public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
BukkitDeathEventListener, Listener {
@@ -133,7 +135,7 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
@EventHandler(ignoreCancelled = true)
public void onMapInitialize(@NotNull MapInitializeEvent event) {
if (plugin.getSettings().getSynchronization().isPersistLockedMaps() && event.getMap().isLocked()) {
getPlugin().runAsync(() -> ((BukkitHuskSync) plugin).renderMapFromFile(event.getMap()));
getPlugin().runAsync(() -> ((BukkitHuskSync) plugin).renderInitializingLockedMap(event.getMap()));
}
}

View File

@@ -40,7 +40,6 @@ import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.UUID;
@Getter
@@ -124,7 +123,6 @@ public class BukkitLockedEventListener implements LockedHandler, Listener {
private void cancelPlayerEvent(@NotNull UUID uuid, @NotNull Cancellable event) {
if (cancelPlayerEvent(uuid)) {
event.setCancelled(true);
plugin.debug("Cancelled event " + event.getClass().getSimpleName() + " from " + Objects.requireNonNull(plugin.getServer().getPlayer(uuid)).getName());
}
}

View File

@@ -40,12 +40,11 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
}
@Override
@SuppressWarnings("UnstableApiUsage")
public void onLoad() {
super.onLoad();
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(getPlugin()));
PacketEvents.getAPI().getSettings().reEncodeByDefault(false)
.checkForUpdates(false)
.bStats(true);
PacketEvents.getAPI().getSettings().reEncodeByDefault(false).checkForUpdates(false);
PacketEvents.getAPI().load();
}
@@ -61,6 +60,7 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
private static final Set<PacketType.Play.Client> ALLOWED_PACKETS = Set.of(
PacketType.Play.Client.KEEP_ALIVE, PacketType.Play.Client.PONG, PacketType.Play.Client.PLUGIN_MESSAGE, // Connection packets
PacketType.Play.Client.PLAYER_LOADED, PacketType.Play.Client.CLIENT_TICK_END, // Connection packets
PacketType.Play.Client.CHAT_MESSAGE, PacketType.Play.Client.CHAT_COMMAND, PacketType.Play.Client.CHAT_SESSION_UPDATE, // Chat / command packets
PacketType.Play.Client.PLAYER_POSITION, PacketType.Play.Client.PLAYER_POSITION_AND_ROTATION, PacketType.Play.Client.PLAYER_ROTATION, // Movement packets
PacketType.Play.Client.HELD_ITEM_CHANGE, PacketType.Play.Client.ANIMATION, PacketType.Play.Client.TELEPORT_CONFIRM, // Animation packets

View File

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

View File

@@ -44,6 +44,7 @@ public class PaperEventListener extends BukkitEventListener {
}
@Override
@SuppressWarnings("RedundantMethodOverride")
public void onEnable() {
getPlugin().getServer().getPluginManager().registerEvents(this, getPlugin());
lockedHandler.onEnable();

View File

@@ -17,15 +17,15 @@
* limitations under the License.
*/
package net.william278.husksync.util;
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.querz.nbt.io.NBTUtil;
import net.querz.nbt.tag.CompoundTag;
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;
@@ -35,27 +35,33 @@ 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.util.List;
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 BukkitMapPersister {
public interface BukkitMapHandler {
// The map used to store HuskSync data in ItemStack NBT
String MAP_DATA_KEY = "husksync:persisted_locked_map";
// The key used to store the serialized map data in NBT
String MAP_PIXEL_DATA_KEY = "canvas_data";
// The key used to store the map of World UIDs to MapView IDs in NBT
String MAP_VIEW_ID_MAPPINGS_KEY = "id_mappings";
// 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
@@ -96,14 +102,109 @@ public interface BukkitMapPersister {
}
if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) {
items[i] = function.apply(item);
} else if (item.getItemMeta() instanceof BlockStateMeta b && b.getBlockState() instanceof Container box) {
} 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());
@@ -131,155 +232,160 @@ public interface BukkitMapPersister {
// Persist map data
final ReadWriteNBT mapData = nbt.getOrCreateCompound(MAP_DATA_KEY);
final String worldUid = view.getWorld().getUID().toString();
mapData.setByteArray(MAP_PIXEL_DATA_KEY, canvas.extractMapData().toBytes());
nbt.getOrCreateCompound(MAP_VIEW_ID_MAPPINGS_KEY).setInteger(worldUid, view.getId());
getPlugin().debug(String.format("Saved data for locked map (#%s, UID: %s)", view.getId(), worldUid));
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 int dataVersion = getPlugin().getDataVersion(getPlugin().getMinecraftVersion());
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);
final ReadableNBT mapIds = nbt.getCompound(MAP_VIEW_ID_MAPPINGS_KEY);
if (mapData == null || mapIds == null) {
if (mapData == null) {
return;
}
// Search for an existing map view
Optional<String> world = Optional.empty();
for (String worldUid : mapIds.getKeys()) {
world = getPlugin().getServer().getWorlds().stream()
.map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid))
.findFirst();
if (world.isPresent()) {
break;
// 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);
}
}
if (world.isPresent()) {
final String uid = world.get();
final Optional<MapView> existingView = this.getMapView(mapIds.getInteger(uid));
if (existingView.isPresent()) {
final MapView view = existingView.get();
view.setLocked(true);
meta.setMapView(view);
map.setItemMeta(meta);
getPlugin().debug(String.format("View exists (#%s); updated map (UID: %s)", view.getId(), uid));
return;
}
}
// Read the pixel data and generate a map view otherwise
final MapData canvasData;
try {
getPlugin().debug("Deserializing map data from NBT and generating view...");
canvasData = MapData.fromByteArray(
dataVersion,
Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY), "Pixel data null!"));
} catch (Throwable e) {
getPlugin().log(Level.WARNING, "Failed to deserialize map data from NBT", e);
return;
}
// Add a renderer to the map with the data and save to file
final MapView view = generateRenderedMap(canvasData);
final String worldUid = getDefaultMapWorld().getUID().toString();
meta.setMapView(view);
map.setItemMeta(meta);
saveMapToFile(canvasData, view.getId());
// Set the map view ID in NBT
NBT.modify(map, editable -> {
Objects.requireNonNull(editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY),
"Map view ID mappings compound is null")
.setInteger(worldUid, view.getId());
});
getPlugin().debug(String.format("Generated view (#%s) and updated map (UID: %s)", view.getId(), worldUid));
});
return map;
}
default void renderMapFromFile(@NotNull MapView view) {
final File mapFile = new File(getMapCacheFolder(), view.getId() + ".dat");
if (!mapFile.exists()) {
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;
}
final MapData canvasData;
try {
canvasData = MapData.fromNbt(mapFile);
} catch (Throwable e) {
getPlugin().log(Level.WARNING, "Failed to deserialize map data from file", e);
Optional<MapView> optionalView = getMapView(newId);
if (optionalView.isPresent()) {
meta.setMapView(optionalView.get());
getPlugin().debug("Map ID set to #%s".formatted(newId));
return;
}
// Create a new map view renderer with the map data color at each pixel
// use view.removeRenderer() to remove all this maps renderers
view.getRenderers().forEach(view::removeRenderer);
view.addRenderer(new PersistentMapRenderer(canvasData));
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.NORMAL);
view.setScale(MapView.Scale.CLOSEST);
view.setTrackingPosition(false);
view.setUnlimitedTracking(false);
// Set the view to the map
setMapView(view);
}
default void saveMapToFile(@NotNull MapData data, int id) {
getPlugin().runAsync(() -> {
final File mapFile = new File(getMapCacheFolder(), id + ".dat");
if (mapFile.exists()) {
return;
}
try {
final CompoundTag rootTag = new CompoundTag();
rootTag.put("data", data.toNBT().getTag());
NBTUtil.write(rootTag, mapFile);
} catch (Throwable e) {
getPlugin().log(Level.WARNING, "Failed to serialize map data to file", e);
}
});
MapData data = readMapData(getPlugin().getServerName(), view.getId());
if (data == null) {
data = readLegacyMapFileData(view.getId());
}
@NotNull
private File getMapCacheFolder() {
final File mapCache = new File(getPlugin().getDataFolder(), "maps");
if (!mapCache.exists() && !mapCache.mkdirs()) {
getPlugin().log(Level.WARNING, "Failed to create maps folder");
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;
}
return mapCache;
renderMapView(view, data);
}
// Sets the renderer of a map, and returns the generated MapView
@NotNull
private MapView generateRenderedMap(@NotNull MapData canvasData) {
final MapView view = Bukkit.createMap(getDefaultMapWorld());
view.getRenderers().clear();
return generateRenderedMap(canvasData, Bukkit.createMap(getDefaultMapWorld()));
}
// Create a new map view renderer with the map data color at each pixel
@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.NORMAL);
view.setScale(MapView.Scale.CLOSEST);
view.setTrackingPosition(false);
view.setUnlimitedTracking(false);
// Set the view to the map and return it
setMapView(view);
return view;
}
@NotNull
private static World getDefaultMapWorld() {
final World world = Bukkit.getWorlds().getFirst();
final World world = Bukkit.getWorlds().get(0);
if (world == null) {
throw new IllegalStateException("No worlds are loaded on the server!");
}
@@ -356,12 +462,44 @@ public interface BukkitMapPersister {
);
}
// 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")
@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];
@@ -451,7 +589,6 @@ public interface BukkitMapPersister {
@NotNull
private MapData extractMapData() {
final List<MapBanner> banners = Lists.newArrayList();
final String BANNER_PREFIX = "banner_";
for (int i = 0; i < getCursors().size(); i++) {
final MapCursor cursor = getCursors().getCursor(i);
final String type = cursor.getType().getKey().getKey();
@@ -466,13 +603,16 @@ public interface BukkitMapPersister {
}
}
return MapData.fromPixels(mapDataVersion, pixels, getDimension(), (byte) 2, banners, List.of());
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

@@ -146,7 +146,7 @@ public class LegacyMigrator extends Migrator {
try {
plugin.getDatabase().addSnapshot(data.user(), convertedData);
} catch (Throwable e) {
plugin.log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().getUsername() + ": " + e.getMessage());
plugin.log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().getName() + ": " + e.getMessage());
return;
}

View File

@@ -57,8 +57,9 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
}
@Override
public boolean isOffline() {
return player == null || !player.isOnline();
public boolean hasDisconnected() {
return getPlugin().getDisconnectingPlayers().contains(getUuid())
|| player == null || !player.isOnline();
}
@Override

View File

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

View File

@@ -5,7 +5,7 @@ website: 'https://william278.net/'
main: 'net.william278.husksync.PaperHuskSync'
loader: 'net.william278.husksync.PaperHuskSyncLoader'
version: '${version}'
api-version: '1.19'
api-version: '${minecraft_api_version}'
folia-supported: true
dependencies:
server:

View File

@@ -1,7 +1,7 @@
name: 'HuskSync'
version: '${version}'
main: 'net.william278.husksync.BukkitHuskSync'
api-version: 1.17
api-version: '${minecraft_api_version}'
author: 'William278'
description: '${description}'
website: 'https://william278.net'

View File

@@ -3,27 +3,31 @@ plugins {
}
dependencies {
api 'commons-io:commons-io:2.17.0'
api 'org.apache.commons:commons-text:1.12.0'
api 'commons-io:commons-io:2.21.0'
api 'org.apache.commons:commons-text:1.14.0'
api 'net.william278:minedown:1.8.2'
api 'org.json:json:20240303'
api 'com.google.code.gson:gson:2.11.0'
api 'net.william278:mapdataapi:2.0'
api 'org.json:json:20250517'
api 'com.google.code.gson:gson:2.13.2'
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
api 'de.exlll:configlib-yaml:4.5.0'
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:6.1.0') {
api('com.zaxxer:HikariCP:7.0.2') {
exclude module: 'slf4j-api'
}
compileOnly 'net.william278.uniform:uniform-common:1.2.2'
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.34'
compileOnly 'org.jetbrains:annotations:26.0.1'
compileOnly 'net.kyori:adventure-api:4.17.0'
compileOnly 'net.kyori:adventure-platform-api:4.3.4'
compileOnly 'com.google.guava:guava:33.3.1-jre'
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
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"
@@ -33,10 +37,10 @@ dependencies {
testImplementation "redis.clients:jedis:$jedis_version"
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
testImplementation 'com.google.guava:guava:33.3.1-jre'
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
testCompileOnly 'de.exlll:configlib-yaml:4.5.0'
testCompileOnly 'org.jetbrains:annotations:26.0.1'
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.34'
annotationProcessor 'org.projectlombok:lombok:1.18.42'
}

View File

@@ -20,6 +20,7 @@
package net.william278.husksync;
import com.fatboyindustrial.gsonjavatime.Converters;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.kyori.adventure.audience.Audience;
@@ -34,14 +35,13 @@ import net.william278.husksync.data.Identifier;
import net.william278.husksync.data.SerializerRegistry;
import net.william278.husksync.database.Database;
import net.william278.husksync.event.EventDispatcher;
import net.william278.husksync.listener.LockedHandler;
import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.sync.DataSyncer;
import net.william278.husksync.user.ConsoleUser;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.util.CompatibilityChecker;
import net.william278.husksync.util.LegacyConverter;
import net.william278.husksync.util.Task;
import net.william278.husksync.util.*;
import net.william278.uniform.Uniform;
import org.jetbrains.annotations.NotNull;
@@ -54,7 +54,7 @@ import java.util.logging.Level;
* Abstract implementation of the HuskSync plugin.
*/
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider, SerializerRegistry,
CompatibilityChecker {
CompatibilityChecker, DumpProvider, DataVersionSupplier {
int SPIGOT_RESOURCE_ID = 97144;
@@ -138,7 +138,7 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
if (getPlayerCustomDataStore().containsKey(user.getUuid())) {
return getPlayerCustomDataStore().get(user.getUuid());
}
final Map<Identifier, Data> data = new HashMap<>();
final Map<Identifier, Data> data = Maps.newHashMap();
getPlayerCustomDataStore().put(user.getUuid(), data);
return data;
}
@@ -249,14 +249,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
@NotNull
Version getMinecraftVersion();
/**
* Returns the data version for a Minecraft version
*
* @param minecraftVersion the Minecraft version
* @return the data version int
*/
int getDataVersion(@NotNull Version minecraftVersion);
/**
* Returns the platform type
*
@@ -302,6 +294,9 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
}
}
@NotNull
LockedHandler getLockedHandler();
/**
* Get the set of UUIDs of "locked players", for which events will be canceled.
* </p>
@@ -310,6 +305,12 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
@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);
}

View File

@@ -99,6 +99,18 @@ public class HuskSyncAPI {
return plugin.supplyAsync(() -> plugin.getDatabase().getUser(uuid));
}
/**
* Get an {@link OnlineUser} by their UUID
*
* @param uuid the UUID of the user to get
* @return The {@link OnlineUser} wrapped in an optional, if they are online on <i>this</i> server.
* @since 3.7.2
*/
@NotNull
public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
return plugin.getOnlineUser(uuid);
}
/**
* Get a {@link User} by their username
*
@@ -137,7 +149,7 @@ public class HuskSyncAPI {
*/
public CompletableFuture<Optional<DataSnapshot.Unpacked>> getCurrentData(@NotNull User user) {
return plugin.getRedisManager()
.getUserData(UUID.randomUUID(), user)
.getOnlineUserData(UUID.randomUUID(), user, DataSnapshot.SaveCause.API)
.thenApply(data -> data.or(() -> plugin.getDatabase().getLatestSnapshot(user)))
.thenApply(data -> data.map(snapshot -> snapshot.unpack(plugin)));
}

View File

@@ -23,7 +23,6 @@ import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.Data;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.redis.RedisKeyType;
import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
@@ -37,7 +36,7 @@ import java.util.Optional;
public class EnderChestCommand extends ItemsCommand {
public EnderChestCommand(@NotNull HuskSync plugin) {
super("enderchest", List.of("echest", "openechest"), plugin);
super("enderchest", List.of("echest", "openechest"), DataSnapshot.SaveCause.ENDERCHEST_COMMAND, plugin);
}
@Override
@@ -51,7 +50,7 @@ public class EnderChestCommand extends ItemsCommand {
}
// Display opening message
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(),
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getName(),
snapshot.getTimestamp().format(DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage);
@@ -60,8 +59,8 @@ public class EnderChestCommand extends ItemsCommand {
final Data.Items.EnderChest enderChest = optionalEnderChest.get();
viewer.showGui(
enderChest,
plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getUsername())
.orElse(new MineDown(String.format("%s's Ender Chest", user.getUsername()))),
plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getName())
.orElse(new MineDown(String.format("%s's Ender Chest", user.getName()))),
allowEdit,
enderChest.getSlotCount(),
(itemsOnClose) -> {
@@ -84,18 +83,17 @@ public class EnderChestCommand extends ItemsCommand {
// Create and pack the snapshot with the updated enderChest
final DataSnapshot.Packed snapshot = latestData.get().copy();
boolean pin = plugin.getSettings().getSynchronization().doAutoPin(saveCause);
snapshot.edit(plugin, (data) -> {
data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items));
data.setSaveCause(DataSnapshot.SaveCause.ENDERCHEST_COMMAND);
data.setPinned(
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND)
);
data.setSaveCause(saveCause);
data.setPinned(pin);
});
// Save data
final RedisManager redis = plugin.getRedisManager();
plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot));
redis.sendUserDataUpdate(user, data);
});
}

View File

@@ -24,9 +24,10 @@ 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.HoverEvent;
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;
@@ -35,18 +36,17 @@ import net.william278.husksync.database.Database;
import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.util.LegacyConverter;
import net.william278.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.apache.commons.text.WordUtils;
import org.jetbrains.annotations.NotNull;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -69,7 +69,8 @@ public class HuskSyncCommand extends PluginCommand {
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("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)"),
@@ -98,6 +99,7 @@ public class HuskSyncCommand extends PluginCommand {
command.setDefaultExecutor((ctx) -> about(command, ctx));
command.addSubCommand("about", (sub) -> sub.setDefaultExecutor((ctx) -> about(command, ctx)));
command.addSubCommand("status", needsOp("status"), status());
command.addSubCommand("dump", needsOp("dump"), dump());
command.addSubCommand("reload", needsOp("reload"), reload());
command.addSubCommand("update", needsOp("update"), update());
command.addSubCommand("forceupgrade", forceUpgrade());
@@ -120,6 +122,26 @@ public class HuskSyncCommand extends PluginCommand {
});
}
@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) -> {
@@ -234,86 +256,4 @@ public class HuskSyncCommand extends PluginCommand {
});
}
private enum StatusLine {
PLUGIN_VERSION(plugin -> Component.text("v" + plugin.getPluginVersion().toStringWithoutMetadata())
.appendSpace().append(plugin.getPluginVersion().getMetadata().isBlank() ? Component.empty()
: Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))),
SERVER_VERSION(plugin -> Component.text(plugin.getServerVersion())),
LANGUAGE(plugin -> Component.text(plugin.getSettings().getLanguage())),
MINECRAFT_VERSION(plugin -> Component.text(plugin.getMinecraftVersion().toString())),
JAVA_VERSION(plugin -> Component.text(System.getProperty("java.version"))),
JAVA_VENDOR(plugin -> Component.text(System.getProperty("java.vendor"))),
SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())),
SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully(
plugin.getSettings().getSynchronization().getMode().toString()
))),
DELAY_LATENCY(plugin -> Component.text(
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms"
)),
DATABASE_TYPE(plugin ->
Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() +
(plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ?
(plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : ""))
),
IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())),
USING_REDIS_SENTINEL(plugin -> getBoolean(
!plugin.getSettings().getRedis().getSentinel().getMaster().isBlank()
)),
USING_REDIS_PASSWORD(plugin -> getBoolean(
!plugin.getSettings().getRedis().getCredentials().getPassword().isBlank()
)),
REDIS_USING_SSL(plugin -> getBoolean(
plugin.getSettings().getRedis().getCredentials().isUseSsl()
)),
IS_REDIS_LOCAL(plugin -> getLocalhostBoolean(
plugin.getSettings().getRedis().getCredentials().getHost()
)),
DATA_TYPES(plugin -> Component.join(
JoinConfiguration.commas(true),
plugin.getRegisteredDataTypes().stream().map(i -> Component.textOfChildren(Component.text(i.toString())
.appendSpace().append(Component.text(i.isEnabled() ? '✔' : '❌')))
.color(i.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED)
.hoverEvent(HoverEvent.showText(
Component.text(i.isEnabled() ? "Enabled" : "Disabled")
.append(Component.newline())
.append(Component.text("Dependencies: %s".formatted(i.getDependencies()
.isEmpty() ? "(None)" : i.getDependencies().stream()
.map(d -> "%s (%s)".formatted(
d.getKey().value(), d.isRequired() ? "Required" : "Optional"
)).collect(Collectors.joining(", ")))
).color(NamedTextColor.GRAY))
))).toList()
));
private final Function<HuskSync, Component> supplier;
StatusLine(@NotNull Function<HuskSync, Component> supplier) {
this.supplier = supplier;
}
@NotNull
private Component get(@NotNull HuskSync plugin) {
return Component
.text("").appendSpace()
.append(Component.text(
WordUtils.capitalizeFully(name().replaceAll("_", " ")),
TextColor.color(0x848484)
))
.append(Component.text(':')).append(Component.space().color(NamedTextColor.WHITE))
.append(supplier.apply(plugin));
}
@NotNull
private static Component getBoolean(boolean value) {
return Component.text(value ? "Yes" : "No", value ? NamedTextColor.GREEN : NamedTextColor.RED);
}
@NotNull
private static Component getLocalhostBoolean(@NotNull String value) {
return getBoolean(value.equals("127.0.0.1") || value.equals("0.0.0.0")
|| value.equals("localhost") || value.equals("::1"));
}
}
}

View File

@@ -23,7 +23,6 @@ import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.Data;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.redis.RedisKeyType;
import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
@@ -37,7 +36,7 @@ import java.util.Optional;
public class InventoryCommand extends ItemsCommand {
public InventoryCommand(@NotNull HuskSync plugin) {
super("inventory", List.of("invsee", "openinv"), plugin);
super("inventory", List.of("invsee", "openinv"), DataSnapshot.SaveCause.INVENTORY_COMMAND, plugin);
}
@Override
@@ -45,14 +44,13 @@ public class InventoryCommand extends ItemsCommand {
@NotNull User user, boolean allowEdit) {
final Optional<Data.Items.Inventory> optionalInventory = snapshot.getInventory();
if (optionalInventory.isEmpty()) {
viewer.sendMessage(new MineDown("what the FUCK is happening"));
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
return;
}
// Display opening message
plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(),
plugin.getLocales().getLocale("inventory_viewer_opened", user.getName(),
snapshot.getTimestamp().format(DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage);
@@ -61,8 +59,8 @@ public class InventoryCommand extends ItemsCommand {
final Data.Items.Inventory inventory = optionalInventory.get();
viewer.showGui(
inventory,
plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getUsername())
.orElse(new MineDown(String.format("%s's Inventory", user.getUsername()))),
plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getName())
.orElse(new MineDown(String.format("%s's Inventory", user.getName()))),
allowEdit,
inventory.getSlotCount(),
(itemsOnClose) -> {
@@ -85,18 +83,17 @@ public class InventoryCommand extends ItemsCommand {
// Create and pack the snapshot with the updated inventory
final DataSnapshot.Packed snapshot = latestData.get().copy();
boolean pin = plugin.getSettings().getSynchronization().doAutoPin(saveCause);
snapshot.edit(plugin, (data) -> {
data.getInventory().ifPresent(inventory -> inventory.setContents(items));
data.setSaveCause(DataSnapshot.SaveCause.INVENTORY_COMMAND);
data.setPinned(
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND)
);
data.setSaveCause(saveCause);
data.setPinned(pin);
});
// Save data
final RedisManager redis = plugin.getRedisManager();
plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot));
redis.sendUserDataUpdate(user, data);
});
}

View File

@@ -34,8 +34,12 @@ import java.util.UUID;
public abstract class ItemsCommand extends PluginCommand {
protected ItemsCommand(@NotNull String name, @NotNull List<String> aliases, @NotNull HuskSync plugin) {
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
@@ -50,7 +54,7 @@ public abstract class ItemsCommand extends PluginCommand {
return;
}
this.showSnapshotItems(online, user, version);
}, user("username"), uuid("version"));
}, user("username"), versionUuid());
command.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final CommandUser executor = user(command, ctx);
@@ -65,7 +69,7 @@ public abstract class ItemsCommand extends PluginCommand {
// View (and edit) the latest user data
private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) {
plugin.getRedisManager().getUserData(user.getUuid(), user).thenAccept(data -> data
plugin.getRedisManager().getOnlineUserData(user.getUuid(), user, saveCause).thenAccept(d -> d
.or(() -> plugin.getDatabase().getLatestSnapshot(user))
.or(() -> {
plugin.getLocales().getLocale("error_no_data_to_display")

View File

@@ -23,6 +23,7 @@ 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;
@@ -31,6 +32,7 @@ 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;
@@ -75,6 +77,20 @@ public abstract class PluginCommand extends Command {
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 -> {
@@ -83,20 +99,29 @@ public abstract class PluginCommand extends Command {
() -> CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader)
);
}, (context, builder) -> {
plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getUsername()));
plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getName()));
return builder.buildFuture();
});
}
@NotNull
protected <S> ArgumentElement<S, UUID> uuid(@NotNull String name) {
return new ArgumentElement<>(name, reader -> {
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) -> builder.buildFuture());
}, (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 {

View File

@@ -20,15 +20,19 @@
package net.william278.husksync.command;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.redis.RedisKeyType;
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.DataDumper;
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;
@@ -52,6 +56,7 @@ public class UserDataCommand extends PluginCommand {
command.addSubCommand("view", needsOp("view"), view());
command.addSubCommand("list", needsOp("list"), list());
command.addSubCommand("delete", needsOp("delete"), delete());
command.addSubCommand("save", needsOp("save"), save());
command.addSubCommand("restore", needsOp("restore"), restore());
command.addSubCommand("pin", needsOp("pin"), pin());
command.addSubCommand("dump", needsOp("dump"), dump());
@@ -102,6 +107,13 @@ public class UserDataCommand extends PluginCommand {
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)) {
@@ -113,7 +125,7 @@ public class UserDataCommand extends PluginCommand {
plugin.getLocales().getLocale("data_deleted",
version.toString().split("-")[0],
version.toString(),
user.getUsername(),
user.getName(),
user.getUuid().toString())
.ifPresent(executor::sendMessage);
}
@@ -145,9 +157,9 @@ public class UserDataCommand extends PluginCommand {
// Save data
final RedisManager redis = plugin.getRedisManager();
plugin.getDataSyncer().saveData(user, data, (u, s) -> {
redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s, RedisKeyType.TTL_1_YEAR));
redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s));
redis.sendUserDataUpdate(u, s);
plugin.getLocales().getLocale("data_restored", u.getUsername(), u.getUuid().toString(),
plugin.getLocales().getLocale("data_restored", u.getName(), u.getUuid().toString(),
s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage);
});
}
@@ -169,11 +181,11 @@ public class UserDataCommand extends PluginCommand {
plugin.getDatabase().pinSnapshot(user, data.getId());
}
plugin.getLocales().getLocale(data.isPinned() ? "data_unpinned" : "data_pinned", data.getShortId(),
data.getId().toString(), user.getUsername(), user.getUuid().toString())
data.getId().toString(), user.getName(), user.getUuid().toString())
.ifPresent(executor::sendMessage);
}
// Dump a snapshot
// Lookup a snapshot by UUID and dump
private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version,
@NotNull DumpType type) {
final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, version);
@@ -182,14 +194,20 @@ public class UserDataCommand extends PluginCommand {
.ifPresent(executor::sendMessage);
return;
}
this.dumpSnapshot(executor, user, data.get(), type);
}
// Dump the data
final DataSnapshot.Packed userData = data.get();
final DataDumper dumper = DataDumper.create(userData, user, plugin);
// Dump a snapshot
private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user,
@NotNull DataSnapshot.Packed userData, @NotNull DumpType type) {
final UserDataDumper dumper = UserDataDumper.create(userData, user, plugin);
try {
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getUsername(),
(type == DumpType.WEB ? dumper.toWeb() : dumper.toFile()))
final String url = type == DumpType.WEB ? dumper.toWeb() : dumper.toFile();
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getName())
.ifPresent(executor::sendMessage);
executor.sendMessage(Component.text(url)
.clickEvent(type == DumpType.WEB ? ClickEvent.openUrl(url) : ClickEvent.copyToClipboard(url))
.decorate(TextDecoration.UNDERLINED).color(NamedTextColor.GRAY));
} catch (Throwable e) {
plugin.log(Level.SEVERE, "Failed to dump user data", e);
}
@@ -198,15 +216,15 @@ public class UserDataCommand extends PluginCommand {
@NotNull
private CommandProvider view() {
return (sub) -> {
sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
viewLatestSnapshot(user(sub, ctx), user);
}, user("username"));
sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
viewSnapshot(user(sub, ctx), user, version);
}, user("username"), uuid("version"));
}, user("username"), versionUuid());
sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
viewLatestSnapshot(user(sub, ctx), user);
}, user("username"));
};
}
@@ -231,7 +249,15 @@ public class UserDataCommand extends PluginCommand {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
deleteSnapshot(user(sub, ctx), user, version);
}, user("username"), uuid("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
@@ -240,7 +266,7 @@ public class UserDataCommand extends PluginCommand {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
restoreSnapshot(user(sub, ctx), user, version);
}, user("username"), uuid("version"));
}, user("username"), versionUuid());
}
@NotNull
@@ -249,17 +275,32 @@ public class UserDataCommand extends PluginCommand {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
pinSnapshot(user(sub, ctx), user, version);
}, user("username"), uuid("version"));
}, user("username"), versionUuid());
}
@NotNull
private CommandProvider dump() {
return (sub) -> sub.addSyntax((ctx) -> {
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"), uuid("version"), dumpType());
}, user("username"), versionUuid(), dumpType());
};
}
private <S> ArgumentElement<S, DumpType> dumpType() {

View File

@@ -22,13 +22,11 @@ package net.william278.husksync.config;
import de.exlll.configlib.Configuration;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.nio.file.Path;
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@@ -67,4 +65,8 @@ public class Server {
return super.equals(other);
}
public String getName() {
final String envServerName = System.getenv("HUSKSYNC_SERVER_NAME");
return envServerName == null ? name : envServerName;
}
}

View File

@@ -150,7 +150,7 @@ public class Settings {
}
}
// 𝓡𝓮𝓭𝓲𝓼 settings
// Redis settings
@Comment("Redis settings")
private RedisSettings redis = new RedisSettings();
@@ -159,7 +159,9 @@ public class Settings {
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class RedisSettings {
@Comment("Specify the credentials of your Redis database here. Set \"password\" to '' if you don't have one")
@Comment({"Specify the credentials of your Redis server here.",
"Set \"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
@@ -168,8 +170,49 @@ public class Settings {
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!")
@@ -321,6 +364,9 @@ public class Settings {
@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());
}

View File

@@ -31,7 +31,10 @@ public interface DataHolder {
Map<Identifier, Data> getData();
default Optional<? extends Data> getData(@NotNull Identifier id) {
return getData().entrySet().stream().filter(e -> e.getKey().equals(id)).map(Map.Entry::getValue).findFirst();
if (getData().containsKey(id)) {
return Optional.of(getData().get(id));
}
return Optional.empty();
}
default void setData(@NotNull Identifier identifier, @NotNull Data data) {

View File

@@ -370,7 +370,7 @@ public class DataSnapshot {
public static class Unpacked extends DataSnapshot implements DataHolder {
@Expose(serialize = false, deserialize = false)
private final TreeMap<Identifier, Data> deserialized;
private final Map<Identifier, Data> deserialized;
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
@@ -381,7 +381,7 @@ public class DataSnapshot {
}
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
@NotNull String saveCause, @NotNull String serverName, @NotNull TreeMap<Identifier, Data> data,
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<Identifier, Data> data,
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
super(id, pinned, timestamp, saveCause, serverName, Map.of(), minecraftVersion, platformType, formatVersion);
this.deserialized = data;
@@ -389,14 +389,15 @@ public class DataSnapshot {
@NotNull
@ApiStatus.Internal
private TreeMap<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
private Map<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
return data.entrySet().stream()
.filter(e -> plugin.getIdentifier(e.getKey()).isPresent())
.map(entry -> Map.entry(plugin.getIdentifier(entry.getKey()).orElseThrow(), entry.getValue()))
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> plugin.deserializeData(entry.getKey(), entry.getValue(), getMinecraftVersion()),
(a, b) -> b, () -> Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR)
(a, b) -> a,
HashMap::new
));
}
@@ -406,7 +407,9 @@ public class DataSnapshot {
return deserialized.entrySet().stream()
.collect(Collectors.toMap(
entry -> entry.getKey().toString(),
entry -> plugin.serializeData(entry.getKey(), entry.getValue())
entry -> plugin.serializeData(entry.getKey(), entry.getValue()),
(a, b) -> a,
HashMap::new
));
}
@@ -421,6 +424,20 @@ public class DataSnapshot {
return deserialized;
}
/**
* Get a sorted iterable of the snapshots the snapshot is holding
*
* @return The data map
* @since 3.8.2
*/
@NotNull
@ApiStatus.Internal
public Iterable<Map.Entry<Identifier, Data>> getSortedIterable() {
final TreeMap<Identifier, Data> tree = Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR);
tree.putAll(deserialized);
return tree.entrySet();
}
/**
* Pack the {@link DataSnapshot} into a {@link DataSnapshot.Packed packed} snapshot
*
@@ -453,12 +470,12 @@ public class DataSnapshot {
private String serverName;
private boolean pinned;
private OffsetDateTime timestamp;
private final TreeMap<Identifier, Data> data;
private final Map<Identifier, Data> data;
private Builder(@NotNull HuskSync plugin) {
this.plugin = plugin;
this.pinned = false;
this.data = Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR);
this.data = Maps.newHashMap();
this.timestamp = OffsetDateTime.now();
this.id = UUID.randomUUID();
this.serverName = plugin.getServerName();
@@ -880,6 +897,20 @@ public class DataSnapshot {
*/
public static final SaveCause BACKUP_RESTORE = of("BACKUP_RESTORE");
/**
* Indicates data was saved from executing the {@code /userdata save} command
*
* @since 3.8
*/
public static final SaveCause SAVE_COMMAND = of("SAVE_COMMAND", true);
/**
* Indicates data was saved from executing the {@code /userdata dump} command
*
* @since 3.8
*/
public static final SaveCause DUMP_COMMAND = of("DUMP_COMMAND", true);
/**
* Indicates data was saved by an API call
*
@@ -923,7 +954,7 @@ public class DataSnapshot {
*/
@NotNull
public static SaveCause of(@NotNull String name) {
return of(name,true);
return of(name, true);
}
/**

View File

@@ -22,9 +22,11 @@ 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;
@@ -36,7 +38,10 @@ import java.util.stream.Stream;
* Identifiers of different types of {@link Data}s
*/
@Getter
public class Identifier {
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);
@@ -92,8 +97,8 @@ public class Identifier {
*/
@NotNull
public static Identifier from(@NotNull Key key, @NotNull Set<Dependency> dependencies) {
if (key.namespace().equals("husksync")) {
throw new IllegalArgumentException("You cannot register a key with \"husksync\" as the namespace!");
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);
}
@@ -142,7 +147,7 @@ public class Identifier {
@NotNull
private static Identifier huskSync(@Subst("null") @NotNull String name,
boolean configDefault) throws InvalidKeyException {
return new Identifier(Key.key("husksync", name), configDefault, Collections.emptySet());
return new Identifier(Key.key(DEFAULT_NAMESPACE, name), configDefault, Collections.emptySet());
}
// Return an identifier with a HuskSync namespace
@@ -150,7 +155,7 @@ public class Identifier {
private static Identifier huskSync(@Subst("null") @NotNull String name,
@SuppressWarnings("SameParameterValue") boolean configDefault,
@NotNull Dependency... dependents) throws InvalidKeyException {
return new Identifier(Key.key("husksync", name), configDefault, Set.of(dependents));
return new Identifier(Key.key(DEFAULT_NAMESPACE, name), configDefault, Set.of(dependents));
}
/**
@@ -208,13 +213,30 @@ public class Identifier {
* @return {@code false} if {@link #getKeyNamespace()} returns "husksync"; {@code true} otherwise
*/
public boolean isCustom() {
return !getKeyNamespace().equals("husksync");
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
@@ -223,25 +245,45 @@ public class Identifier {
}
/**
* Returns {@code true} if the given object is an identifier with the same key as this identifier
* Return whether this Identifier is equal to another Identifier
*
* @param obj the object to compare
* @return {@code true} if the given object is an identifier with the same key as this identifier
* @param obj another object
* @return {@code true} if this identifier matches the identifier of {@code obj}
* @since 3.8
*/
@Override
public boolean equals(Object obj) {
public boolean equals(@Nullable Object obj) {
if (obj instanceof Identifier other) {
return key.equals(other.key);
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>
@@ -315,6 +357,11 @@ public class Identifier {
}
return false;
}
@Override
public int hashCode() {
return key.toString().hashCode();
}
}
}

View File

@@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;
public interface SerializerRegistry {
@@ -40,7 +41,7 @@ public interface SerializerRegistry {
* @since 3.0
*/
@NotNull
<T extends Data> TreeMap<Identifier, Serializer<T>> getSerializers();
<T extends Data> Map<Identifier, Serializer<T>> getSerializers();
/**
* Register a data serializer for the given {@link Identifier}
@@ -87,8 +88,7 @@ public interface SerializerRegistry {
* @since 3.0
*/
default Optional<Identifier> getIdentifier(@NotNull String key) {
return getSerializers().keySet().stream()
.filter(id -> id.getKey().asString().equals(key)).findFirst();
return getSerializers().keySet().stream().filter(e -> e.toString().equals(key)).findFirst();
}
/**
@@ -99,9 +99,7 @@ public interface SerializerRegistry {
* @since 3.5.4
*/
default Optional<Serializer<Data>> getSerializer(@NotNull Identifier identifier) {
return getSerializers().entrySet().stream()
.filter(entry -> entry.getKey().getKey().equals(identifier.getKey()))
.map(Map.Entry::getValue).findFirst();
return Optional.ofNullable(getSerializers().get(identifier));
}
/**
@@ -153,14 +151,14 @@ public interface SerializerRegistry {
}
/**
* Get the set of registered data types
* Get the list of registered data types, in dependency order
*
* @return the set of registered data types
* @return the list of registered data types
* @since 3.0
*/
@NotNull
default Set<Identifier> getRegisteredDataTypes() {
return getSerializers().keySet();
default List<Identifier> getRegisteredDataTypes() {
return getSerializers().keySet().stream().sorted(DEPENDENCY_ORDER_COMPARATOR).toList();
}
// Returns if a data type is available and enabled in the config

View File

@@ -27,6 +27,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.stream.Collectors;
/**
* A holder of data in the form of {@link Data}s, which can be synced
@@ -46,7 +47,11 @@ public interface UserDataHolder extends DataHolder {
.filter(Identifier::isEnabled)
.map(id -> Map.entry(id, getData(id)))
.filter(data -> data.getValue().isPresent())
.collect(HashMap::new, (map, data) -> map.put(data.getKey(), data.getValue().get()), HashMap::putAll);
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().get(),
(a, b) -> a, HashMap::new
));
}
/**
@@ -75,6 +80,15 @@ public interface UserDataHolder extends DataHolder {
return DataSnapshot.builder(getPlugin()).data(this.getData()).saveCause(saveCause).buildAndPack();
}
/**
* Returns whether data can be applied to the holder at this time
*
* @return {@code true} if data can be applied, otherwise false
*/
default boolean cannotApplySnapshot() {
return false;
}
/**
* Deserialize and apply a data snapshot to this data owner
* <p>
@@ -90,9 +104,12 @@ public interface UserDataHolder extends DataHolder {
* @since 3.0
*/
default void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull ThrowingConsumer<Boolean> runAfter) {
final HuskSync plugin = getPlugin();
if (cannotApplySnapshot()) {
return;
}
// Unpack the snapshot
final HuskSync plugin = getPlugin();
final DataSnapshot.Unpacked unpacked;
try {
unpacked = snapshot.unpack(plugin);
@@ -104,8 +121,12 @@ public interface UserDataHolder extends DataHolder {
// Synchronously attempt to apply the snapshot
plugin.runSync(() -> {
if (cannotApplySnapshot()) {
return;
}
try {
for (Map.Entry<Identifier, Data> entry : unpacked.getData().entrySet()) {
for (Map.Entry<Identifier, Data> entry : unpacked.getSortedIterable()) {
final Identifier identifier = entry.getKey();
if (!identifier.isEnabled()) {
continue;

View File

@@ -24,8 +24,10 @@ import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -56,8 +58,8 @@ public abstract class Database {
@SuppressWarnings("SameParameterValue")
@NotNull
protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException {
return formatStatementTables(new String(Objects.requireNonNull(plugin.getResource(schemaFileName))
.readAllBytes(), StandardCharsets.UTF_8)).split(";");
return Arrays.stream(formatStatementTables(new String(Objects.requireNonNull(plugin.getResource(schemaFileName))
.readAllBytes(), StandardCharsets.UTF_8)).split(";")).filter(s -> !s.isBlank()).toArray(String[]::new);
}
/**
@@ -67,10 +69,12 @@ public abstract class Database {
* @return the formatted statement, with table placeholders replaced with the correct names
*/
@NotNull
protected final String formatStatementTables(@NotNull String sql) {
protected final String formatStatementTables(@NotNull @Language("SQL") String sql) {
final Settings.DatabaseSettings settings = plugin.getSettings().getDatabase();
return sql.replaceAll("%users_table%", settings.getTableName(TableName.USERS))
.replaceAll("%user_data_table%", settings.getTableName(TableName.USER_DATA));
.replaceAll("%user_data_table%", settings.getTableName(TableName.USER_DATA))
.replaceAll("%map_data_table%", settings.getTableName(TableName.MAP_DATA))
.replaceAll("%map_ids_table%", settings.getTableName(TableName.MAP_IDS));
}
/**
@@ -135,6 +139,15 @@ public abstract class Database {
@NotNull
public abstract List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user);
/**
* Get the number of unpinned {@link DataSnapshot}s a user has
*
* @param user the user to count snapshots for
* @return the number of snapshots this user has saved
*/
@Blocking
public abstract int getUnpinnedSnapshotCount(@NotNull User user);
/**
* Gets a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
*
@@ -246,6 +259,58 @@ public abstract class Database {
});
}
/**
* Write map data to a database
*
* @param serverName Name of the server the map originates from
* @param mapId Original map ID
* @param data Map data
*/
@Blocking
public abstract void saveMapData(@NotNull String serverName, int mapId, byte @NotNull [] data);
/**
* Read map data from a database
*
* @param serverName Name of the server the map originates from
* @param mapId Original map ID
* @return the map data bytes, or null if not found
*/
@Blocking
public abstract byte @Nullable [] getMapData(@NotNull String serverName, int mapId);
/**
* Reverse lookup: given a local map binding, find the origin server and map ID.
*
* @param serverName Name of the local server (to_server_name in the binding)
* @param mapId Local map ID on this server (to_id in the binding)
* @return Map.Entry with origin server name (key) and origin map ID (value), or null if not found
*/
@Blocking
public abstract @Nullable Map.Entry<String, Integer> getMapBinding(@NotNull String serverName, int mapId);
/**
* Bind map IDs across different servers
*
* @param fromServerName Name of the server the map originates from
* @param fromMapId Original map ID
* @param toServerName Name of the new server
* @param toMapId New map ID
*/
@Blocking
public abstract void setMapBinding(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName, int toMapId);
/**
* Get map ID for the new server
*
* @param fromServerName Name of the server the map originates from
* @param fromMapId Original map ID
* @param toServerName Name of the new server
* @return New map ID or -1 if not found
*/
@Blocking
public abstract int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName);
/**
* Wipes <b>all</b> {@link User} entries from the database.
* <b>This should only be used when preparing tables for a data migration.</b>
@@ -283,7 +348,9 @@ public abstract class Database {
@Getter
public enum TableName {
USERS("husksync_users"),
USER_DATA("husksync_user_data");
USER_DATA("husksync_user_data"),
MAP_DATA("husksync_map_data"),
MAP_IDS("husksync_map_ids");
private final String defaultName;

View File

@@ -35,13 +35,11 @@ import org.bson.conversions.Bson;
import org.bson.types.Binary;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;
import java.util.TimeZone;
import java.util.UUID;
import java.util.*;
import java.util.logging.Level;
public class MongoDbDatabase extends Database {
@@ -50,11 +48,15 @@ public class MongoDbDatabase extends Database {
private final String usersTable;
private final String userDataTable;
private final String mapDataTable;
private final String mapIdsTable;
public MongoDbDatabase(@NotNull HuskSync plugin) {
super(plugin);
this.usersTable = plugin.getSettings().getDatabase().getTableName(TableName.USERS);
this.userDataTable = plugin.getSettings().getDatabase().getTableName(TableName.USER_DATA);
this.mapDataTable = plugin.getSettings().getDatabase().getTableName(TableName.MAP_DATA);
this.mapIdsTable = plugin.getSettings().getDatabase().getTableName(TableName.MAP_IDS);
}
@Override
@@ -74,6 +76,12 @@ public class MongoDbDatabase extends Database {
if (mongoCollectionHelper.getCollection(userDataTable) == null) {
mongoCollectionHelper.createCollection(userDataTable);
}
if (mongoCollectionHelper.getCollection(mapDataTable) == null) {
mongoCollectionHelper.createCollection(mapDataTable);
}
if (mongoCollectionHelper.getCollection(mapIdsTable) == null) {
mongoCollectionHelper.createCollection(mapIdsTable);
}
} catch (Exception e) {
throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " +
"Please check the supplied database credentials in the config file", e);
@@ -99,7 +107,7 @@ public class MongoDbDatabase extends Database {
try {
getUser(user.getUuid()).ifPresentOrElse(
existingUser -> {
if (!existingUser.getUsername().equals(user.getUsername())) {
if (!existingUser.getName().equals(user.getName())) {
// Update a user's name if it has changed in the database
try {
Document filter = new Document("uuid", existingUser.getUuid());
@@ -108,7 +116,7 @@ public class MongoDbDatabase extends Database {
throw new MongoException("User document returned null!");
}
Bson updates = Updates.set("username", user.getUsername());
Bson updates = Updates.set("username", user.getName());
mongoCollectionHelper.updateDocument(usersTable, doc, updates);
} catch (MongoException e) {
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
@@ -118,7 +126,7 @@ public class MongoDbDatabase extends Database {
() -> {
// Insert new player data into the database
try {
Document doc = new Document("uuid", user.getUuid()).append("username", user.getUsername());
Document doc = new Document("uuid", user.getUuid()).append("username", user.getName());
mongoCollectionHelper.insertDocument(usersTable, doc);
} catch (MongoException e) {
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
@@ -226,6 +234,17 @@ public class MongoDbDatabase extends Database {
}
}
@Override
public int getUnpinnedSnapshotCount(@NotNull User user) {
try {
Document filter = new Document("player_uuid", user.getUuid()).append("pinned", false);
return (int) mongoCollectionHelper.getCollection(userDataTable).countDocuments(filter);
} catch (MongoException e) {
plugin.log(Level.SEVERE, "Failed to fetch a user's current snapshot count", e);
}
return 0;
}
@Blocking
@Override
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
@@ -251,17 +270,14 @@ public class MongoDbDatabase extends Database {
@Override
protected void rotateSnapshots(@NotNull User user) {
try {
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
final int unpinnedSnapshots = getUnpinnedSnapshotCount(user);
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
if (unpinnedUserData.size() > maxSnapshots) {
if (unpinnedSnapshots > maxSnapshots) {
Document filter = new Document("player_uuid", user.getUuid()).append("pinned", false);
Document sort = new Document("timestamp", 1); // 1 = Ascending
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable)
.find(filter)
.sort(sort)
.limit(unpinnedUserData.size() - maxSnapshots);
.find(filter).sort(sort)
.limit(unpinnedSnapshots - maxSnapshots);
for (Document doc : iterable) {
mongoCollectionHelper.deleteDocument(userDataTable, doc);
@@ -345,6 +361,85 @@ public class MongoDbDatabase extends Database {
}
}
@Blocking
@Override
public void saveMapData(@NotNull String serverName, int mapId, byte @NotNull [] data) {
try {
Document doc = new Document("server_name", serverName)
.append("map_id", mapId)
.append("data", new Binary(data));
mongoCollectionHelper.insertDocument(mapDataTable, doc);
} catch (MongoException e) {
plugin.log(Level.SEVERE, "Failed to write map data to the database", e);
}
}
@Blocking
@Override
public byte @Nullable [] getMapData(@NotNull String serverName, int mapId) {
try {
Document filter = new Document("server_name", serverName).append("map_id", mapId);
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(mapDataTable).find(filter);
Document doc = iterable.first();
if (doc != null) {
final Binary bin = doc.get("data", Binary.class);
return bin.getData();
}
} catch (MongoException e) {
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
}
return null;
}
@Blocking
@Override
public @Nullable Map.Entry<String, Integer> getMapBinding(@NotNull String serverName, int mapId) {
final Document filter = new Document("to_server_name", serverName).append("to_id", mapId);
final FindIterable<Document> iterable = mongoCollectionHelper.getCollection(mapIdsTable).find(filter);
final Document doc = iterable.first();
if (doc != null) {
return new AbstractMap.SimpleImmutableEntry<>(
doc.getString("from_server_name"),
doc.getInteger("from_id")
);
}
return null;
}
@Blocking
@Override
public void setMapBinding(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName, int toMapId) {
try {
final Document doc = new Document("from_server_name", fromServerName)
.append("from_id", fromMapId)
.append("to_server_name", toServerName)
.append("to_id", toMapId);
mongoCollectionHelper.insertDocument(mapIdsTable, doc);
} catch (MongoException e) {
plugin.log(Level.SEVERE, "Failed to connect map IDs in the database", e);
}
}
@Blocking
@Override
public int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName) {
try {
final Document filter = new Document("from_server_name", fromServerName)
.append("from_id", fromMapId)
.append("to_server_name", toServerName);
final FindIterable<Document> iterable = mongoCollectionHelper.getCollection(mapIdsTable).find(filter);
final Document doc = iterable.first();
if (doc != null) {
return doc.getInteger("to_id");
}
return -1;
} catch (MongoException e) {
plugin.log(Level.SEVERE, "Failed to get new map id from the database", e);
return -1;
}
}
@Blocking
@Override
public void wipeDatabase() {

View File

@@ -27,6 +27,7 @@ import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@@ -140,7 +141,7 @@ public class MySqlDatabase extends Database {
public void ensureUser(@NotNull User user) {
getUser(user.getUuid()).ifPresentOrElse(
existingUser -> {
if (!existingUser.getUsername().equals(user.getUsername())) {
if (!existingUser.getName().equals(user.getName())) {
// Update a user's name if it has changed in the database
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
@@ -148,11 +149,12 @@ public class MySqlDatabase extends Database {
SET `username`=?
WHERE `uuid`=?"""))) {
statement.setString(1, user.getUsername());
statement.setString(1, user.getName());
statement.setString(2, existingUser.getUuid().toString());
statement.executeUpdate();
}
plugin.log(Level.INFO, "Updated " + user.getUsername() + "'s name in the database (" + existingUser.getUsername() + " -> " + user.getUsername() + ")");
plugin.log(Level.INFO, "Updated " + user.getName() + "'s name in the database ("
+ existingUser.getName() + " -> " + user.getName() + ")");
} catch (SQLException e) {
plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e);
}
@@ -166,7 +168,7 @@ public class MySqlDatabase extends Database {
VALUES (?,?);"""))) {
statement.setString(1, user.getUuid().toString());
statement.setString(2, user.getUsername());
statement.setString(2, user.getName());
statement.executeUpdate();
}
} catch (SQLException e) {
@@ -297,11 +299,30 @@ public class MySqlDatabase extends Database {
return retrievedData;
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
plugin.log(Level.SEVERE, "Failed to fetch a user's list of snapshots from the database", e);
}
return retrievedData;
}
@Override
public int getUnpinnedSnapshotCount(@NotNull User user) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT COUNT(`version_uuid`)
FROM `%user_data_table%`
WHERE `player_uuid`=? AND `pinned`=false;"""))) {
statement.setString(1, user.getUuid().toString());
final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
return resultSet.getInt(1);
}
}
} catch (SQLException e) {
plugin.log(Level.SEVERE, "Failed to fetch a user's current snapshot count", e);
}
return 0;
}
@Blocking
@Override
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
@@ -334,10 +355,9 @@ public class MySqlDatabase extends Database {
@Blocking
@Override
protected void rotateSnapshots(@NotNull User user) {
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
final int unpinnedSnapshots = getUnpinnedSnapshotCount(user);
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
if (unpinnedUserData.size() > maxSnapshots) {
if (unpinnedSnapshots > maxSnapshots) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
DELETE FROM `%user_data_table%`
@@ -345,7 +365,7 @@ public class MySqlDatabase extends Database {
AND `pinned` IS FALSE
ORDER BY `timestamp` ASC
LIMIT %entry_count%;""".replace("%entry_count%",
Integer.toString(unpinnedUserData.size() - maxSnapshots))))) {
Integer.toString(unpinnedSnapshots - maxSnapshots))))) {
statement.setString(1, user.getUuid().toString());
statement.executeUpdate();
}
@@ -433,6 +453,120 @@ public class MySqlDatabase extends Database {
}
}
@Blocking
@Override
public void saveMapData(@NotNull String serverName, int mapId, byte @NotNull [] data) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
INSERT INTO `%map_data_table%`
(`server_name`,`map_id`,`data`)
VALUES (?,?,?);"""))) {
statement.setString(1, serverName);
statement.setInt(2, mapId);
statement.setBlob(3, new ByteArrayInputStream(data));
statement.executeUpdate();
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to write map data to the database", e);
}
}
@Blocking
@Override
public byte @Nullable [] getMapData(@NotNull String serverName, int mapId) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `data`
FROM `%map_data_table%`
WHERE `server_name`=? AND `map_id`=?
LIMIT 1;"""))) {
statement.setString(1, serverName);
statement.setInt(2, mapId);
final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
final Blob blob = resultSet.getBlob("data");
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
blob.free();
return dataByteArray;
}
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
}
return null;
}
@Blocking
@Override
public @Nullable Map.Entry<String, Integer> getMapBinding(@NotNull String serverName, int mapId) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `from_server_name`, `from_id`
FROM `%map_ids_table%`
WHERE `to_server_name`=? AND `to_id`=?
LIMIT 1;
"""))) {
statement.setString(1, serverName);
statement.setInt(2, mapId);
final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
return new AbstractMap.SimpleImmutableEntry<>(
resultSet.getString("from_server_name"),
resultSet.getInt("from_id")
);
}
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
}
return null;
}
@Blocking
@Override
public void setMapBinding(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName, int toMapId) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
INSERT INTO `%map_ids_table%`
(`from_server_name`,`from_id`,`to_server_name`,`to_id`)
VALUES (?,?,?,?);"""))) {
statement.setString(1, fromServerName);
statement.setInt(2, fromMapId);
statement.setString(3, toServerName);
statement.setInt(4, toMapId);
statement.executeUpdate();
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to connect map IDs in the database", e);
}
}
@Blocking
@Override
public int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `to_id`
FROM `%map_ids_table%`
WHERE `from_server_name`=? AND `from_id`=? AND `to_server_name`=?
LIMIT 1;"""))) {
statement.setString(1, fromServerName);
statement.setInt(2, fromMapId);
statement.setString(3, toServerName);
final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
return resultSet.getInt("to_id");
}
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to get new map id from the database", e);
}
return -1;
}
@Override
public void wipeDatabase() {
try (Connection connection = getConnection()) {

View File

@@ -27,6 +27,7 @@ import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.sql.*;
@@ -133,7 +134,7 @@ public class PostgresDatabase extends Database {
public void ensureUser(@NotNull User user) {
getUser(user.getUuid()).ifPresentOrElse(
existingUser -> {
if (!existingUser.getUsername().equals(user.getUsername())) {
if (!existingUser.getName().equals(user.getName())) {
// Update a user's name if it has changed in the database
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
@@ -141,11 +142,11 @@ public class PostgresDatabase extends Database {
SET username=?
WHERE uuid=?;"""))) {
statement.setString(1, user.getUsername());
statement.setString(1, user.getName());
statement.setObject(2, existingUser.getUuid());
statement.executeUpdate();
}
plugin.log(Level.INFO, "Updated " + user.getUsername() + "'s name in the database (" + existingUser.getUsername() + " -> " + user.getUsername() + ")");
plugin.log(Level.INFO, "Updated " + user.getName() + "'s name in the database (" + existingUser.getName() + " -> " + user.getName() + ")");
} catch (SQLException e) {
plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e);
}
@@ -159,7 +160,7 @@ public class PostgresDatabase extends Database {
VALUES (?,?);"""))) {
statement.setObject(1, user.getUuid());
statement.setString(2, user.getUsername());
statement.setString(2, user.getName());
statement.executeUpdate();
}
} catch (SQLException e) {
@@ -287,11 +288,30 @@ public class PostgresDatabase extends Database {
return retrievedData;
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
plugin.log(Level.SEVERE, "Failed to fetch a user's list of snapshots from the database", e);
}
return retrievedData;
}
@Override
public int getUnpinnedSnapshotCount(@NotNull User user) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT COUNT(version_uuid)
FROM %user_data_table%
WHERE player_uuid=? AND pinned=false;"""))) {
statement.setObject(1, user.getUuid());
final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
return resultSet.getInt(1);
}
}
} catch (SQLException e) {
plugin.log(Level.SEVERE, "Failed to fetch a user's current snapshot count", e);
}
return 0;
}
@Blocking
@Override
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
@@ -322,10 +342,9 @@ public class PostgresDatabase extends Database {
@Blocking
@Override
protected void rotateSnapshots(@NotNull User user) {
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
final int unpinnedSnapshots = getUnpinnedSnapshotCount(user);
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
if (unpinnedUserData.size() > maxSnapshots) {
if (unpinnedSnapshots > maxSnapshots) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
WITH cte AS (
@@ -338,7 +357,7 @@ public class PostgresDatabase extends Database {
)
DELETE FROM %user_data_table%
WHERE version_uuid IN (SELECT version_uuid FROM cte);""".replace("%entry_count%",
Integer.toString(unpinnedUserData.size() - maxSnapshots))))) {
Integer.toString(unpinnedSnapshots - maxSnapshots))))) {
statement.setObject(1, user.getUuid());
statement.executeUpdate();
}
@@ -416,8 +435,8 @@ public class PostgresDatabase extends Database {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
UPDATE %user_data_table%
SET save_cause=?,pinned=?,data=?
WHERE player_uuid=? AND version_uuid=?
LIMIT 1;"""))) {
WHERE player_uuid=? AND version_uuid=?;
"""))) {
statement.setString(1, data.getSaveCause().name());
statement.setBoolean(2, data.isPinned());
statement.setBytes(3, data.asBytes(plugin));
@@ -430,6 +449,117 @@ public class PostgresDatabase extends Database {
}
}
@Blocking
@Override
public void saveMapData(@NotNull String serverName, int mapId, byte @NotNull [] data) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
INSERT INTO %map_data_table%
(server_name,map_id,data)
VALUES (?,?,?);"""))) {
statement.setString(1, serverName);
statement.setInt(2, mapId);
statement.setBytes(3, data);
statement.executeUpdate();
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to write map data to the database", e);
}
}
@Blocking
@Override
public byte @Nullable [] getMapData(@NotNull String serverName, int mapId) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT data
FROM %map_data_table%
WHERE server_name=? AND map_id=?
LIMIT 1;"""))) {
statement.setString(1, serverName);
statement.setInt(2, mapId);
final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
return resultSet.getBytes("data");
}
return null;
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
}
return null;
}
@Blocking
@Override
public @Nullable Map.Entry<String, Integer> getMapBinding(@NotNull String serverName, int mapId) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT from_server_name, from_id
FROM %map_ids_table%
WHERE to_server_name=? AND to_id=?
LIMIT 1;
"""))) {
statement.setString(1, serverName);
statement.setInt(2, mapId);
final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
return new AbstractMap.SimpleImmutableEntry<>(
resultSet.getString("from_server_name"),
resultSet.getInt("from_id")
);
}
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
}
return null;
}
@Blocking
@Override
public void setMapBinding(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName, int toMapId) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
INSERT INTO %map_ids_table%
(from_server_name,from_id,to_server_name,to_id)
VALUES (?,?,?,?);"""))) {
statement.setString(1, fromServerName);
statement.setInt(2, fromMapId);
statement.setString(3, toServerName);
statement.setInt(4, toMapId);
statement.executeUpdate();
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to connect map IDs in the database", e);
}
}
@Blocking
@Override
public int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT to_id
FROM %map_ids_table%
WHERE from_server_name=? AND from_id=? AND to_server_name=?
LIMIT 1;"""))) {
statement.setString(1, fromServerName);
statement.setInt(2, fromMapId);
statement.setString(3, toServerName);
final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
return resultSet.getInt("to_id");
}
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to get new map id from the database", e);
}
return -1;
}
@Override
public void wipeDatabase() {
try (Connection connection = getConnection()) {

View File

@@ -25,9 +25,7 @@ import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.OnlineUser;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.*;
import static net.william278.husksync.config.Settings.SynchronizationSettings.SaveOnDeathSettings;
@@ -49,6 +47,7 @@ public abstract class EventListener {
* @param user The {@link OnlineUser} to handle
*/
protected final void handlePlayerJoin(@NotNull OnlineUser user) {
plugin.getDisconnectingPlayers().remove(user.getUuid());
if (user.isNpc()) {
return;
}
@@ -62,12 +61,18 @@ public abstract class EventListener {
* @param user The {@link OnlineUser} to handle
*/
protected final void handlePlayerQuit(@NotNull OnlineUser user) {
if (user.isNpc() || plugin.isDisabling() || plugin.isLocked(user.getUuid())) {
// Check the user is a user, the plugin isn't disabling, then mark as disconnecting
if (user.isNpc() || plugin.isDisabling()) {
return;
}
plugin.getDisconnectingPlayers().add(user.getUuid());
// Lock, then save their data if the user is unlocked
if (!plugin.isLocked(user.getUuid())) {
plugin.lockPlayer(user.getUuid());
plugin.getDataSyncer().syncSaveUserData(user);
}
}
/**
* Handles the saving of data when the world save event is fired
@@ -79,9 +84,9 @@ public abstract class EventListener {
return;
}
usersInWorld.stream()
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
.forEach(user -> plugin.getDataSyncer().saveData(
user, user.createSnapshot(DataSnapshot.SaveCause.WORLD_SAVE)
.filter(user -> !user.isNpc() && !user.hasDisconnected() && !plugin.isLocked(user.getUuid()))
.forEach(user -> plugin.getDataSyncer().saveCurrentUserData(
user, DataSnapshot.SaveCause.WORLD_SAVE
));
}
@@ -98,8 +103,9 @@ public abstract class EventListener {
return;
}
// We don't persist this to Redis for syncing, as this snapshot is from a state they won't be in post-respawn
final DataSnapshot.Packed snapshot = user.createSnapshot(DataSnapshot.SaveCause.DEATH);
snapshot.edit(plugin, (data -> data.getInventory().ifPresent(inventory -> inventory.setContents(items))));
snapshot.edit(plugin, (data -> data.getInventory().ifPresent(inv -> inv.setContents(items))));
plugin.getDataSyncer().saveData(user, snapshot);
}
@@ -108,16 +114,12 @@ public abstract class EventListener {
* Handle the plugin disabling
*/
public void handlePluginDisable() {
// Save for all online players
// Save for all online players.
plugin.getOnlineUsers().stream()
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
.forEach(user -> {
plugin.lockPlayer(user.getUuid());
plugin.getDataSyncer().saveData(
user,
user.createSnapshot(DataSnapshot.SaveCause.SERVER_SHUTDOWN),
(saved, data) -> plugin.getRedisManager().clearUserData(saved)
);
plugin.getDataSyncer().saveCurrentUserData(user, DataSnapshot.SaveCause.SERVER_SHUTDOWN);
});
// Close outstanding connections

View File

@@ -0,0 +1,45 @@
/*
* 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.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import org.jetbrains.annotations.NotNull;
import net.william278.husksync.adapter.Adaptable;
import net.william278.mapdataapi.MapData;
import java.io.IOException;
@AllArgsConstructor
public class AdaptableMapData implements Adaptable {
@SerializedName("data")
private final byte[] data;
public AdaptableMapData(@NotNull MapData data) {
this(data.toBytes());
}
@NotNull
public MapData getData(int dataVersion) throws IOException {
return MapData.fromByteArray(dataVersion, data);
}
}

View File

@@ -27,7 +27,10 @@ public enum RedisKeyType {
LATEST_SNAPSHOT,
SERVER_SWITCH,
DATA_CHECKOUT;
DATA_CHECKOUT,
MAP_ID,
MAP_ID_REVERSED,
MAP_DATA;
public static final int TTL_1_YEAR = 60 * 60 * 24 * 7 * 52; // 1 year
public static final int TTL_10_SECONDS = 10; // 10 seconds

View File

@@ -25,6 +25,7 @@ import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import redis.clients.jedis.*;
import redis.clients.jedis.exceptions.JedisException;
import redis.clients.jedis.util.Pool;
@@ -61,41 +62,62 @@ public class RedisManager extends JedisPubSub {
/**
* Initialize Redis connection pool
*/
@Blocking
public void initialize() throws IllegalStateException {
final Settings.RedisSettings.RedisCredentials credentials = plugin.getSettings().getRedis().getCredentials();
final String user = credentials.getUser();
final String password = credentials.getPassword();
final String host = credentials.getHost();
final int port = credentials.getPort();
final int database = credentials.getDatabase();
final boolean useSSL = credentials.isUseSsl();
// Create the jedis pool
// Configure JedisPoolConfig
final JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(0);
config.setTestOnBorrow(true);
config.setTestOnReturn(true);
config.setMaxTotal(credentials.getMaxTotalConnections());
config.setMaxIdle(credentials.getMaxIdleConnections());
config.setMinIdle(credentials.getMinIdleConnections());
config.setTestOnBorrow(credentials.isTestOnBorrow());
config.setTestOnReturn(credentials.isTestOnReturn());
config.setTestWhileIdle(credentials.isTestWhileIdle());
config.setMinEvictableIdleTimeMillis(credentials.getMinEvictableIdleTimeMillis());
config.setTimeBetweenEvictionRunsMillis(credentials.getTimeBetweenEvictionRunsMillis());
final Settings.RedisSettings.RedisSentinel sentinel = plugin.getSettings().getRedis().getSentinel();
Set<String> redisSentinelNodes = new HashSet<>(sentinel.getNodes());
if (redisSentinelNodes.isEmpty()) {
this.jedisPool = password.isEmpty()
? new JedisPool(config, host, port, 0, useSSL)
: new JedisPool(config, host, port, 0, password, useSSL);
// Standalone Redis setup
DefaultJedisClientConfig.Builder clientConfigBuilder = DefaultJedisClientConfig.builder()
.ssl(useSSL)
.database(database)
.timeoutMillis(credentials.getConnectionTimeout()) // connection and socket timeout combined
.user(user.isEmpty() ? null : user)
.password(password.isEmpty() ? null : password);
this.jedisPool = new JedisPool(config, new HostAndPort(host, port), clientConfigBuilder.build());
} else {
final String sentinelPassword = sentinel.getPassword();
this.jedisPool = new JedisSentinelPool(sentinel.getMaster(), redisSentinelNodes, password.isEmpty()
? null : password, sentinelPassword.isEmpty() ? null : sentinelPassword);
this.jedisPool = new JedisSentinelPool(
sentinel.getMaster(),
redisSentinelNodes,
config,
credentials.getConnectionTimeout(),
credentials.getSocketTimeout(),
password.isEmpty() ? null : password,
sentinelPassword.isEmpty() ? null : sentinelPassword,
database);
}
// Ping the server to check the connection
try {
jedisPool.getResource().ping();
try (var jedis = jedisPool.getResource()) {
jedis.ping();
} catch (JedisException e) {
throw new IllegalStateException("Failed to establish connection with Redis. "
+ "Please check the supplied credentials in the config file", e);
throw new IllegalStateException("Failed to establish connection with Redis. " +
"Please check the supplied credentials in the config file", e);
}
// Subscribe using a thread (rather than a task)
enabled = true;
new Thread(this::subscribe, "husksync:redis_subscriber").start();
}
@@ -112,8 +134,7 @@ public class RedisManager extends JedisPubSub {
this,
Arrays.stream(RedisMessage.Type.values())
.map(type -> type.getMessageChannel(clusterId))
.toArray(String[]::new)
);
.toArray(String[]::new));
} catch (Throwable t) {
// Thread was unlocked due error
onThreadUnlock(t);
@@ -157,28 +178,47 @@ public class RedisManager extends JedisPubSub {
final RedisMessage redisMessage = RedisMessage.fromJson(plugin, message);
switch (messageType) {
case UPDATE_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
case UPDATE_USER_DATA -> redisMessage.getTargetUser(plugin).ifPresent(
user -> {
plugin.lockPlayer(user.getUuid());
try {
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload());
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin,
redisMessage.getPayload());
user.applySnapshot(data, DataSnapshot.UpdateCause.UPDATED);
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred updating user data from Redis", e);
user.completeSync(false, DataSnapshot.UpdateCause.UPDATED, plugin);
}
}
);
case REQUEST_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
});
case REQUEST_USER_DATA -> redisMessage.getTargetUser(plugin).ifPresent(
user -> RedisMessage.create(
UUID.fromString(new String(redisMessage.getPayload(), StandardCharsets.UTF_8)),
user.createSnapshot(DataSnapshot.SaveCause.INVENTORY_COMMAND).asBytes(plugin)
).dispatch(plugin, RedisMessage.Type.RETURN_USER_DATA)
);
user.createSnapshot(DataSnapshot.SaveCause.INVENTORY_COMMAND).asBytes(plugin))
.dispatch(plugin, RedisMessage.Type.RETURN_USER_DATA));
case CHECK_IN_PETITION -> {
if (!redisMessage.isTargetServer(plugin)
|| !plugin.getSettings().getSynchronization().isCheckinPetitions()) {
return;
}
final String payload = new String(redisMessage.getPayload(), StandardCharsets.UTF_8);
final User user = new User(UUID.fromString(payload.split("/")[0]), payload.split("/")[1]);
// Only release checkout if user is truly offline AND not being processed
final boolean isOnline = plugin.getOnlineUser(user.getUuid()).isPresent();
final boolean isLocked = plugin.isLocked(user.getUuid());
if (isOnline || isLocked) {
plugin.debug("[%s] Petition ignored - user still being processed (online=%s, locked=%s)"
.formatted(user.getName(), isOnline, isLocked));
return;
}
plugin.getRedisManager().setUserCheckedOut(user, false);
plugin.debug("[%s] Petition accepted - user checked in".formatted(user.getName()));
}
case RETURN_USER_DATA -> {
final CompletableFuture<Optional<DataSnapshot.Packed>> future = pendingRequests.get(
redisMessage.getTargetUuid()
);
final UUID target = redisMessage.getTargetUuid().orElse(null);
final CompletableFuture<Optional<DataSnapshot.Packed>> future = pendingRequests.get(target);
if (future != null) {
try {
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload());
@@ -187,7 +227,7 @@ public class RedisManager extends JedisPubSub {
plugin.log(Level.SEVERE, "An exception occurred returning user data from Redis", e);
future.complete(Optional.empty());
}
pendingRequests.remove(redisMessage.getTargetUuid());
pendingRequests.remove(target);
}
}
}
@@ -210,58 +250,58 @@ public class RedisManager extends JedisPubSub {
}
}
@Blocking
public void sendUserDataUpdate(@NotNull User user, @NotNull DataSnapshot.Packed data) {
plugin.runAsync(() -> {
final RedisMessage redisMessage = RedisMessage.create(user.getUuid(), data.asBytes(plugin));
redisMessage.dispatch(plugin, RedisMessage.Type.UPDATE_USER_DATA);
});
}
public CompletableFuture<Optional<DataSnapshot.Packed>> getUserData(@NotNull UUID requestId, @NotNull User user) {
@Blocking
public void petitionServerCheckin(@NotNull String server, @NotNull User user) {
final RedisMessage redisMessage = RedisMessage.create(
server, "%s/%s".formatted(user.getUuid(), user.getName()).getBytes(StandardCharsets.UTF_8));
redisMessage.dispatch(plugin, RedisMessage.Type.CHECK_IN_PETITION);
}
public CompletableFuture<Optional<DataSnapshot.Packed>> getOnlineUserData(@NotNull UUID requestId,
@NotNull User user,
@NotNull DataSnapshot.SaveCause saveCause) {
return plugin.getOnlineUser(user.getUuid())
.map(online -> CompletableFuture.completedFuture(
Optional.of(online.createSnapshot(DataSnapshot.SaveCause.API)))
)
.orElse(this.requestData(requestId, user));
Optional.of(online.createSnapshot(saveCause))))
.orElse(this.getNetworkedUserData(requestId, user));
}
private CompletableFuture<Optional<DataSnapshot.Packed>> requestData(@NotNull UUID requestId, @NotNull User user) {
// Request a user's dat x-server
private CompletableFuture<Optional<DataSnapshot.Packed>> getNetworkedUserData(@NotNull UUID requestId,
@NotNull User user) {
final CompletableFuture<Optional<DataSnapshot.Packed>> future = new CompletableFuture<>();
pendingRequests.put(requestId, future);
plugin.runAsync(() -> {
final RedisMessage redisMessage = RedisMessage.create(
user.getUuid(),
requestId.toString().getBytes(StandardCharsets.UTF_8)
);
requestId.toString().getBytes(StandardCharsets.UTF_8));
redisMessage.dispatch(plugin, RedisMessage.Type.REQUEST_USER_DATA);
});
return future
.orTimeout(
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds(),
TimeUnit.MILLISECONDS
)
TimeUnit.MILLISECONDS)
.exceptionally(throwable -> {
pendingRequests.remove(requestId);
return Optional.empty();
});
}
/**
* Set a user's data to Redis
*
* @param user the user to set data for
* @param data the user's data to set
* @param timeToLive The time to cache the data for
*/
// Set a user's data to Redis
@Blocking
public void setUserData(@NotNull User user, @NotNull DataSnapshot.Packed data, int timeToLive) {
public void setUserData(@NotNull User user, @NotNull DataSnapshot.Packed data) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.setex(
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId),
timeToLive,
data.asBytes(plugin)
);
plugin.debug(String.format("[%s] Set %s key on Redis", user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
RedisKeyType.TTL_1_YEAR,
data.asBytes(plugin));
plugin.debug(String.format("[%s] Set %s key on Redis", user.getName(), RedisKeyType.LATEST_SNAPSHOT));
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred setting user data on Redis", e);
}
@@ -271,9 +311,8 @@ public class RedisManager extends JedisPubSub {
public void clearUserData(@NotNull User user) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.del(
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId)
);
plugin.debug(String.format("[%s] Cleared %s on Redis", user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId));
plugin.debug(String.format("[%s] Cleared %s on Redis", user.getName(), RedisKeyType.LATEST_SNAPSHOT));
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred clearing user data on Redis", e);
}
@@ -286,16 +325,15 @@ public class RedisManager extends JedisPubSub {
if (checkedOut) {
jedis.set(
key.getBytes(StandardCharsets.UTF_8),
plugin.getServerName().getBytes(StandardCharsets.UTF_8)
);
plugin.getServerName().getBytes(StandardCharsets.UTF_8));
} else {
if (jedis.del(key.getBytes(StandardCharsets.UTF_8)) == 0) {
plugin.debug(String.format("[%s] %s key not set on Redis when attempting removal (%s)",
user.getUsername(), RedisKeyType.DATA_CHECKOUT, key));
user.getName(), RedisKeyType.DATA_CHECKOUT, key));
return;
}
}
plugin.debug(String.format("[%s] %s %s key %s Redis (%s)", user.getUsername(),
plugin.debug(String.format("[%s] %s %s key %s Redis (%s)", user.getName(),
checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT, checkedOut ? "to" : "from", key));
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred setting checkout to", e);
@@ -310,13 +348,13 @@ public class RedisManager extends JedisPubSub {
if (readData != null) {
final String checkoutServer = new String(readData, StandardCharsets.UTF_8);
plugin.debug(String.format("[%s] Waiting for %s %s key to be unset on Redis",
user.getUsername(), checkoutServer, RedisKeyType.DATA_CHECKOUT));
user.getName(), checkoutServer, RedisKeyType.DATA_CHECKOUT));
return Optional.of(checkoutServer);
}
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred getting a user's checkout key from Redis", e);
}
plugin.debug(String.format("[%s] %s key not set on Redis", user.getUsername(),
plugin.debug(String.format("[%s] %s key not set on Redis", user.getName(),
RedisKeyType.DATA_CHECKOUT));
return Optional.empty();
}
@@ -351,10 +389,9 @@ public class RedisManager extends JedisPubSub {
jedis.setex(
getKey(RedisKeyType.SERVER_SWITCH, user.getUuid(), clusterId),
RedisKeyType.TTL_10_SECONDS,
new byte[0]
);
new byte[0]);
plugin.debug(String.format("[%s] Set %s key to Redis",
user.getUsername(), RedisKeyType.SERVER_SWITCH));
user.getName(), RedisKeyType.SERVER_SWITCH));
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred setting a user's server switch key from Redis", e);
}
@@ -364,7 +401,8 @@ public class RedisManager extends JedisPubSub {
* Fetch a user's data from Redis and consume the key if found
*
* @param user The user to fetch data for
* @return The user's data, if it's present on the database. Otherwise, an empty optional.
* @return The user's data, if it's present on the database. Otherwise, an empty
* optional.
*/
@Blocking
public Optional<DataSnapshot.Packed> getUserData(@NotNull User user) {
@@ -373,11 +411,11 @@ public class RedisManager extends JedisPubSub {
final byte[] dataByteArray = jedis.get(key);
if (dataByteArray == null) {
plugin.debug(String.format("[%s] Waiting for %s key from Redis",
user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
user.getName(), RedisKeyType.LATEST_SNAPSHOT));
return Optional.empty();
}
plugin.debug(String.format("[%s] Read %s key from Redis",
user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
user.getName(), RedisKeyType.LATEST_SNAPSHOT));
// Consume the key (delete from redis)
jedis.del(key);
@@ -397,11 +435,11 @@ public class RedisManager extends JedisPubSub {
final byte[] readData = jedis.get(key);
if (readData == null) {
plugin.debug(String.format("[%s] Waiting for %s key from Redis",
user.getUsername(), RedisKeyType.SERVER_SWITCH));
user.getName(), RedisKeyType.SERVER_SWITCH));
return false;
}
plugin.debug(String.format("[%s] Read %s key from Redis",
user.getUsername(), RedisKeyType.SERVER_SWITCH));
user.getName(), RedisKeyType.SERVER_SWITCH));
// Consume the key (delete from redis)
jedis.del(key);
@@ -412,6 +450,121 @@ public class RedisManager extends JedisPubSub {
}
}
@Blocking
public String getStatusDump() {
try (Jedis jedis = jedisPool.getResource()) {
return jedis.info();
}
}
@Blocking
public long getLatency() {
final long startTime = System.currentTimeMillis();
try (Jedis jedis = jedisPool.getResource()) {
jedis.ping();
return System.currentTimeMillis() - startTime;
}
}
@Blocking
public String getVersion() {
final String info = getStatusDump();
for (String line : info.split("\n")) {
if (line.startsWith("redis_version:")) {
return line.split(":")[1];
}
}
return "unknown";
}
@Blocking
public void bindMapIds(@NotNull String fromServer, int fromId, @NotNull String toServer, int toId) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.setex(
getMapIdKey(fromServer, fromId, toServer, clusterId),
RedisKeyType.TTL_1_YEAR,
String.valueOf(toId).getBytes(StandardCharsets.UTF_8));
jedis.setex(
getReversedMapIdKey(toServer, toId, clusterId),
RedisKeyType.TTL_1_YEAR,
String.format("%s:%s", fromServer, fromId).getBytes(StandardCharsets.UTF_8));
plugin.debug(String.format("Bound map %s:%s -> %s:%s on Redis", fromServer, fromId, toServer, toId));
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred binding map ids on Redis", e);
}
}
@Blocking
public Optional<Integer> getBoundMapId(@NotNull String fromServer, int fromId, @NotNull String toServer) {
try (Jedis jedis = jedisPool.getResource()) {
final byte[] readData = jedis.get(getMapIdKey(fromServer, fromId, toServer, clusterId));
if (readData == null) {
plugin.debug(String.format("[%s:%s] No bound map id for server %s Redis",
fromServer, fromId, toServer));
return Optional.empty();
}
plugin.debug(String.format("[%s:%s] Read bound map id for server %s from Redis",
fromServer, fromId, toServer));
return Optional.of(Integer.parseInt(new String(readData, StandardCharsets.UTF_8)));
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred getting bound map id from Redis", e);
return Optional.empty();
}
}
@Blocking
public @Nullable Map.Entry<String, Integer> getReversedMapBound(@NotNull String toServer, int toId) {
try (Jedis jedis = jedisPool.getResource()) {
final byte[] readData = jedis.get(getReversedMapIdKey(toServer, toId, clusterId));
if (readData == null) {
plugin.debug(String.format("[%s:%s] No reversed map bound on Redis",
toServer, toId));
return null;
}
plugin.debug(String.format("[%s:%s] Read reversed map bound from Redis",
toServer, toId));
String[] parts = new String(readData, StandardCharsets.UTF_8).split(":");
return Map.entry(parts[0], Integer.parseInt(parts[1]));
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred reading reversed map bound from Redis", e);
return null;
}
}
@Blocking
public void setMapData(@NotNull String serverName, int mapId, byte[] data) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.setex(
getMapDataKey(serverName, mapId, clusterId),
RedisKeyType.TTL_1_YEAR,
data);
plugin.debug(String.format("Set map data %s:%s on Redis", serverName, mapId));
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred setting map data on Redis", e);
}
}
@Blocking
public byte @Nullable [] getMapData(@NotNull String serverName, int mapId) {
try (Jedis jedis = jedisPool.getResource()) {
final byte[] readData = jedis.get(getMapDataKey(serverName, mapId, clusterId));
if (readData == null) {
plugin.debug(String.format("[%s:%s] No map data on Redis",
serverName, mapId));
return null;
}
plugin.debug(String.format("[%s:%s] Read map data from Redis",
serverName, mapId));
return readData;
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred reading map data from Redis", e);
return null;
}
}
@Blocking
public void terminate() {
enabled = false;
@@ -432,4 +585,20 @@ public class RedisManager extends JedisPubSub {
return String.format("%s:%s", keyType.getKeyPrefix(clusterId), uuid);
}
private static byte[] getMapIdKey(@NotNull String fromServer, int fromId, @NotNull String toServer,
@NotNull String clusterId) {
return String.format("%s:%s:%s:%s", RedisKeyType.MAP_ID.getKeyPrefix(clusterId), fromServer, fromId, toServer)
.getBytes(StandardCharsets.UTF_8);
}
private static byte[] getReversedMapIdKey(@NotNull String toServer, int toId, @NotNull String clusterId) {
return String.format("%s:%s:%s", RedisKeyType.MAP_ID_REVERSED.getKeyPrefix(clusterId), toServer, toId)
.getBytes(StandardCharsets.UTF_8);
}
private static byte[] getMapDataKey(@NotNull String serverName, int mapId, @NotNull String clusterId) {
return String.format("%s:%s:%s", RedisKeyType.MAP_DATA.getKeyPrefix(clusterId), serverName, mapId)
.getBytes(StandardCharsets.UTF_8);
}
}

View File

@@ -25,25 +25,38 @@ import lombok.Getter;
import lombok.Setter;
import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.Adaptable;
import net.william278.husksync.user.OnlineUser;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
@Setter
public class RedisMessage implements Adaptable {
private @Nullable String targetServer;
@SerializedName("target_uuid")
private UUID targetUuid;
private @Nullable UUID targetUuid;
@Getter
@Setter
@SerializedName("payload")
private byte[] payload;
private RedisMessage(byte[] payload) {
setPayload(payload);
}
private RedisMessage(@NotNull UUID targetUuid, byte[] message) {
this(message);
this.setTargetUuid(targetUuid);
this.setPayload(message);
}
private RedisMessage(@NotNull String targetServer, byte[] message) {
this(message);
this.setTargetServer(targetServer);
}
@SuppressWarnings("unused")
@@ -55,6 +68,11 @@ public class RedisMessage implements Adaptable {
return new RedisMessage(targetUuid, message);
}
@NotNull
public static RedisMessage create(@NotNull String targetServer, byte[] message) {
return new RedisMessage(targetServer, message);
}
@NotNull
public static RedisMessage fromJson(@NotNull HuskSync plugin, @NotNull String json) throws JsonSyntaxException {
return plugin.getGson().fromJson(json, RedisMessage.class);
@@ -67,20 +85,23 @@ public class RedisMessage implements Adaptable {
));
}
@NotNull
public UUID getTargetUuid() {
return targetUuid;
public Optional<UUID> getTargetUuid() {
return Optional.ofNullable(targetUuid);
}
public void setTargetUuid(@NotNull UUID targetUuid) {
this.targetUuid = targetUuid;
public Optional<OnlineUser> getTargetUser(@NotNull HuskSync plugin) {
return getTargetUuid().flatMap(plugin::getOnlineUser);
}
public boolean isTargetServer(@NotNull HuskSync plugin) {
return targetServer != null && targetServer.equals(plugin.getServerName());
}
public enum Type {
UPDATE_USER_DATA,
REQUEST_USER_DATA,
RETURN_USER_DATA;
RETURN_USER_DATA,
CHECK_IN_PETITION;
@NotNull
public String getMessageChannel(@NotNull String clusterId) {

View File

@@ -94,6 +94,16 @@ public abstract class DataSyncer {
*/
public abstract void syncSaveUserData(@NotNull OnlineUser user);
/**
* Save a user's current data
*
* @param onlineUser the user to save data of
* @param cause the save cause
*/
public void saveCurrentUserData(@NotNull OnlineUser onlineUser, @NotNull DataSnapshot.SaveCause cause) {
this.saveData(onlineUser, onlineUser.createSnapshot(cause), getRedis()::setUserData);
}
/**
* Save a {@link DataSnapshot.Packed user's data snapshot} to the database,
* first firing the {@link net.william278.husksync.event.DataSaveEvent}. This will not update data on Redis.
@@ -163,7 +173,7 @@ public abstract class DataSyncer {
() -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin)
);
} catch (Throwable e) {
plugin.log(Level.WARNING, "Failed to set %s's data from the database".formatted(user.getUsername()), e);
plugin.log(Level.WARNING, "Failed to set %s's data from the database".formatted(user.getName()), e);
user.completeSync(false, DataSnapshot.UpdateCause.SYNCHRONIZED, plugin);
}
}
@@ -175,7 +185,7 @@ public abstract class DataSyncer {
final AtomicReference<Task.Repeating> task = new AtomicReference<>();
final AtomicBoolean processing = new AtomicBoolean(false);
final Runnable runnable = () -> {
if (user.isOffline()) {
if (user.cannotApplySnapshot()) {
task.get().cancel();
return;
}
@@ -188,7 +198,7 @@ public abstract class DataSyncer {
if (plugin.isDisabling() || timesRun.getAndIncrement() > maxListenAttempts) {
task.get().cancel();
plugin.debug(String.format("[%s] Redis timed out after %s attempts; setting from database",
user.getUsername(), timesRun.get()));
user.getName(), timesRun.get()));
setUserFromDatabase(user);
return;
}

View File

@@ -21,7 +21,6 @@ package net.william278.husksync.sync;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.redis.RedisKeyType;
import net.william278.husksync.user.OnlineUser;
import org.jetbrains.annotations.NotNull;
@@ -63,7 +62,10 @@ public class DelayDataSyncer extends DataSyncer {
getRedis().setUserServerSwitch(onlineUser);
saveData(
onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
(user, data) -> getRedis().setUserData(user, data, RedisKeyType.TTL_10_SECONDS)
(user, data) -> {
getRedis().setUserData(user, data);
plugin.unlockPlayer(user.getUuid());
}
);
});
}

View File

@@ -21,10 +21,11 @@ package net.william278.husksync.sync;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.redis.RedisKeyType;
import net.william278.husksync.user.OnlineUser;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public class LockstepDataSyncer extends DataSyncer {
public LockstepDataSyncer(@NotNull HuskSync plugin) {
@@ -45,9 +46,21 @@ public class LockstepDataSyncer extends DataSyncer {
@Override
public void syncApplyUserData(@NotNull OnlineUser user) {
this.listenForRedisData(user, () -> {
if (getRedis().getUserCheckedOut(user).isPresent()) {
if (user.cannotApplySnapshot()) {
plugin.debug("Not checking data state for user who has gone offline: %s".formatted(user.getName()));
return false;
}
// If they are checked out, ask the server to check them back in and return false
final Optional<String> server = getRedis().getUserCheckedOut(user);
if (server.isPresent() && !server.get().equals(plugin.getServerName())) {
if (plugin.getSettings().getSynchronization().isCheckinPetitions()) {
getRedis().petitionServerCheckin(server.get(), user);
}
return false;
}
// If they are checked in - or checked out on *this* server - we can apply their latest data
getRedis().setUserCheckedOut(user, true);
getRedis().getUserData(user).ifPresentOrElse(
data -> user.applySnapshot(data, DataSnapshot.UpdateCause.SYNCHRONIZED),
@@ -62,8 +75,9 @@ public class LockstepDataSyncer extends DataSyncer {
plugin.runAsync(() -> saveData(
onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
(user, data) -> {
getRedis().setUserData(user, data, RedisKeyType.TTL_1_YEAR);
getRedis().setUserData(user, data);
getRedis().setUserCheckedOut(user, false);
plugin.unlockPlayer(user.getUuid());
}
));
}

View File

@@ -43,11 +43,27 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
}
/**
* Indicates if the player has gone offline
* Indicates if the player is offline
*
* @return {@code true} if the player has left the server; {@code false} otherwise
* @deprecated use {@code hasDisconnected} instead
*/
public abstract boolean isOffline();
@Deprecated(since = "3.8")
public boolean isOffline() {
return hasDisconnected();
}
public abstract boolean hasDisconnected();
// Users cannot have snapshots applied if they have disconnected!
@Override
public boolean cannotApplySnapshot() {
if (hasDisconnected()) {
getPlugin().debug("[%s] Cannot apply snapshot as user is offline!".formatted(getName()));
return true;
}
return false;
}
@NotNull
@Override
@@ -117,7 +133,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
/**
* Set a player's status from a {@link DataSnapshot}
* Apply a {@link DataSnapshot} to a player, updating their data
*
* @param snapshot The {@link DataSnapshot} to set the player's status from
* @param cause The {@link DataSnapshot.UpdateCause} of the snapshot
@@ -125,14 +141,12 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
*/
public void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull DataSnapshot.UpdateCause cause) {
getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> {
if (!isOffline()) {
getPlugin().debug(String.format("Applying snapshot (%s) to %s (cause: %s)",
snapshot.getShortId(), getUsername(), cause.getDisplayName()
getPlugin().debug(String.format("Attempting to apply snapshot (%s) to %s (cause: %s)",
snapshot.getShortId(), getName(), cause.getDisplayName()
));
UserDataHolder.super.applySnapshot(
event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin())
);
}
});
}

View File

@@ -19,6 +19,9 @@
package net.william278.husksync.user;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
@@ -26,39 +29,18 @@ import java.util.UUID;
/**
* Represents a user who has their data synchronized by HuskSync
*/
@Getter
@AllArgsConstructor
@EqualsAndHashCode(exclude = {"name"})
public class User {
private final UUID uuid;
private final String name;
private final String username;
public User(@NotNull UUID uuid, @NotNull String username) {
this.username = username;
this.uuid = uuid;
}
/**
* Get the user's unique account ID
*/
@NotNull
public UUID getUuid() {
return uuid;
}
/**
* Get the user's username
*/
@NotNull
@Deprecated(since = "3.7.4")
public String getUsername() {
return username;
}
@Override
public boolean equals(Object object) {
if (object instanceof User other) {
return this.getUuid().equals(other.getUuid());
}
return super.equals(object);
return name;
}
}

View File

@@ -22,12 +22,13 @@ package net.william278.husksync.util;
import de.exlll.configlib.Configuration;
import de.exlll.configlib.YamlConfigurationProperties;
import de.exlll.configlib.YamlConfigurationStore;
import lombok.AllArgsConstructor;
import net.william278.desertwell.util.Version;
import net.william278.husksync.HuskSync;
import org.jetbrains.annotations.NotNull;
import java.io.InputStream;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.logging.Level;
import static net.william278.husksync.config.ConfigProvider.YAML_CONFIGURATION_PROPERTIES;
@@ -38,23 +39,22 @@ public interface CompatibilityChecker {
default void checkCompatibility() throws HuskSync.FailedToLoadException {
final YamlConfigurationProperties p = YAML_CONFIGURATION_PROPERTIES.build();
final Version compatible;
final CompatibilityConfig compat;
// Load compatibility file
try (InputStream input = getResource(COMPATIBILITY_FILE)) {
final CompatibilityConfig compat = new YamlConfigurationStore<>(CompatibilityConfig.class, p).read(input);
compatible = Objects.requireNonNull(compat.getCompatibleWith());
compat = new YamlConfigurationStore<>(CompatibilityConfig.class, p).read(input);
} catch (Throwable e) {
getPlugin().log(Level.WARNING, "Failed to load compatibility config, skipping check.", e);
return;
}
// Check compatibility
if (compatible.compareTo(getPlugin().getMinecraftVersion()) != 0) {
if (!compat.isCompatibleWith(getPlugin().getMinecraftVersion())) {
throw new HuskSync.FailedToLoadException("""
Incompatible Minecraft version. This version of HuskSync is designed for Minecraft %s.
Please download the correct version of HuskSync for your server's Minecraft version (%s)."""
.formatted(compatible.toString(), getPlugin().getMinecraftVersion().toString()));
.formatted(compat.minecraftVersionRange(), getPlugin().getMinecraftVersion().toString()));
}
}
@@ -64,11 +64,38 @@ public interface CompatibilityChecker {
HuskSync getPlugin();
@Configuration
record CompatibilityConfig(@NotNull String minecraftVersion) {
record CompatibilityConfig(@NotNull String minecraftVersionRange) {
@NotNull
public Version getCompatibleWith() {
return Version.fromString(minecraftVersion);
@AllArgsConstructor
enum ExpressionType {
GTE(">=", (v, s) -> v.compareTo(Version.fromString(s.substring(2))) >= 0),
LTE("<=", (v, s) -> v.compareTo(Version.fromString(s.substring(2))) <= 0),
GT(">", (v, s) -> v.compareTo(Version.fromString(s.substring(1))) > 0),
LT("<", (v, s) -> v.compareTo(Version.fromString(s.substring(1))) < 0),
NOT("!", (v, s) -> v.compareTo(Version.fromString(s.substring(1))) != 0),
E("=", (v, s) -> v.compareTo(Version.fromString(s.substring(1))) == 0);
private final String match;
private final BiFunction<Version, String, Boolean> function;
private static boolean check(@NotNull String versionRange, @NotNull Version mcVer) {
boolean passes = true;
versions:
for (String exp : versionRange.split(" ")) {
for (ExpressionType type : values()) {
if (exp.trim().startsWith(type.match)) {
passes = passes && type.function.apply(mcVer, exp.trim());
continue versions;
}
}
passes = passes && mcVer.compareTo(Version.fromString(exp.trim())) == 0;
}
return passes;
}
}
public boolean isCompatibleWith(@NotNull Version version) {
return ExpressionType.check(minecraftVersionRange, version);
}
}

View File

@@ -1,189 +0,0 @@
/*
* 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.gson.JsonObject;
import com.google.gson.JsonParser;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.StringJoiner;
import java.util.logging.Level;
/**
* Utility class for dumping {@link DataSnapshot}s to a file or as a paste on the web
*/
public class DataDumper {
private static final String LOGS_SITE_ENDPOINT = "https://api.mclo.gs/1/log";
private final HuskSync plugin;
private final DataSnapshot.Packed snapshot;
private final User user;
private DataDumper(@NotNull DataSnapshot.Packed snapshot, @NotNull User user, @NotNull HuskSync implementor) {
this.snapshot = snapshot;
this.user = user;
this.plugin = implementor;
}
/**
* Create a {@link DataDumper} of the given {@link DataSnapshot}
*
* @param dataSnapshot The {@link DataSnapshot} to dump
* @param user The {@link User} whose data is being dumped
* @param plugin The implementing {@link HuskSync} plugin
* @return A {@link DataDumper} for the given {@link DataSnapshot}
*/
public static DataDumper create(@NotNull DataSnapshot.Packed dataSnapshot,
@NotNull User user, @NotNull HuskSync plugin) {
return new DataDumper(dataSnapshot, user, plugin);
}
/**
* Dumps the data snapshot to a string
*
* @return the data snapshot as a string
*/
@Override
@NotNull
public String toString() {
return snapshot.asJson(plugin);
}
@NotNull
public String toWeb() {
try {
final URL url = URI.create(LOGS_SITE_ENDPOINT).toURL();
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
// Dispatch the request
final byte[] messageBody = getWebContentField().getBytes(StandardCharsets.UTF_8);
final int messageLength = messageBody.length;
connection.setFixedLengthStreamingMode(messageLength);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
connection.connect();
try (OutputStream messageOutputStream = connection.getOutputStream()) {
messageOutputStream.write(messageBody);
}
// Get the response
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
// Get the body as a json
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
final StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
// Parse the response as json
final JsonObject responseJson = JsonParser.parseString(response.toString()).getAsJsonObject();
if (responseJson.has("url")) {
return responseJson.get("url").getAsString();
}
return "(Failed to get URL from response)";
}
} else {
return "(Failed to upload to logs site, got: " + connection.getResponseCode() + ")";
}
} catch (Throwable e) {
plugin.log(Level.SEVERE, "Failed to upload data to logs site", e);
}
return "(Failed to upload to logs site)";
}
@NotNull
private String getWebContentField() {
return "content=" + URLEncoder.encode(toString(), StandardCharsets.UTF_8);
}
/**
* Dump the {@link DataSnapshot} to a file and return the file name
*
* @return the relative path of the file the data was dumped to
*/
@NotNull
public String toFile() throws IOException {
final Path filePath = getFilePath();
try (final FileWriter writer = new FileWriter(filePath.toFile(), StandardCharsets.UTF_8, false)) {
writer.write(toString()); // Write the data from #getString to the file using a writer
return filePath.toString();
} catch (IOException e) {
throw new IOException("Failed to write data to file", e);
}
}
/**
* Get the file path to dump the data to
*
* @return the file path
* @throws IOException if the prerequisite dumps parent folder could not be created
*/
@NotNull
private Path getFilePath() throws IOException {
return getDumpsFolder().resolve(getFileName());
}
/**
* Get the folder to dump the data to and create it if it does not exist
*
* @return the dumps folder
* @throws IOException if the folder could not be created
*/
@NotNull
private Path getDumpsFolder() throws IOException {
final Path dumps = plugin.getConfigDirectory().resolve("dumps");
if (!Files.exists(dumps)) {
Files.createDirectory(dumps);
}
return dumps;
}
/**
* Get the name of the file to dump the data snapshot to
*
* @return the file name
*/
@NotNull
private String getFileName() {
return new StringJoiner("_")
.add(user.getUsername())
.add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")))
.add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH))
.add(snapshot.getShortId())
+ ".json";
}
}

View File

@@ -49,7 +49,7 @@ public class DataSnapshotList {
.map(snapshot -> plugin.getLocales()
.getRawLocale(!snapshot.isInvalid() ? "data_list_item" : "data_list_item_invalid",
getNumberIcon(snapshotNumber.getAndIncrement()),
dataOwner.getUsername(),
dataOwner.getName(),
snapshot.getId().toString(),
snapshot.getShortId(),
snapshot.isPinned() ? "" : " ",
@@ -63,10 +63,10 @@ public class DataSnapshotList {
.orElse("" + snapshot.getId())).toList(),
plugin.getLocales().getBaseChatList(6)
.setHeaderFormat(plugin.getLocales()
.getRawLocale("data_list_title", dataOwner.getUsername(),
.getRawLocale("data_list_title", dataOwner.getName(),
"%first_item_on_page_index%", "%last_item_on_page_index%", "%total_items%")
.orElse(""))
.setCommand("/husksync:userdata list " + dataOwner.getUsername())
.setCommand("/husksync:userdata list " + dataOwner.getName())
.build());
}

View File

@@ -59,7 +59,7 @@ public class DataSnapshotOverview {
// Title message, timestamp, owner and cause.
final Locales locales = plugin.getLocales();
locales.getLocale("data_manager_title", snapshot.getShortId(), snapshot.getId().toString(),
dataOwner.getUsername(), dataOwner.getUuid().toString())
dataOwner.getName(), dataOwner.getUuid().toString())
.ifPresent(user::sendMessage);
locales.getLocale("data_manager_timestamp",
snapshot.getTimestamp().format(DateTimeFormatter
@@ -107,13 +107,13 @@ public class DataSnapshotOverview {
if (user.hasPermission("husksync.command.inventory.edit")
&& user.hasPermission("husksync.command.enderchest.edit")) {
locales.getLocale("data_manager_item_buttons", dataOwner.getUsername(), snapshot.getId().toString())
locales.getLocale("data_manager_item_buttons", dataOwner.getName(), snapshot.getId().toString())
.ifPresent(user::sendMessage);
}
locales.getLocale("data_manager_management_buttons", dataOwner.getUsername(), snapshot.getId().toString())
locales.getLocale("data_manager_management_buttons", dataOwner.getName(), snapshot.getId().toString())
.ifPresent(user::sendMessage);
if (user.hasPermission("husksync.command.userdata.dump")) {
locales.getLocale("data_manager_system_buttons", dataOwner.getUsername(), snapshot.getId().toString())
locales.getLocale("data_manager_system_buttons", dataOwner.getName(), snapshot.getId().toString())
.ifPresent(user::sendMessage);
}
}

View File

@@ -0,0 +1,82 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.util;
import net.william278.desertwell.util.Version;
import net.william278.husksync.HuskSync;
import org.jetbrains.annotations.NotNull;
public interface DataVersionSupplier {
int VERSION1_16_5 = 2586;
int VERSION1_17_1 = 2730;
int VERSION1_18_2 = 2975;
int VERSION1_19_2 = 3120;
int VERSION1_19_4 = 3337;
int VERSION1_20_1 = 3465;
int VERSION1_20_2 = 3578;
int VERSION1_20_4 = 3700;
int VERSION1_20_5 = 3837;
int VERSION1_21_1 = 3955;
int VERSION1_21_3 = 4082;
int VERSION1_21_4 = 4189;
int VERSION1_21_5 = 4323;
int VERSION1_21_6 = 4435;
int VERSION1_21_7 = 4438;
int VERSION1_21_8 = 4438;
int VERSION1_21_9 = 4554;
int VERSION1_21_10 = 4556;
int VERSION1_21_11 = 4671;
/**
* Returns the data version for a Minecraft version
*
* @param mcVersion the Minecraft version
* @return the data version int
*/
default int getDataVersion(@NotNull Version mcVersion) {
return switch (mcVersion.toStringWithoutMetadata()) {
case "1.16", "1.16.1", "1.16.2", "1.16.3", "1.16.4", "1.16.5" -> VERSION1_16_5;
case "1.17", "1.17.1" -> VERSION1_17_1;
case "1.18", "1.18.1", "1.18.2" -> VERSION1_18_2;
case "1.19", "1.19.1", "1.19.2" -> VERSION1_19_2;
case "1.19.4" -> VERSION1_19_4;
case "1.20", "1.20.1" -> VERSION1_20_1;
case "1.20.2" -> VERSION1_20_2;
case "1.20.4" -> VERSION1_20_4;
case "1.20.5", "1.20.6" -> VERSION1_20_5;
case "1.21", "1.21.1" -> VERSION1_21_1;
case "1.21.2", "1.21.3" -> VERSION1_21_3;
case "1.21.4" -> VERSION1_21_4;
case "1.21.5" -> VERSION1_21_5;
case "1.21.6" -> VERSION1_21_6;
case "1.21.7" -> VERSION1_21_7;
case "1.21.8" -> VERSION1_21_8;
case "1.21.9" -> VERSION1_21_9;
case "1.21.10" -> VERSION1_21_10;
case "1.21.11" -> VERSION1_21_11;
default -> VERSION1_21_11; // Latest supported version
};
}
@NotNull
HuskSync getPlugin();
}

View File

@@ -0,0 +1,160 @@
/*
* 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.HuskSync;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.user.OnlineUser;
import net.william278.toilet.DumpOptions;
import net.william278.toilet.Toilet;
import net.william278.toilet.dump.*;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
import static net.william278.toilet.DumpOptions.*;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public interface DumpProvider {
@NotNull String BYTEBIN_URL = "https://bytebin.lucko.me";
@NotNull String VIEWER_URL = "https://william278.net/dump";
@NotNull
Toilet getToilet();
@NotNull
@Blocking
default String createDump(@NotNull CommandUser u) {
return getToilet().dump(getPluginStatus(), u instanceof OnlineUser o
? new DumpUser(o.getName(), o.getUuid()) : null,
getRedisInfo()).toString();
}
@NotNull
default DumpOptions getDumpOptions() {
return builder()
.bytebinUrl(BYTEBIN_URL)
.viewerUrl(VIEWER_URL)
.projectMeta(ProjectMeta.builder()
.id("husksync")
.name("HuskSync")
.version(getPlugin().getPluginVersion().toString())
.md5("unknown")
.author("William278")
.sourceCode("https://github.com/WiIIiam278/HuskSync")
.website("https://william278.net/project/husksync")
.support("https://discord.gg/tVYhJfyDWG")
.build())
.fileInclusionRules(List.of(
FileInclusionRule.configFile("config.yml", "Config File"),
FileInclusionRule.configFile(getMessagesFile(), "Locales File")
))
.compatibilityRules(List.of(
getCompatibilityWarning("CombatLogX", "Combat loggers require additional" +
"configuration for use with HuskSync. Check https://william278.net/docs/husksync/event-priorities"),
getIncompatibleNotice("UltimateAutoRestart", "Restart plugins are not" +
"compatible with HuskSync as they affect the way the server shuts down, preventing data" +
"from saving correctly during a restart. Check https://william278.net/docs/husksync/troubleshooting")
))
.build();
}
@NotNull
@Blocking
private PluginStatus getPluginStatus() {
return PluginStatus.builder()
.blocks(List.of(getSystemStatus(), getRegisteredDataTypes()))
.build();
}
@NotNull
@Blocking
private PluginStatus.MapStatusBlock getSystemStatus() {
return new PluginStatus.MapStatusBlock(
Map.ofEntries(
Map.entry("Language", StatusLine.LANGUAGE.getValue(getPlugin())),
Map.entry("Database Type", StatusLine.DATABASE_TYPE.getValue(getPlugin())),
Map.entry("Database Local", StatusLine.IS_DATABASE_LOCAL.getValue(getPlugin())),
Map.entry("Locked User Handler", StatusLine.LOCKED_USER_HANDLER.getValue(getPlugin())),
Map.entry("Server Name", StatusLine.SERVER_NAME.getValue(getPlugin())),
Map.entry("Redis Version", StatusLine.REDIS_VERSION.getValue(getPlugin())),
Map.entry("Redis Latency", StatusLine.REDIS_LATENCY.getValue(getPlugin())),
Map.entry("Redis Sentinel", StatusLine.USING_REDIS_SENTINEL.getValue(getPlugin())),
Map.entry("Redis Database", StatusLine.REDIS_DATABASE.getValue(getPlugin())),
Map.entry("Redis User", StatusLine.USING_REDIS_USER.getValue(getPlugin())),
Map.entry("Redis Password", StatusLine.USING_REDIS_PASSWORD.getValue(getPlugin())),
Map.entry("Redis SSL", StatusLine.REDIS_USING_SSL.getValue(getPlugin())),
Map.entry("Redis Local", StatusLine.IS_REDIS_LOCAL.getValue(getPlugin()))
),
"Plugin Status", "fa6-solid:wrench"
);
}
@NotNull
@Blocking
private PluginStatus.MapStatusBlock getRegisteredDataTypes() {
return new PluginStatus.MapStatusBlock(
getPlugin().getRegisteredDataTypes().stream().collect(Collectors.toMap(
i -> i.getKey().asMinimalString(),
i -> i.isEnabled() ? "✅ Enabled" : "❌ Disabled",
(a, b) -> a)
),
"Registered Data Types", "carbon:data-blob"
);
}
@NotNull
@Blocking
private ExtraFile getRedisInfo() {
return new ExtraFile(
"redis-status", "Redis Status", "devicon-plain:redis",
getPlugin().getRedisManager().getStatusDump(),
"markdown"
);
}
@NotNull
@SuppressWarnings("SameParameterValue")
private CompatibilityRule getCompatibilityWarning(@NotNull String plugin, @NotNull String description) {
return CompatibilityRule.builder()
.labelToApply(new PluginInfo.Label("Warning", "#fcba03", description))
.resourceName(plugin).build();
}
@NotNull
@SuppressWarnings("SameParameterValue")
private CompatibilityRule getIncompatibleNotice(@NotNull String plugin, @NotNull String description) {
return CompatibilityRule.builder()
.labelToApply(new PluginInfo.Label("Incompatible", "#ff3300", description))
.resourceName(plugin).build();
}
@NotNull
private String getMessagesFile() {
return "messages-%s.yml".formatted(getPlugin().getSettings().getLanguage());
}
@NotNull
HuskSync getPlugin();
}

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.util;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.william278.husksync.HuskSync;
import net.william278.husksync.database.Database;
import org.apache.commons.text.WordUtils;
import org.jetbrains.annotations.NotNull;
import java.util.function.Function;
import java.util.stream.Collectors;
public enum StatusLine {
PLUGIN_VERSION(plugin -> Component.text("v" + plugin.getPluginVersion().toStringWithoutMetadata())
.appendSpace().append(plugin.getPluginVersion().getMetadata().isBlank() ? Component.empty()
: Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))),
SERVER_VERSION(plugin -> Component.text(plugin.getServerVersion())),
LANGUAGE(plugin -> Component.text(plugin.getSettings().getLanguage())),
MINECRAFT_VERSION(plugin -> Component.text(plugin.getMinecraftVersion().toString())),
JAVA_VERSION(plugin -> Component.text(System.getProperty("java.version"))),
JAVA_VENDOR(plugin -> Component.text(System.getProperty("java.vendor"))),
SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())),
SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully(
plugin.getSettings().getSynchronization().getMode().toString()
))),
DELAY_LATENCY(plugin -> Component.text(
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms"
)),
DATABASE_TYPE(plugin ->
Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() +
(plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ?
(plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : ""))
),
IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())),
REDIS_VERSION(plugin -> Component.text(plugin.getRedisManager().getVersion())),
USING_REDIS_SENTINEL(plugin -> getBoolean(
!plugin.getSettings().getRedis().getSentinel().getMaster().isBlank()
)),
REDIS_DATABASE(plugin -> Component.text(plugin.getSettings().getRedis().getCredentials().getDatabase())),
USING_REDIS_USER(plugin -> getBoolean(
!plugin.getSettings().getRedis().getCredentials().getUser().isBlank()
)),
USING_REDIS_PASSWORD(plugin -> getBoolean(
!plugin.getSettings().getRedis().getCredentials().getPassword().isBlank()
)),
REDIS_USING_SSL(plugin -> getBoolean(
plugin.getSettings().getRedis().getCredentials().isUseSsl()
)),
REDIS_LATENCY(plugin -> Component.text("%sms".formatted(plugin.getRedisManager().getLatency()))),
IS_REDIS_LOCAL(plugin -> getLocalhostBoolean(
plugin.getSettings().getRedis().getCredentials().getHost()
)),
LOCKED_USER_HANDLER(plugin -> Component.text(plugin.getLockedHandler().getClass().getSimpleName())),
DATA_TYPES(plugin -> Component.join(
JoinConfiguration.commas(true),
plugin.getRegisteredDataTypes().stream().map(i -> Component.textOfChildren(Component.text(i.toString())
.appendSpace().append(Component.text(i.isEnabled() ? '✔' : '❌')))
.color(i.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED)
.hoverEvent(HoverEvent.showText(
Component.text(i.isEnabled() ? "Enabled" : "Disabled")
.append(Component.newline())
.append(Component.text("Dependencies: %s".formatted(i.getDependencies()
.isEmpty() ? "(None)" : i.getDependencies().stream()
.map(d -> "%s (%s)".formatted(
d.getKey().value(), d.isRequired() ? "Required" : "Optional"
)).collect(Collectors.joining(", ")))
).color(NamedTextColor.GRAY))
))).toList()
));
private final Function<HuskSync, Component> supplier;
StatusLine(@NotNull Function<HuskSync, Component> supplier) {
this.supplier = supplier;
}
@NotNull
public Component get(@NotNull HuskSync plugin) {
return Component
.text("").appendSpace()
.append(Component.text(
WordUtils.capitalizeFully(name().replaceAll("_", " ")),
TextColor.color(0x848484)
))
.append(Component.text(':')).append(Component.space().color(NamedTextColor.WHITE))
.append(supplier.apply(plugin));
}
@NotNull
public String getValue(@NotNull HuskSync plugin) {
return PlainTextComponentSerializer.plainText().serialize(supplier.apply(plugin));
}
@NotNull
private static Component getBoolean(boolean value) {
return Component.text(value ? "Yes" : "No", value ? NamedTextColor.GREEN : NamedTextColor.RED);
}
@NotNull
private static Component getLocalhostBoolean(@NotNull String value) {
return getBoolean(value.equals("127.0.0.1") || value.equals("0.0.0.0")
|| value.equals("localhost") || value.equals("::1"));
}
}

View File

@@ -0,0 +1,145 @@
/*
* 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 lombok.AccessLevel;
import lombok.AllArgsConstructor;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import net.william278.toilet.web.Flusher;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.StringJoiner;
import java.util.logging.Level;
import static net.william278.husksync.util.DumpProvider.BYTEBIN_URL;
/**
* Utility class for dumping {@link DataSnapshot}s to a file or as a paste on the web
*/
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class UserDataDumper implements Flusher {
private static final String PASTE_VIEWER_URL = "https://pastes.dev";
private final DataSnapshot.Packed snapshot;
private final User user;
private final HuskSync plugin;
/**
* Create a {@link UserDataDumper} of the given {@link DataSnapshot}
*
* @param snapshot The {@link DataSnapshot} to dump
* @param user The {@link User} whose data is being dumped
* @param plugin The implementing {@link HuskSync} plugin
* @return A {@link UserDataDumper} for the given {@link DataSnapshot}
*/
@NotNull
public static UserDataDumper create(@NotNull DataSnapshot.Packed snapshot, @NotNull User user,
@NotNull HuskSync plugin) {
return new UserDataDumper(snapshot, user, plugin);
}
@NotNull
public String toWeb() {
try {
return "%s/%s".formatted(PASTE_VIEWER_URL, uploadDump(toString(), BYTEBIN_URL, "husksync"));
} catch (Throwable e) {
plugin.log(Level.SEVERE, "Failed to upload data.", e);
}
return "(Failed to upload. Try dumping to a file instead.)";
}
/**
* Dump the {@link DataSnapshot} to a file and return the file name
*
* @return the relative path of the file the data was dumped to
*/
@NotNull
public String toFile() throws IOException {
final Path filePath = getFilePath();
try (final FileWriter writer = new FileWriter(filePath.toFile(), StandardCharsets.UTF_8, false)) {
writer.write(toString()); // Write the data from #getString to the file using a writer
return filePath.toString();
} catch (IOException e) {
throw new IOException("Failed to write dump to file", e);
}
}
/**
* Get the file path to dump the data to
*
* @return the file path
* @throws IOException if the prerequisite dumps parent folder could not be created
*/
@NotNull
private Path getFilePath() throws IOException {
return getDumpsFolder().resolve(getFileName());
}
/**
* Get the folder to dump the data to and create it if it does not exist
*
* @return the dumps folder
* @throws IOException if the folder could not be created
*/
@NotNull
private Path getDumpsFolder() throws IOException {
final Path dumps = plugin.getConfigDirectory().resolve("dumps");
if (!Files.exists(dumps)) {
Files.createDirectory(dumps);
}
return dumps;
}
/**
* Get the name of the file to dump the data snapshot to
*
* @return the file name
*/
@NotNull
private String getFileName() {
return new StringJoiner("_")
.add(user.getName())
.add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")))
.add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH))
.add(snapshot.getShortId())
+ ".json";
}
/**
* Dumps the data snapshot to a string
*
* @return the data snapshot as a string
*/
@Override
@NotNull
public String toString() {
return snapshot.asJson(plugin);
}
}

View File

@@ -30,3 +30,27 @@ CREATE TABLE IF NOT EXISTS `%user_data_table%`
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci;
-- Create the map data table if it does not exist
CREATE TABLE IF NOT EXISTS `%map_data_table%`
(
`server_name` varchar(32) NOT NULL,
`map_id` int NOT NULL,
`data` longblob NOT NULL,
PRIMARY KEY (`server_name`, `map_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci;
-- Create the map ids table if it does not exist
CREATE TABLE IF NOT EXISTS `%map_ids_table%`
(
`from_server_name` varchar(32) NOT NULL,
`from_id` int NOT NULL,
`to_server_name` varchar(32) NOT NULL,
`to_id` int NOT NULL,
PRIMARY KEY (`from_server_name`, `from_id`, `to_server_name`),
FOREIGN KEY (`from_server_name`, `from_id`) REFERENCES `%map_data_table%` (`server_name`, `map_id`) ON DELETE CASCADE
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci;

View File

@@ -27,3 +27,25 @@ CREATE TABLE IF NOT EXISTS `%user_data_table%`
FOREIGN KEY (`player_uuid`) REFERENCES `%users_table%` (`uuid`) ON DELETE CASCADE
) CHARACTER SET utf8
COLLATE utf8_unicode_ci;
# Create the map data table if it does not exist
CREATE TABLE IF NOT EXISTS `%map_data_table%`
(
`server_name` varchar(32) NOT NULL,
`map_id` int NOT NULL,
`data` longblob NOT NULL,
PRIMARY KEY (`server_name`, `map_id`)
) CHARACTER SET utf8
COLLATE utf8_unicode_ci;
# Create the map ids table if it does not exist
CREATE TABLE IF NOT EXISTS `%map_ids_table%`
(
`from_server_name` varchar(32) NOT NULL,
`from_id` int NOT NULL,
`to_server_name` varchar(32) NOT NULL,
`to_id` int NOT NULL,
PRIMARY KEY (`from_server_name`, `from_id`, `to_server_name`),
FOREIGN KEY (`from_server_name`, `from_id`) REFERENCES `%map_data_table%` (`server_name`, `map_id`) ON DELETE CASCADE
) CHARACTER SET utf8
COLLATE utf8_unicode_ci;

View File

@@ -20,3 +20,23 @@ CREATE TABLE IF NOT EXISTS "%user_data_table%"
PRIMARY KEY (version_uuid, player_uuid),
FOREIGN KEY (player_uuid) REFERENCES "%users_table%" (uuid) ON DELETE CASCADE
);
-- Create the map data table if it does not exist
CREATE TABLE IF NOT EXISTS "%map_data_table%"
(
server_name varchar(32) NOT NULL,
map_id int NOT NULL,
data bytea NOT NULL,
PRIMARY KEY (server_name, map_id)
);
-- Create the map ids table if it does not exist
CREATE TABLE IF NOT EXISTS "%map_ids_table%"
(
from_server_name varchar(32) NOT NULL,
from_id int NOT NULL,
to_server_name varchar(32) NOT NULL,
to_id int NOT NULL,
PRIMARY KEY (from_server_name, from_id, to_server_name),
FOREIGN KEY (from_server_name, from_id) REFERENCES "%map_data_table%" (server_name, map_id) ON DELETE CASCADE
);

View File

@@ -23,11 +23,12 @@ locales:
data_list_title: '[Лист от](#00fb9a) [снапшоти на данните на потребителя](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)\n'
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
data_deleted: '[❌ Успешно изтрихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
data_restored: '[⏪ Успешно възстановихме](#00fb9a) [текущите потребителски данни за](#00fb9a) [%1%](#00fb9a show_text=&7UUID на Играча:\n&8%2%) [от снапшот](#00fb9a) [%3%.](#00fb9a show_text=&7Версия на UUID:\n&8%4%)'
data_pinned: '[※ Успешно закачихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
data_unpinned: '[※ Успешно откачихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
data_dumped: '[☂ Successfully dumped user data snapshot %1% for %2%. Click to view:](#00fb9a)'
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
@@ -40,6 +41,8 @@ locales:
save_cause_world_save: 'world save'
save_cause_death: 'death'
save_cause_server_shutdown: 'server shutdown'
save_cause_save_command: 'save command'
save_cause_dump_command: 'dump command'
save_cause_inventory_command: 'inventory command'
save_cause_enderchest_command: 'enderchest command'
save_cause_backup_restore: 'backup restore'
@@ -51,6 +54,9 @@ locales:
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Презаредихме конфигурацията и файловете със съобщения.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
error_invalid_syntax: '[Грешка:](#ff3300) [Неправилен синтаксис. Използвайте:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Грешка:](#ff3300) [Не можахме да открием играч с това име.](#ff7e5e)'
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'

View File

@@ -23,11 +23,12 @@ locales:
data_list_title: '[Nutzerdaten-Schnappschüsse von %1%:](#00fb9a) [(%2%-%3% von](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
data_list_item: '[%1%](gray show_text=&7Nutzerdaten-Schnappschuss für %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Angeheftet:\n&8Angeheftete Schnappschüsse werden nicht automatisch rotiert. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versions-Zeitstempel:&7\n&8Zeitpunkt der Speicherung der Daten\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Speicherungsgrund:\n&8Grund für das Speichern der Daten run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Schnappschuss-Größe:&7\n&8Geschätzte Dateigröße des Schnappschusses (in KiB) run_command=/userdata view %2% %3%)'
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
data_deleted: '[❌ Nutzerdaten-Schnappschuss erfolgreich gelöscht](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_restored: '[⏪ Erfgreich wiederhergestellt](#00fb9a) [Aktuelle Nutzerdaten des Schnappschusses von %1%](#00fb9a show_text=&7Spieler-UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versions-UUID:\n&8%4%)'
data_pinned: '[※ Nutzerdaten-Schnappschuss erfolgreich angepinnt](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)'
data_unpinned: '[※ Nutzerdaten-Schnappschuss erfolgreich losgelöst](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)'
data_dumped: '[☂ Nutzerdaten-Schnappschuss %1% für %2% erfolgreich gedumpt nach:](#00fb9a) &7%3%'
data_dumped: '[☂ Nutzerdaten-Schnappschuss %1% für %2% erfolgreich gedumpt nach:](#00fb9a)'
list_footer: '\n%1%[Seite](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
list_previous_page_button: '[◀](white show_text=&7Siehe vorherige Seite run_command=%2% %1%) '
list_next_page_button: ' [▶](white show_text=&7Siehe nächste Seite run_command=%2% %1%)'
@@ -39,18 +40,23 @@ locales:
save_cause_disconnect: 'Server verlassen'
save_cause_world_save: 'Welt gespeichert'
save_cause_death: 'Tod'
save_cause_server_shutdown: 'Server gestoppt'
save_cause_inventory_command: 'Inventar Befehl'
save_cause_enderchest_command: 'Enderchest Befehl'
save_cause_backup_restore: 'Backup wiederhergestellt'
save_cause_server_shutdown: 'server gestoppt'
save_cause_save_command: 'save command'
save_cause_dump_command: 'dump command'
save_cause_inventory_command: 'inventar Befehl'
save_cause_enderchest_command: 'enderchest Befehl'
save_cause_backup_restore: 'backup wiederhergestellt'
save_cause_api: 'API'
save_cause_mpdb_migration: 'MPDB Migration'
save_cause_legacy_migration: 'Legacy Migration'
save_cause_legacy_migration: 'legacy Migration'
save_cause_converted_from_v2: 'Import von v2'
reload_complete: '[HuskSync](#00fb9a bold) [| Die Konfigurations- und Sprachdateien wurden neu geladen.](#00fb9a)\n[⚠ Stelle sicher, dass die Konfigurationsdateien auf allen Servern aktuell sind!](#00fb9a)\n[Ein Neustart wird benötigt, damit Konfigurations-Änderungen wirkbar werden.](#00fb9a italic)'
up_to_date: '[HuskSync](#00fb9a bold) [| Du verwendest die neuste Version von HuskSync (v%1%).](#00fb9a)'
update_available: '[HuskSync](#ff7e5e bold) [| Eine neue Version von HuskSync ist verfügbar: v%1% (Aktuelle Version: v%2%).](#ff7e5e)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
error_invalid_syntax: '[Fehler:](#ff3300) [Falsche Syntax. Nutze:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Fehler:](#ff3300) [Es konnte kein Spieler mit diesem Namen gefunden werden.](#ff7e5e)'
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'

View File

@@ -23,11 +23,12 @@ locales:
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_unpinned: '[※ Successfully unpinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
data_dumped: '[☂ Successfully dumped user data snapshot %1% for %2%. Click to view:](#00fb9a)'
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
@@ -40,6 +41,8 @@ locales:
save_cause_world_save: 'world save'
save_cause_death: 'death'
save_cause_server_shutdown: 'server shutdown'
save_cause_save_command: 'save command'
save_cause_dump_command: 'dump command'
save_cause_inventory_command: 'inventory command'
save_cause_enderchest_command: 'enderchest command'
save_cause_backup_restore: 'backup restore'
@@ -51,6 +54,9 @@ locales:
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Reloaded config and message files.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
error_invalid_syntax: '[Error:](#ff3300) [Incorrect syntax. Usage:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Error:](#ff3300) [Could not find a player by that name.](#ff7e5e)'
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'

View File

@@ -23,11 +23,12 @@ locales:
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
data_deleted: '[❌ Se ha eliminado correctamente la snapshot del usuario](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_restored: '[⏪ Restaurado correctamente](#00fb9a) [%1%](#00fb9a show_text=&7UUID del jugador:\n&8%2%)[Informacion actual de la snapshot del jugador](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
data_pinned: '[※ Se ha anclado perfectamente la snapshot del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7UUID del usuario:\n&8%4%)'
data_unpinned: '[※ Se ha desanclado perfectamente la snapshot del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7UUID del usuario:\n&8%4%)'
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a)'
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
@@ -40,6 +41,8 @@ locales:
save_cause_world_save: 'world save'
save_cause_death: 'death'
save_cause_server_shutdown: 'server shutdown'
save_cause_save_command: 'save command'
save_cause_dump_command: 'dump command'
save_cause_inventory_command: 'inventory command'
save_cause_enderchest_command: 'enderchest command'
save_cause_backup_restore: 'backup restore'
@@ -51,6 +54,9 @@ locales:
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Recargada la configuración y los archivos de lenguaje.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
error_invalid_syntax: '[Error:](#ff3300) [Sintanxis incorrecta. Usa:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Error:](#ff3300) [No se ha podido encontrar un jugador con ese nombre.](#ff7e5e)'
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'

View File

@@ -23,11 +23,12 @@ locales:
data_list_title: '[Les instantanés des données utilisateur de %1%:](#00fb9a) [(%2%-%3% sur](#00fb9a)[%4%](#00fb9a bold)[)](#00fb9a)\n'
data_list_item: '[%1%](gray show_text=&7Instantané des données utilisateur pour %2%\n&8⚡ %4% run_command=/userdataview %2% %3%) [%5%](#d8ff2b show_text=&7Épinglé:\n&8Les instantanés épinglés ne serontpas automatiquement supprimés. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962show_text=&7Horodatage de la version:&7\n&8Quand les données ont été enregistrées\n&8%7% run_command=/userdataview %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Cause de la sauvegarde:\n&8Ce qui a causél''enregistrement des données run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fashow_text=&7Taille de l''instantané:&7\n&8Taille du fichier estimée de l''instantané (en KiB) run_command=/userdataview %2% %3%)'
data_list_item_invalid: '[%1%](dark_gray show_text=&7Instantané des données utilisateur pour %2%\n&8⚡%4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Épinglé:\n&8Lesinstantanés épinglés ne seront pas automatiquement supprimés. suggest_command=/userdata delete %2%%3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Instantané des donnéesinvalide\n&#ff7e5e&Cliquez pour supprimer\n\n&7⚠ %10% suggest_command=/userdata delete%2% %3%)'
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
data_deleted: '[❌ Instantané des données utilisateur supprimé avec succès](#00fb9a) [%1%](#00fb9ashow_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%.](#00fb9a show_text=&7UUID du joueur:\n&8%4%)'
data_restored: '[⏪ Données utilisateur actuelles de %1% restaurées avec succès à partir de l''instantané](#00fb9a) [%3%.](#00fb9a show_text=&7UUID de la version:\n&8%4%)'
data_pinned: '[※ Instantané des données utilisateur épinglé avec succès](#00fb9a) [%1%](#00fb9ashow_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%.](#00fb9a show_text=&7UUID du joueur:\n&8%4%)'
data_unpinned: '[※ Instantané des données utilisateur détaché avec succès](#00fb9a) [%1%](#00fb9ashow_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%.](#00fb9a show_text=&7UUID du joueur:\n&8%4%)'
data_dumped: '[☂ Dump de l''instantané des données utilisateur %1% pour %2% à:](#00fb9a)&7%3%'
data_dumped: '[☂ Dump de l''instantané des données utilisateur %1% pour %2% à:](#00fb9a)'
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
list_previous_page_button: '[◀](white show_text=&7Voir la page précédente run_command=%2%%1%) '
list_next_page_button: ' [▶](white show_text=&7Voir la page suivante run_command=%2% %1%)'
@@ -40,6 +41,8 @@ locales:
save_cause_world_save: 'sauvegarde du monde'
save_cause_death: 'mort'
save_cause_server_shutdown: 'arrêt du serveur'
save_cause_save_command: 'save command'
save_cause_dump_command: 'dump command'
save_cause_inventory_command: 'commande d''inventaire'
save_cause_enderchest_command: 'commande du coffre de l''Ender'
save_cause_backup_restore: 'restauration de sauvegarde'
@@ -51,6 +54,9 @@ locales:
update_available: '[HuskSync](#ff7e5e bold) [| Une nouvelle version de HuskSync est disponible:v%1% (version actuelle: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Config et messages rechargés.](#00fb9a)\n[⚠Assurez-vous que les fichiers de configuration sont à jour sur tous les serveurs!](#00fb9a)\n[Un redémarrage est nécessairepour que les modifications de configuration prennent effet.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| Rapport d''état du système:](#00fb9a)'
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
error_invalid_syntax: '[Erreur:](#ff3300) [Syntaxe incorrecte. Utilisation:](#ff7e5e) [%1%](#ff7e5eitalic show_text=&#ff7e5e&Cliquez pour suggérer suggest_command=%1%)'
error_invalid_player: '[Erreur:](#ff3300) [Impossible de trouver un joueur avec ce nom.](#ff7e5e)'
error_invalid_data: '[Erreur:](#ff3300) [Impossible de déballer les données de l''instantané car elles sont invalides ou corrompues.](#ff7e5e) [(Détails…)](gray show_text=&7⚠ %1%)'

View File

@@ -23,11 +23,12 @@ locales:
data_list_title: '[Cuplikan data %1%:](#00fb9a) [(%2%-%3% dari](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
data_list_item: '[%1%](gray show_text=&7Cuplikan data pengguna untuk %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Disematkan:\n&8Cuplikan yang disematkan tidak akan dirotasi otomatis. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versi stampel waktu:&7\n&8Saat data disimpan\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Disimpan karena:\n&8Apa yang menyebabkan data disimpan run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Ukuran cuplikan:&7\n&8Perkiraan ukuran file cuplikan (dalam KiB) run_command=/userdata view %2% %3%)'
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
data_deleted: '[❌ Berhasil menghapus cuplikan data pengguna](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
data_restored: '[⏪ Berhasil dipulihkan](#00fb9a) [%1%](#00fb9a show_text=&7UUID Pemain:\n&8%2%)[data pengguna saat ini dari cuplikan](#00fb9a) [%3%.](#00fb9a show_text=&7Versi UUID:\n&8%4%)'
data_pinned: '[※ Berhasil menyematkan cuplikan data pengguna](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
data_unpinned: '[※ Berhasil melepaskan cuplikan data pengguna yang disematkan](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
data_dumped: '[☂ Berhasil membuang cuplikan data pengguna %1% untuk %2% ke:](#00fb9a) &7%3%'
data_dumped: '[☂ Berhasil membuang cuplikan data pengguna %1% untuk %2% ke:](#00fb9a)'
list_footer: '\n%1%[Halaman](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
list_previous_page_button: '[◀](white show_text=&7Lihat halaman sebelumnya run_command=%2% %1%) '
list_next_page_button: ' [▶](white show_text=&7Lihat halaman selanjutnya run_command=%2% %1%)'
@@ -40,6 +41,8 @@ locales:
save_cause_world_save: 'penyimpanan dunia'
save_cause_death: 'kematian'
save_cause_server_shutdown: 'pematian server'
save_cause_save_command: 'save command'
save_cause_dump_command: 'dump command'
save_cause_inventory_command: 'perintah inventaris'
save_cause_enderchest_command: 'perintah enderchest'
save_cause_backup_restore: 'pemulihan cadangan'
@@ -51,6 +54,9 @@ locales:
update_available: '[HuskSync](#ff7e5e bold) [| Versi baru HuskSync tersedia: v%1% (menjalankan: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Memuat ulang file konfigurasi dan pesan.](#00fb9a)\n[⚠ Pastikan file konfigurasi sudah diperbarui di semua server!](#00fb9a)\n[Diperlukan pengaktifan ulang agar perubahan konfigurasi dapat diterapkan.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| Laporan status sistem:](#00fb9a)'
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
error_invalid_syntax: '[Kesalahan:](#ff3300) [Sintaks salah. Penggunaan:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Klik untuk menyarankan suggest_command=%1%)'
error_invalid_player: '[Kesalahan:](#ff3300) [Tidak dapat menemukan pemain dengan nama tersebut.](#ff7e5e)'
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'

View File

@@ -23,11 +23,12 @@ locales:
data_list_title: '[Lista delle istantanee di %1%:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
data_list_item: '[%1%](gray show_text=&7Instantanea di %2%&8⚡ id: %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Fissata:\n&8Se fissata, l''istantanea non viene mai modificata. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Data di salvataggio:&7\n&8Momento preciso in cui è stato salvato il dato\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Causa di salvataggio:\n&8Che cosa ha causato il salvataggio run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Peso dell''istantanea:&7\n&8Peso stimato del file (in KiB) run_command=/userdata view %2% %3%)'
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
data_deleted: '[❌ Istantanea eliminata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_restored: '[⏪ Ripristato con successo](#00fb9a) [Dati dall''istantanea di](#00fb9a)[%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versione di UUID:\n&8%4%)'
data_pinned: '[※ Instantanea fissata](#00fb9a) [%1%](#00fb9a show_text=&7UUID della versione:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_unpinned: '[※ L''istantanea dei dati utente è stata sbloccata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_dumped: '[☂ Hai ottenuto il dump dell''istantanea %1% di %2% nel formato:](#00fb9a) &7%3%'
data_dumped: '[☂ Hai ottenuto il dump dell''istantanea %1% di %2% nel formato:](#00fb9a)'
list_footer: '\n%1%[Pagina](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
list_previous_page_button: '[◀](white show_text=&7Visualizza pagina precedente run_command=%2% %1%) '
list_next_page_button: ' [▶](white show_text=&7Visualizza pagina successiva run_command=%2% %1%)'
@@ -40,6 +41,8 @@ locales:
save_cause_world_save: 'world save'
save_cause_death: 'death'
save_cause_server_shutdown: 'server shutdown'
save_cause_save_command: 'save command'
save_cause_dump_command: 'dump command'
save_cause_inventory_command: 'inventory command'
save_cause_enderchest_command: 'enderchest command'
save_cause_backup_restore: 'backup restore'
@@ -51,6 +54,9 @@ locales:
update_available: '[HuskSync](#ff7e5e bold) [| Disponibile una nuova versione: v%1% (running: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Configurazione e messaggi ricaricati.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
error_invalid_syntax: '[Errore:](#ff3300) [Sintassi errata. Usa:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Errore:](#ff3300) [Impossibile trovare un giocatore con questo nome.](#ff7e5e)'
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'

View File

@@ -12,7 +12,7 @@ locales:
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7バージョンタイムスタンプ:\n&8データの保存時期)'
data_manager_pinned: '[※ ピン留めされたスナップショット](#d8ff2b show_text=&7ピン留め:\n&8このユーザーデータのスナップショットは自動的にローテーションされません。)'
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存理由:\n&8データが保存された理由)'
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7サーバー:\n&8データが保存されたサーバー名)'
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7スナップショットサイズ:\n&8スナップショットの推定ファイルサイズ単位:KiB)\n'
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7体力) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7空腹度) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7経験値レベル) [🏹 %5%](dark_aqua show_text=&7ゲームモード)'
data_manager_advancements_statistics: '[⭐ 進捗: %1%](color=#ffc43b-#f5c962 show_text=&7達成した進捗:\n&8%2%) [⌛ プレイ時間: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7ゲーム内のプレイ時間\n&8⚠ ゲーム内の統計に基づく)\n'
@@ -22,12 +22,13 @@ locales:
data_manager_advancements_preview_remaining: 'さらに %1% 件…'
data_list_title: '[%1% のユーザーデータスナップショット:](#00fb9a) [(%4%件中](#00fb9a bold) [%2%-%3%件](#00fb9a)[)](#00fb9a)\n'
data_list_item: '[%1%](gray show_text=&7%2% のユーザーデータスナップショット&8⚡ %4% run_command=/husksync:userdata view %2% %3%) [%5%](#d8ff2b show_text=&7ピン留め:\n&8ピン留めされたスナップショットは自動的にローテーションしません。 run_command=/husksync:userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7バージョンタイムスタンプ:&7\n&8データの保存時期\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7保存理由:\n&8データが保存された理由 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7スナップショットサイズ:&7\n&8スナップショットの推定ファイルサイズ (単位:KiB) run_command=/userdata view %2% %3%)'
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
data_list_item_invalid: '[%1%](dark_gray show_text=&7%2% のユーザーデータスナップショット\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7ピン留め:\n&8ピン留めされたスナップショットは自動的にローテーションしません。 suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&無効なデータのスナップショット\n&#ff7e5e&クリックで消去\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
data_saved: '[%1% の現在のユーザーデータのスナップショットを正常に保存しました。](#00fb9a)'
data_deleted: '[❌](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [の消去に成功しました。](#00fb9a)'
data_restored: '[⏪](#00fb9a) [スナップショット](#00fb9a) [%3%](#00fb9a show_text=&7Version UUID:\n&8%4%) [から](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [の現在のユーザーデータの復元に成功しました。](#00fb9a)'
data_pinned: '[※](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [のピン留めに成功しました。](#00fb9a)'
data_unpinned: '[※](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [のピン外しに成功しました。](#00fb9a)'
data_dumped: '[☂ %2% のユーザーデータスナップショット %1% のダンプに成功:](#00fb9a) &7%3%'
data_dumped: '[☂ %2% のユーザーデータスナップショット %1% のダンプに成功:](#00fb9a)'
list_footer: '\n%1%[ページ](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
list_previous_page_button: '[◀](white show_text=&7前のページへ run_command=%2% %1%) '
list_next_page_button: ' [▶](white show_text=&7次のページへ run_command=%2% %1%)'
@@ -36,30 +37,35 @@ locales:
list_page_jumper_current_page: '[%1%](#00fb9a)'
list_page_jumper_separator: ' '
list_page_jumper_group_separator: '…'
save_cause_disconnect: 'disconnect'
save_cause_world_save: 'world save'
save_cause_death: 'death'
save_cause_server_shutdown: 'server shutdown'
save_cause_inventory_command: 'inventory command'
save_cause_enderchest_command: 'enderchest command'
save_cause_backup_restore: 'backup restore'
save_cause_disconnect: '切断'
save_cause_world_save: 'ワールド保存'
save_cause_death: '死亡'
save_cause_server_shutdown: 'サーバーシャットダウン'
save_cause_save_command: 'セーブコマンド'
save_cause_dump_command: 'ダンプコマンド'
save_cause_inventory_command: 'インベントリコマンド'
save_cause_enderchest_command: 'エンダーチェストコマンド'
save_cause_backup_restore: 'バックアップ復元'
save_cause_api: 'API'
save_cause_mpdb_migration: 'MPDB migration'
save_cause_legacy_migration: 'legacy migration'
save_cause_converted_from_v2: 'converted from v2'
save_cause_mpdb_migration: 'MPDB移行'
save_cause_legacy_migration: 'レガシー移行'
save_cause_converted_from_v2: 'v2からの変換'
up_to_date: '[HuskSync](#00fb9a bold) [| HuskSyncの最新バージョンを実行しています(v%1%).](#00fb9a)'
update_available: '[HuskSync](#ff7e5e bold) [| HuskSyncの新バージョンが更新されています: v%1% (実行中: v%2%).](#ff7e5e)'
update_available: '[HuskSync](#ff7e5e bold) [| HuskSyncの新バージョンが利用可能になりました: v%1% (実行中: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| 設定ファイルとメッセージファイルを再読み込みしました。](#00fb9a)\n[⚠ すべてのサーバーで設定ファイルが最新であることを確認してください!](#00fb9a)\n[設定の変更を有効にするには再起動が必要です。](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
error_invalid_syntax: '[Error:](#ff3300) [構文が正しくありません。使用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&クリックでサジェスト suggest_command=%1%)'
error_invalid_player: '[Error:](#ff3300) [そのプレイヤーは見つかりませんでした](#ff7e5e)'
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
error_no_permission: '[Error:](#ff3300) [このコマンドを実行する権限がありません](#ff7e5e)'
error_console_command_only: '[Error:](#ff3300) [そのコマンドは%1%コンソールからのみ実行できます](#ff7e5e)'
error_in_game_command_only: 'Error: そのコマンドはゲーム内でしか使えません。'
error_no_data_to_display: '[Error:](#ff3300) [表示するユーザーデータが見つかりませんでした。](#ff7e5e)'
error_invalid_version_uuid: '[Error:](#ff3300) [そのバージョンUUIDのユーザーデータが見つかりませんでした。](#ff7e5e)'
system_status_header: '[HuskSync](#00fb9a bold) [| システムステータスレポート:](#00fb9a)'
system_dump_confirm: '[HuskSync](#00fb9a bold) [| システムダンプを作成しますか? 含まれる内容:](#00fb9a)\n[• 最新のサーバーログと HuskSync の設定ファイル](gray)\n[• 現在のプラグインシステムの状態](gray)\n[• Java と Minecraft サーバー環境の情報](gray)\n[• 現在インストールされている他のプラグイン一覧](gray)\n[確認するには、次を実行してください:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7クリックでダンプを準備 run_command=/husksync dump confirm)'
system_dump_started: '[HuskSync](#00fb9a bold) [| システムステータスダンプを準備中です。お待ちください…](#00fb9a)'
system_dump_ready: '[HuskSync](#00fb9a bold) [| システムステータスダンプが準備できました!クリックで表示:](#00fb9a)'
error_invalid_syntax: '[エラー:](#ff3300) [構文が正しくありません。使用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&クリックでサジェスト suggest_command=%1%)'
error_invalid_player: '[エラー:](#ff3300) [そのプレイヤーは見つかりませんでした](#ff7e5e)'
error_invalid_data: '[エラー:](#ff3300) [スナップショットが無効または破損しているため、ユーザーデータを展開できません。](#ff7e5e) [(詳細…)](gray show_text=&7⚠ %1%)'
error_no_permission: '[エラー:](#ff3300) [このコマンドを実行する権限がありません](#ff7e5e)'
error_console_command_only: '[エラー:](#ff3300) [そのコマンドは%1%コンソールからのみ実行できます](#ff7e5e)'
error_in_game_command_only: 'エラー: そのコマンドはゲーム内でしか使えません。'
error_no_data_to_display: '[エラー:](#ff3300) [表示するユーザーデータが見つかりませんでした。](#ff7e5e)'
error_invalid_version_uuid: '[エラー:](#ff3300) [そのバージョンUUIDのユーザーデータが見つかりませんでした。](#ff7e5e)'
husksync_command_description: 'HuskSyncプラグインを管理する'
userdata_command_description: 'プレヤーのユーザーデータを表示・管理・復元する'
userdata_command_description: 'プレヤーのユーザーデータを表示・管理・復元する'
inventory_command_description: 'プレイヤーのインベントリを閲覧・編集する'
enderchest_command_description: 'プレイヤーのエンダーチェストを閲覧・編集する'

View File

@@ -23,11 +23,12 @@ locales:
data_list_title: '[%1%님의 유저 데이터 스냅샷 목록:](#00fb9a) [(%2%-%3% 중](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
data_list_item: '[%1%](gray show_text=&7%2%&7님의 유저 데이터 스냅샷&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7고정됨:\n&8고정된 스냅샷은 자동적으로 갱신되지 않습니다. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7저장 시각:&7\n&8데이터가 저장된 시각입니다.\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7저장 사유:\n&8데이터가 저장된 사유입니다. run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7스냅샷 크기:&7\n&8스냅샷 파일의 대략적인 크기입니다. (단위 KiB) run_command=/userdata view %2% %3%)'
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
data_deleted: '[❌ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%) [님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 삭제하였습니다.](#00fb9a)'
data_restored: '[⏪ 성공적으로 복구되었습니다.](#00fb9a) [%1%](#00fb9a show_text=&7플레이어 UUID:\n&8%2%)[님의 현재 유저 데이터 스냅샷이](#00fb9a) [%3%](#00fb9a show_text=&7버전 UUID:\n&8%4%)[으로 변경되었습니다.](#00fb9a)'
data_pinned: '[※ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%)[님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 고정하였습니다.](#00fb9a)'
data_unpinned: '[※ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%) [님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 고정 해제하였습니다.](#00fb9a)'
data_dumped: '[☂ 성공적으로 %2%님의 유저 데이터 스냅샷 %1%를 다음으로 덤프하였습니다:](#00fb9a) &7%3%'
data_dumped: '[☂ 성공적으로 %2%님의 유저 데이터 스냅샷 %1%를 다음으로 덤프하였습니다:](#00fb9a)'
list_footer: '\n%1%[페이지](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
list_previous_page_button: '[◀](white show_text=&7이전 페이지 보기 run_command=%2% %1%) '
list_next_page_button: ' [▶](white show_text=&7다음 페이지 보기 run_command=%2% %1%)'
@@ -40,6 +41,8 @@ locales:
save_cause_world_save: 'world save'
save_cause_death: 'death'
save_cause_server_shutdown: 'server shutdown'
save_cause_save_command: 'save command'
save_cause_dump_command: 'dump command'
save_cause_inventory_command: 'inventory command'
save_cause_enderchest_command: 'enderchest command'
save_cause_backup_restore: 'backup restore'
@@ -51,6 +54,9 @@ locales:
update_available: '[HuskSync](#ff7e5e bold) [| 새로운 버전의 HuskSync가 존재합니다: v%1% (현재 버전: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| 콘피그와 메시지 파일을 다시 불러왔습니다.](#00fb9a)\n[⚠ 모든 서버의 컨피그 파일을 변경하였는지 확인하세요!](#00fb9a)\n[몇몇 설정은 재시작 후에 적용됩니다.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
error_invalid_syntax: '[오류:](#ff3300) [잘못된 사용법. 사용법:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&클릭하여 입력할 수 있습니다. suggest_command=%1%)'
error_invalid_player: '[오류:](#ff3300) [해당 이름의 사용자를 찾을 수 없습니다.](#ff7e5e)'
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'

View File

@@ -23,11 +23,12 @@ locales:
data_list_title: '[%1%''s momentopnamen van gebruikersgegevens:](#00fb9a) [(%2%-%3% van](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
data_list_item: '[%1%](gray show_text=&7Gebruikersgegevens momentopname voor %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Vastgezet:\n&8Vastgezette momentopnamen worden niet automatisch gerouleerd. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versie tijdmarkering:&7\n&8Wanneer de data was opgeslagen\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Reden opslaan:\n&8Waarom de data is opgeslagen run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Grootte van momentopname:&7\n&8Geschatte bestandsgrootte van de momentopname (in KiB) run_command=/userdata view %2% %3%)'
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
data_deleted: '[❌ Momentopname van gebruikersgegevens is verwijderd](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
data_restored: '[⏪ Succesvol hersteld](#00fb9a) [%1%](#00fb9a show_text=&7Speler UUID:\n&8%2%)[''s huidige gebruikersgegevens uit momentopname](#00fb9a) [%3%.](#00fb9a show_text=&7Versie UUID:\n&8%4%)'
data_pinned: '[※ Momentopname van gebruikersgegevens is vastgezet](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
data_unpinned: '[※ Momentopname van gebruikersgegevens is losgemaakt](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
data_dumped: '[☂ De momentopname van gebruikersgegevens %1% voor %2% is met succes gedumpt naar:](#00fb9a) &7%3%'
data_dumped: '[☂ De momentopname van gebruikersgegevens %1% voor %2% is met succes gedumpt naar:](#00fb9a)'
list_footer: '\n%1%[Pagina](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
list_previous_page_button: '[◀](white show_text=&7Bekijk vorige pagina run_command=%2% %1%) '
list_next_page_button: ' [▶](white show_text=&7Bekijk volgende pagina run_command=%2% %1%)'
@@ -40,6 +41,8 @@ locales:
save_cause_world_save: 'world save'
save_cause_death: 'death'
save_cause_server_shutdown: 'server shutdown'
save_cause_save_command: 'save command'
save_cause_dump_command: 'dump command'
save_cause_inventory_command: 'inventory command'
save_cause_enderchest_command: 'enderchest command'
save_cause_backup_restore: 'backup restore'
@@ -51,6 +54,9 @@ locales:
update_available: '[HuskSync](#ff7e5e bold) [| Er is een nieuwe versie van HuskSync beschikbaar: v%1% (huidige versie: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Configuratie- en berichtbestanden opnieuw geladen.](#00fb9a)\n[⚠ Controleer of de configuratiebestanden up-to-date zijn op alle servers!](#00fb9a)\n[Een herstart is nodig voor de configuratiewijzigingen van kracht te laten worden.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
error_invalid_syntax: '[Error:](#ff3300) [Onjuiste syntaxis. Gebruik:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Error:](#ff3300) [Kan geen speler met die naam vinden.](#ff7e5e)'
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'

View File

@@ -23,11 +23,12 @@ locales:
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
data_deleted: '[❌ Snapshot de dados do usuário deletada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_restored: '[⏪ Restaurada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
data_pinned: '[※ Snapshot de dados do usuário marcada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_unpinned: '[※ Snapshot de dados do usuário desmarcada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
data_dumped: '[☂ Successfully dumped user data snapshot %1% for %2%. Click to view:](#00fb9a)'
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
@@ -40,6 +41,8 @@ locales:
save_cause_world_save: 'world save'
save_cause_death: 'death'
save_cause_server_shutdown: 'server shutdown'
save_cause_save_command: 'save command'
save_cause_dump_command: 'dump command'
save_cause_inventory_command: 'inventory command'
save_cause_enderchest_command: 'enderchest command'
save_cause_backup_restore: 'backup restore'
@@ -51,6 +54,9 @@ locales:
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Arquivos de configuração e mensagens recarregados.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
error_invalid_syntax: '[Error:](#ff3300) [Sintaxe incorreta. Utilize:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Error:](#ff3300) [Não foi possível encontrar um jogador com esse nome.](#ff7e5e)'
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'

View File

@@ -23,11 +23,12 @@ locales:
data_list_title: '[Снимки данных %1%:](#00fb9a) [(%2%-%3% из](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
data_list_item: '[%1%](gray show_text=&7Снимок данных %4% пользователя %2%&8⚡ run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Закреплен:\n&8Закрепленные снимки данных не удаляются автоматически run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Время:&7\n&8Когда данные были сохранены\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Причина сохранения:\n&8Что привело к сохранению данных run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Размер:&7\n&8Предполагаемый размер снимка (в килобайтах) run_command=/userdata view %2% %3%)'
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
data_deleted: '[❌ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID игрока:\n&8%4%) [удален.](#00fb9a)'
data_restored: '[⏪ Данные пользователя](#00fb9a) [%1%](#00fb9a show_text=&7UUID игрока:\n&8%2%) [из снимка](#00fb9a) [%3%](#00fb9a show_text=&7UUID снимка:\n&8%4%) [успешно восстановлены.](#00fb9a)'
data_pinned: '[※ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID игрока:\n&8%4%) [успешно закреплен.](#00fb9a)'
data_unpinned: '[※ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID пользователя:\n&8%4%) [успешно откреплен.](#00fb9a)'
data_dumped: '[☂ Дамп снимка данных %1% пользователя %2% успешно выгружен:](#00fb9a) &7%3%'
data_dumped: '[☂ Дамп снимка данных %1% пользователя %2% успешно выгружен:](#00fb9a)'
list_footer: '\n%1%[Страница](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%\n'
list_previous_page_button: '[◀](white show_text=&7Просмотр предыдущей страницы run_command=%2% %1%) '
list_next_page_button: ' [▶](white show_text=&7Просмотр следующей страницы run_command=%2% %1%)'
@@ -40,6 +41,8 @@ locales:
save_cause_world_save: 'сохранение мира'
save_cause_death: 'смерть'
save_cause_server_shutdown: 'отключение сервера'
save_cause_save_command: 'save command'
save_cause_dump_command: 'dump command'
save_cause_inventory_command: 'команда inventory'
save_cause_enderchest_command: 'команда enderchest'
save_cause_backup_restore: 'восстановление из снимка'
@@ -51,6 +54,9 @@ locales:
update_available: '[HuskSync](#ff7e5e bold) [| Доступна новая версия HuskSync: v%1% (текущая: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Конфигурация и файлы локализации перезагружены.](#00fb9a)\n[⚠ Убедитесь, что файлы конфигурации обновлены на всех серверах!](#00fb9a)\n[Необходима перезагрузка для вступления изменений конфигурации в силу.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
error_invalid_syntax: '[Ошибка:](#ff3300) [Неправильный синтаксис. Используйте:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Ошибка:](#ff3300) [Не удалось найти игрока с данным именем.](#ff7e5e)'
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'

View File

@@ -23,11 +23,12 @@ locales:
data_list_title: '[%1%''ın kullanıcı veri anlıkları:](#00fb9a) [(%2%-%3% /](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
data_list_item: '[%1%](gray show_text=&7Oyuncu Veri Anlığı %2% için %3%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Sabitlendi:\n&8Sabitlenmiş anlıklar otomatik olarak döndürülmez. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versiyon zaman damgası:&7\n&8Verinin ne zaman kaydedildiği\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Kaydetme sebebi:\n&8Verinin kaydedilme nedeni run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Anlık boyutu:&7\n&8Anlının tahmini dosya boyutu (KiB cinsinden) run_command=/userdata view %2% %3%)'
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
data_deleted: '[❌ Kullanıcı veri anlığı başarıyla silindi](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
data_restored: '[⏪ Başarıyla geri yüklendi](#00fb9a) [%1%](#00fb9a show_text=&7Oyuncu UUID:\n&8%2%)[''ın mevcut kullanıcı verisi anlığından](#00fb9a) [%3%.](#00fb9a show_text=&7Versiyon UUID:\n&8%4%)'
data_pinned: '[※ Kullanıcı veri anlığı başarıyla sabitlendi](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
data_unpinned: '[※ Kullanıcı veri anlığı başarıyla çözüldü](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
data_dumped: '[☂ Kullanıcı veri anlığı başarıyla döküldü %1% için %2%:](#00fb9a) &7%3%'
data_dumped: '[☂ Kullanıcı veri anlığı başarıyla döküldü %1% için %2%:](#00fb9a)'
list_footer: '\n%1%[Sayfa](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
list_previous_page_button: '[◀](white show_text=&7Önceki sayfayı görüntüle run_command=%2% %1%) '
list_next_page_button: ' [▶](white show_text=&7Sonraki sayfayı görüntüle run_command=%2% %1%)'
@@ -40,6 +41,8 @@ locales:
save_cause_world_save: 'dünya kaydı'
save_cause_death: 'ölüm'
save_cause_server_shutdown: 'sunucu kapatma'
save_cause_save_command: 'save command'
save_cause_dump_command: 'dump command'
save_cause_inventory_command: 'envanter komutu'
save_cause_enderchest_command: 'ender sandığı komutu'
save_cause_backup_restore: 'yedek geri yükleme'
@@ -51,6 +54,9 @@ locales:
update_available: '[HuskSync](#ff7e5e bold) [| HuskSync\''in yeni bir sürümü mevcut: v%1% (kullanılan sürüm: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Yapılandırma ve mesaj dosyaları yeniden yüklendi.](#00fb9a)\n[⚠ Lütfen yapılandırma dosyalarının tüm sunucularda güncel olduğundan emin olun!](#00fb9a)\n[Yapılandırma değişikliklerinin etkili olabilmesi için bir yeniden başlatma gereklidir.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| Sistem durumu raporu:](#00fb9a)'
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
error_invalid_syntax: '[Hata:](#ff3300) [Yanlış sözdizimi. Kullanım:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Öneri için tıklayın Suggest_command=%1%)'
error_invalid_player: '[Hata:](#ff3300) [Bu isimde bir oyuncu bulunamadı.](#ff7e5e)'
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'

View File

@@ -23,11 +23,12 @@ locales:
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
data_saved: '[Successfully saved a snapshot of %1%''s current user data.](#00fb9a)'
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_unpinned: '[※ Successfully unpinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
data_dumped: '[☂ Successfully dumped user data snapshot %1% for %2%. Click to view:](#00fb9a)'
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
@@ -40,6 +41,8 @@ locales:
save_cause_world_save: 'world save'
save_cause_death: 'death'
save_cause_server_shutdown: 'server shutdown'
save_cause_save_command: 'save command'
save_cause_dump_command: 'dump command'
save_cause_inventory_command: 'inventory command'
save_cause_enderchest_command: 'enderchest command'
save_cause_backup_restore: 'backup restore'
@@ -51,6 +54,9 @@ locales:
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Перезавантажено конфіґ та файли повідомлень.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
system_dump_confirm: '[HuskSync](#00fb9a bold) [| Prepare a system dump? This will include:](#00fb9a)\n[• Your latest server logs and HuskSync config files](gray)\n[• Current plugin system status information](gray)\n[• Information about your Java & Minecraft server environment](gray)\n[• A list of other currently installed plugins](gray)\n[To confirm, use:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7Click to prepare dump run_command=/husksync dump confirm)'
system_dump_started: '[HuskSync](#00fb9a bold) [| Preparing system status dump, please wait…](#00fb9a)'
system_dump_ready: '[HuskSync](#00fb9a bold) [| System status dump prepared! Click to view:](#00fb9a)'
error_invalid_syntax: '[Помилка:](#ff3300) [Неправильний синтакс. Використання:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Помилка:](#ff3300) [Гравця не знайдено](#ff7e5e)'
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'

View File

@@ -23,11 +23,12 @@ locales:
data_list_title: '[%1%的玩家数据备份:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
data_list_item: '[%1%](gray show_text=&7玩家数据备份 %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7已置顶:\n&8已置顶的备份不会按照备份时间自动排序 run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7备份时间:&7\n&8数据保存时间\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7保存原因:\n&8导致数据保存的原因 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7备份大小:&7\n&8预计备份文件大小(以KiB为单位) run_command=/userdata view %2% %3%)'
data_list_item_invalid: '[%1%](dark_gray show_text=&7%2%的用户数据快照\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7置顶:\n&8已置顶的快照不会自动排序. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&无效的快照数据\n&#ff7e5e&点击删除\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
data_saved: '[已成功保存 %1% 的当前用户数据快照.](#00fb9a)'
data_deleted: '[❌ 成功删除玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&7%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本UUID:\n&7%2%)'
data_restored: '[⏪ 成功恢复玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&7%2%)[的数据备份](#00fb9a) [%3%.](#00fb9a show_text=&7备份版本UUID:\n&7%4%)'
data_pinned: '[※ 成功置顶玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本UUID:\n&8%2%)'
data_unpinned: '[※ 成功取消置顶玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本UUID:\n&8%2%)'
data_dumped: '[☂ 已成功将 %1% 的玩家数据快照 %2% 转储到:](#00fb9a) &7%3%'
data_dumped: '[☂ 已成功将 %1% 的玩家数据快照 %2% 转储到:](#00fb9a)'
list_footer: '\n%1%[页数](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
list_previous_page_button: '[◀](white show_text=&7查看上一页 run_command=%2% %1%) '
list_next_page_button: ' [▶](white show_text=&7查看下一页 run_command=%2% %1%)'
@@ -40,6 +41,8 @@ locales:
save_cause_world_save: '保存世界'
save_cause_death: '死亡'
save_cause_server_shutdown: '服务器关闭'
save_cause_save_command: '保存命令'
save_cause_dump_command: '转储命令'
save_cause_inventory_command: '背包命令'
save_cause_enderchest_command: '末影箱命令'
save_cause_backup_restore: '备份还原'
@@ -51,6 +54,9 @@ locales:
update_available: '[HuskSync](#ff7e5e bold) [| 检测到HuskSync有新版本可以更新了:v%1%(当前版本:v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| 重新加载配置和消息文件完成.](#00fb9a)\n[⚠ 确保在所有服务器上更新配置文件!](#00fb9a)\n[需要重新启动才能使配置更改生效.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| 系统状态报告:](#00fb9a)'
system_dump_confirm: '[HuskSync](#00fb9a bold) [| 准备系统转储? 这将包括:](#00fb9a)\n[• 您最新的服务器日志和 HuskSync 配置文件](gray)\n[• 当前插件系统状态信息](gray)\n[• 有关您的 Java 和 Minecraft 服务器环境的信息](gray)\n[• 其他当前安装的插件列表](gray)\n[要确认, 请执行命令:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7点击以准备转储 run_command=/husksync dump confirm)'
system_dump_started: '[HuskSync](#00fb9a bold) [| 正在准备系统状态转储,请稍候...](#00fb9a)'
system_dump_ready: '[HuskSync](#00fb9a bold) [| 系统状态转储已完成! 点击查看:](#00fb9a)'
error_invalid_syntax: '[错误:](#ff3300) [语法错误.用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&点击建议 suggest_command=%1%)'
error_invalid_player: '[错误:](#ff3300) [找不到这个名称的玩家.](#ff7e5e)'
error_invalid_data: '[错误:](#ff3300) [无法解压缩快照数据, 因为它无效或已损坏.](#ff7e5e) [(详情…)](gray show_text=&7⚠ %1%)'

View File

@@ -23,11 +23,12 @@ locales:
data_list_title: '[%1% 的玩家資料快照:](#00fb9a) [(%2%-%3% 共](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
data_list_item: '[%1%](gray show_text=&7玩家資料快照 %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7已標記:\n&8標記的快照將不會自動輪換。 run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7版本時間戳:\n&8資料儲存時間\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7儲存原因:\n&8觸發儲存的原因 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7快照大小:\n&8快照的預估檔案大小KiB run_command=/userdata view %2% %3%)'
data_list_item_invalid: '[%1%](dark_gray show_text=&7玩家資料快照 %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7已標記:\n&8標記的快照將不會自動輪換。 suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&無效的資料快照\n&#ff7e5e&點擊刪除\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
data_saved: '[✅ 成功儲存 %1% 的目前使用者資料快照。](#00fb9a)'
data_deleted: '[❌ 成功刪除:](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%)'
data_restored: '[⏪ 成功將玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&8%2%)[的資料恢復為 快照:](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
data_pinned: '[※ 成功標記](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%)'
data_unpinned: '[※ 成功解除](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [的標記](#00fb9a)'
data_dumped: '[☂ 成功將 %2% 資料快照 %1% 儲存至:](#00fb9a) &7%3%'
data_dumped: '[☂ 成功將 %2% 資料快照 %1% 儲存至:](#00fb9a)'
list_footer: '\n%1%[頁面](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
list_previous_page_button: '[◀](white show_text=&7查看上一頁 run_command=%2% %1%) '
list_next_page_button: ' [▶](white show_text=&7查看下一頁 run_command=%2% %1%)'
@@ -40,6 +41,8 @@ locales:
save_cause_world_save: '世界儲存'
save_cause_death: '死亡'
save_cause_server_shutdown: '伺服器關閉'
save_cause_save_command: '儲存指令'
save_cause_dump_command: '導出指令'
save_cause_inventory_command: '背包指令'
save_cause_enderchest_command: '終界箱指令'
save_cause_backup_restore: '備份還原'
@@ -51,6 +54,9 @@ locales:
update_available: '[HuskSync](#ff7e5e bold) [| 發現可用的新版本: v%1% (running: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| 配置和語言文件已重新加載。](#00fb9a)\n[⚠ 確保所有伺服器上的配置文件都是最新的!](#00fb9a)\n[重啟後配置變更才會生效。](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| 系統狀態報告:](#00fb9a)'
system_dump_confirm: '[HuskSync](#00fb9a bold) [| 要產生系統狀態紀錄檔嗎?這將包含以下內容:](#00fb9a)\n[• 最近的伺服器日誌與 HuskSync 設定檔](gray)\n[• 插件目前的系統狀態資訊](gray)\n[• 有關您的 Java 與 Minecraft 伺服器環境的資訊](gray)\n[• 目前已安裝的其他插件清單](gray)\n[若要確認,請輸入:](#00fb9a) [/husksync dump confirm](#00fb9a italic show_text=&7點擊以產生紀錄檔 run_command=/husksync dump confirm)'
system_dump_started: '[HuskSync](#00fb9a bold) [| 正在產生系統狀態紀錄檔,請稍候…](#00fb9a)'
system_dump_ready: '[HuskSync](#00fb9a bold) [| 系統狀態紀錄檔已完成!點擊以下連結以查看:](#00fb9a)'
error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&點擊建議 suggest_command=%1%)'
error_invalid_player: '[錯誤:](#ff3300) [找不到這位玩家](#ff7e5e)'
error_invalid_data: '[錯誤:](#ff3300) [無法解壓使用者資料,因為快照無效或已損壞。](#ff7e5e) [(詳細資訊…)](gray show_text=&7⚠ %1%)'

View File

@@ -0,0 +1,59 @@
/*
* 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.desertwell.util.Version;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
@DisplayName("Compatibility Checker Tests")
public class CompatibilityCheckerTests {
@ParameterizedTest(name = "Ver: {0}, Range: {1}")
@DisplayName("Test Compatibility Checker")
@CsvSource({
"1.20.1, 1.21.1, false",
"1.21.1, 1.20.1, false",
"1.7.2, 1.21.5, false",
"1.19.4, 1.21.1, false",
"1.21.3, 1.21.3, true",
"1.20.1, 1.20.1, true",
"1.21.7, 1.21.7, true",
"1.21.8, >=1.21.7, true",
"1.21.8, >1.21.7, true",
"1.0, <1.21.7, true",
"1.17.1, !1.17.1, false",
"1.21.7, '>=1.21.7 <=1.21.8', true",
"1.21.8, '>=1.21.7 <=1.21.8', true",
"1.21.5, '>=1.21.7 <=1.21.8', false",
})
public void testCompatibilityChecker(@NotNull String mcVer, @NotNull String range, boolean exp) {
final Version version = Version.fromString(mcVer);
Assertions.assertNotNull(version, "Version should not be null");
final CompatibilityChecker.CompatibilityConfig config = new CompatibilityChecker.CompatibilityConfig(range);
Assertions.assertEquals(exp, config.isCompatibleWith(version), "Checker should return " + exp);
}
}

View File

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

View File

@@ -20,7 +20,6 @@ The HuskSync API is available for the following platforms:
* `fabric` - Fabric API for Minecraft. Provides Fabric API event listeners and adapters to `net.minecraft` objects.
* `common` - Common API for all platforms.
<details>
<summary>Targeting older versions</summary>
@@ -53,7 +52,7 @@ Add the repository to your `pom.xml` as per below. You can alternatively specify
</repository>
</repositories>
```
Add the dependency to your `pom.xml` as per below. Replace `HUSKSYNC_VERSION` with the latest version of HuskSync (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/HuskSync?color=%23282828&label=%20&style=flat-square). and `MINECRAFT_VERSION` with the version of Minecraft you want to target (e.g. `1.20.1`). A correctly formed version target should look like: `3.7+1.20.1`. Omit the plus symbol and Minecraft version if you are targeting the `common` platform.
Add the dependency to your `pom.xml` as per below. Replace `HUSKSYNC_VERSION` with the latest version of HuskSync (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/HuskSync?color=%23282828&label=%20&style=flat-square). and `MINECRAFT_VERSION` with the version of Minecraft you want to target (e.g. `1.21.10`). A correctly formed version target should look like: `3.7+1.21.10`. Omit the plus symbol and Minecraft version if you are targeting the `common` platform.
```xml
<dependency>
<groupId>net.william278.husksync</groupId>
@@ -76,7 +75,7 @@ allprojects {
}
}
```
Add the dependency as per below. Replace `HUSKSYNC_VERSION` with the latest version of HuskSync (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/HuskSync?color=%23282828&label=%20&style=flat-square). and `MINECRAFT_VERSION` with the version of Minecraft you want to target (e.g. `1.20.1`). A correctly formed version target should look like: `3.7+1.20.1`. Omit the plus symbol and Minecraft version if you are targeting the `common` platform.
Add the dependency as per below. Replace `HUSKSYNC_VERSION` with the latest version of HuskSync (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/HuskSync?color=%23282828&label=%20&style=flat-square). and `MINECRAFT_VERSION` with the version of Minecraft you want to target (e.g. `1.21.10`). A correctly formed version target should look like: `3.7+1.21.10`. Omit the plus symbol and Minecraft version if you are targeting the `common` platform.
```groovy
dependencies {

View File

@@ -11,7 +11,7 @@ This page contains a table of HuskSync commands and their required permission no
<tbody>
<!-- /husksync command -->
<tr>
<td rowspan="6"><code>/husksync</code></td>
<td rowspan="7"><code>/husksync</code></td>
<td><code>/husksync</code></td>
<td>View & manage plugin system information</td>
<td><code>husksync.command.husksync</code></td>
@@ -26,6 +26,11 @@ This page contains a table of HuskSync commands and their required permission no
<td>View plugin system status information</td>
<td><code>husksync.command.husksync.status</code></td>
</tr>
<tr>
<td><code>/husksync dump</code></td>
<td>Perform a web dump of the plugin system & server status.</td>
<td><code>husksync.command.husksync.dump</code></td>
</tr>
<tr>
<td><code>/husksync reload</code></td>
<td>Reload the plugin configuration</td>
@@ -43,7 +48,7 @@ This page contains a table of HuskSync commands and their required permission no
</tr>
<!-- /userdata command -->
<tr>
<td rowspan="7"><code>/userdata</code></td>
<td rowspan="8"><code>/userdata</code></td>
<td><code>/userdata</code></td>
<td>View & manage user data snapshots</td>
<td><code>husksync.command.userdata</code></td>
@@ -63,6 +68,11 @@ This page contains a table of HuskSync commands and their required permission no
<td>Restore a data snapshot for a user</td>
<td><code>husksync.command.userdata.restore</code></td>
</tr>
<tr>
<td><code>/userdata save</code></td>
<td>Create and save a snapshot of a user's current data</td>
<td><code>husksync.command.userdata.save</code></td>
</tr>
<tr>
<td><code>/userdata delete</code></td>
<td>Delete user data snapshots</td>

View File

@@ -1,14 +1,19 @@
HuskSync supports the following versions of Minecraft. Since v3.7, you must download the correct version of HuskSync for your server:
| Minecraft | Latest HuskSync | Java Version | Platforms | Support Ends |
| Minecraft | Latest HuskSync | Java Version | Platforms | Support Status |
|:---------------:|:---------------:|:------------:|:--------------|:------------------------------|
| 1.21.3 | _latest_ | 21 | Paper, Fabric | ✅ **Active Release** |
| 1.21.1 | _latest_ | 21 | Paper, Fabric | ✅ **December 2024** (Non-LTS) |
| 1.20.6 | 3.6.8 | 17 | Paper | _October 2024_ |
| 1.20.4 | 3.6.8 | 17 | Paper | _July 2024_ |
| 1.20.1 | _latest_ | 17 | Paper, Fabric | ✅ **November 2025** (LTS) |
| 1.17.1 - 1.19.4 | 3.6.8 | 17 | Paper | ❌ _Support ended_ |
| 1.16.5 | 3.2.1 | 16 | Paper | ❌ _Support ended_ |
| 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:
@@ -26,3 +31,9 @@ This plugin does not support the following software-Minecraft version combinatio
| below 1.16.5 | _All_ | Upgrade to Minecraft 1.16.5 |
&dagger;Further downstream forks of this server software are also affected.
## Incompatible plugins / mods
Please note the following plugins / mods can cause issues with HuskSync:
* Restart plugins / mods are not supported. These will cause [player data to not save correctly when your server restarts](troubleshooting#issues-with-player-data-going-out-of-sync-during-a-server-restart) due to the way these plugins utilise bash scripts. It's important to understand that restart plugins don't actually restart your server, they just trigger some (often unstable) process-killing scripting logic to occur!
* Combat logging plugins / mods are not supported. Some have built-in support for HuskSync and should work as expected, but for others you may wish to modify the [[Event Priorities]]

View File

@@ -65,10 +65,15 @@ database:
user_data: husksync_user_data
# Redis settings
redis:
# Specify the credentials of your Redis database here. Set "password" to '' if you don't have one
# Specify the credentials of your Redis server here.
# Set "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.
credentials:
host: localhost
port: 6379
# Only change the database if you know what you are doing. The default is 0.
database: 0
user: ''
password: ''
use_ssl: false
# Options for if you're using Redis sentinel. Don't modify this unless you know what you're doing!

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