9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-24 09:09:18 +00:00

Compare commits

...

118 Commits
3.5 ... 3.6.8

Author SHA1 Message Date
William
52ec138273 fix: suppress map cursor paper exception 2024-08-11 12:52:23 +01:00
William
0f7a866652 test: bump test deps to 1.21.1 2024-08-11 12:25:43 +01:00
William
eeb52ac41e deps: bump item-nbt-api to 2.13.2
support MC 1.21.1
2024-08-11 12:25:03 +01:00
William
4c7ec9ec21 docs: update config file 2024-08-09 16:53:26 +01:00
William
2f9064c4c6 refactor: revert "disable attributes/potion effects by default" 2024-08-09 16:48:40 +01:00
William
5c234cdb1d feat: improve server version status text 2024-08-09 15:03:23 +01:00
William
7d8a74381b build: bump runtime dependencies 2024-08-09 14:49:53 +01:00
William
04a7793585 refactor: auto-reformat code 2024-08-09 14:43:54 +01:00
William
ea068529f6 fix: stop syncing ambient effects, close #289
Effects from beacons, conduits, and The Warden will no longer sync.
2024-08-09 14:39:56 +01:00
William
fead3df0d8 fix: add boot warning to fabric 2024-08-09 14:25:17 +01:00
William
0c5a42a344 fix: Cancel outbound PacketEvents packets, close #344 2024-08-09 14:22:53 +01:00
William
75a2378ea8 feat: deprecate Toast notifications 2024-08-09 14:19:34 +01:00
William
662fc96ad5 refactor: disable potion effects & attributes by default 2024-08-09 14:11:19 +01:00
William
07da1c04ce fix: don't apply <1.21 attribute modifiers on >1.21 servers 2024-08-02 18:07:28 +01:00
William
845abf370a fix: more tweaks to fix attribute issues 2024-07-28 18:07:34 +01:00
William
83b5209a75 fix: "attribute modifier already applied" error, close #348 2024-07-26 16:44:04 +01:00
William
8e9850dd19 refactor: make potion effects an optional dep of attributes 2024-07-26 16:35:43 +01:00
William
1d24209b68 feat: add attribute config, don't sync potion modifiers, close #349 2024-07-26 14:26:52 +01:00
William
da70a54d78 ci: add bones publishing to CI 2024-07-24 23:49:20 +01:00
Preva1l
32ac57e2a4 fix: cme on potion effect syncing (#354)
* Started impl for mongo

* fix silly mistake with postgresql

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

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

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

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

Also adds a bit of error checking to health scale syncing
2024-06-21 13:17:53 +01:00
William
3d10b2324f feat: update DataFixer 2024-06-21 11:56:49 +01:00
William
31419f3b97 deps: bump Item NBT API to 2.13.1 2024-06-21 11:56:01 +01:00
William
8105ac27fc deps: bump Uniform to 1.1.8
Fixes startup NPE fetching usage text
2024-06-19 12:49:55 +01:00
William
44f251a948 deps: bump Uniform to 1.1.7
Adds usage text to bukkit & legacy Paper commands
2024-06-19 12:45:50 +01:00
William
463e707d27 deps: bump Uniform to 1.1.6 2024-06-19 12:23:40 +01:00
William
2d85910744 deps: bump Uniform to 1.1.5 2024-06-18 23:47:33 +01:00
William
268b279fdf feat: add the ability to disable HuskSync commands 2024-06-18 13:26:21 +01:00
William
a8ca3314d8 refactor: minor userdata dump refactor 2024-06-18 13:20:24 +01:00
William
2bdd3dae37 fix: enable game mode syncing by default
not sure why this is off by default
2024-06-18 13:11:29 +01:00
William
e29564c4ad deps: bump Uniform to 1.1.4
Fixes namespaced-backed commands being missing
2024-06-18 13:02:58 +01:00
William
6b8bb23af9 fix: cleanup leftover todo 2024-06-18 12:34:56 +01:00
William
91bbe05851 fix: fix various Fabric issues
Adjusted a mixin
Fixed Uniform being relocated causing a ClassNotFound exception (it's a JiJ mod now)
2024-06-18 12:31:58 +01:00
William
8ed6869aad docs: update maven README badge 2024-06-18 01:03:08 +01:00
dependabot[bot]
7efdf0d329 build(deps): bump urllib3 from 2.0.7 to 2.2.2 in /test (#324)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.7 to 2.2.2.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.0.7...2.2.2)

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

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

* refactor: remove commodore

* fix: update Uniform, fix commands

* fix: bump uniform, fix commands on fabric

* feat: use new Uniform command permission system

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

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 11:20:40 +01:00
dependabot[bot]
be0b4e3397 deps: bump dev.triumphteam:triumph-gui from 3.1.7 to 3.1.10 (#318)
Bumps [dev.triumphteam:triumph-gui](https://github.com/TriumphTeam/triumph-gui) from 3.1.7 to 3.1.10.
- [Release notes](https://github.com/TriumphTeam/triumph-gui/releases)
- [Commits](https://github.com/TriumphTeam/triumph-gui/compare/3.1.7...3.1.10)

---
updated-dependencies:
- dependency-name: dev.triumphteam:triumph-gui
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 11:20:32 +01:00
dependabot[bot]
dd1ba594de deps: bump org.jetbrains:annotations from 24.0.1 to 24.1.0 (#317)
Bumps [org.jetbrains:annotations](https://github.com/JetBrains/java-annotations) from 24.0.1 to 24.1.0.
- [Release notes](https://github.com/JetBrains/java-annotations/releases)
- [Changelog](https://github.com/JetBrains/java-annotations/blob/master/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/java-annotations/compare/24.0.1...24.1.0)

---
updated-dependencies:
- dependency-name: org.jetbrains:annotations
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 11:20:24 +01:00
William
89368778f3 feat: add support for Fabric targeting Minecraft 1.20.1 (#217)
* Upgrade the Fabric version and rewrite the code.

* Migrate the completed code of version 1.19.2.

* fabric: some events.

* Updated open source license to Apache 2.0.

* Add Plan analyzer support.

* Fix build.

* `UnsupportedOperationException`

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

* Suppress compiler warnings

* Add commands, adjust registration order

* Inventory and ender chest data/serializers

* Update license headers

* Fixup shaded library relocations

* Fix build

* Potion effects & location serializers

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

* Update fabric.mod.json metadata, correct icon

* Events for Fabric (#218)

* Added apache commons pool2 dependency

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

* Added in Item Pickup and Drop events and mixins

* Update husksync.mixins.json

* Switch drop item event to using Network Handler mixin

* Implemented even more events

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

* Re-implement the dropItem mixin

* Set dropItem mixin as cancellable

* deps: Include all bukkit runtime deps

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

* docs: credit Fabric porters :)

* fix: Item deserialization now working

* refactor: Remove inventory debug log

* docs: Update `fabric.mod.json`

* refactor: update with upstream changes

* fix: dangling JD comment

* fix: config file reference fixes

* refactor: optimize imports, fix relocation

* refactor: move tag references to common

* refactor: use lombok for data / serializer methods

* fix: bad annotating

* refactor: adjust callback formatting

* fabric: bump deps, refactor to match main branch

* fabric: more serializer type work

* feat: register more fabric data serializers

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

* feat: implement remaining Fabric serializers

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

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

* feat: add missing mixins

* feat: implement toKeep/toDrop option on Fabric

* feat: apply stats on sync

* build: append fabric MC version to file name

* feat: add HuskSync API support for Fabric

Also updates the docs

* refactor: fixup a deprecation in the wrong spot

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

* feat: implement viewer GUIs on Fabric

* docs: Fabric is in Alpha for now

---------

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

I also corrected some redundant words.

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-04 20:19:41 +01:00
dependabot[bot]
bee01dd15a deps: bump net.kyori:adventure-platform-bukkit from 4.3.2 to 4.3.3 (#313)
Bumps [net.kyori:adventure-platform-bukkit](https://github.com/KyoriPowered/adventure-platform) from 4.3.2 to 4.3.3.
- [Release notes](https://github.com/KyoriPowered/adventure-platform/releases)
- [Commits](https://github.com/KyoriPowered/adventure-platform/compare/v4.3.2...v4.3.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-04 19:37:43 +01:00
dependabot[bot]
e97551e67e deps: bump com.google.guava:guava from 33.2.0-jre to 33.2.1-jre (#312)
Bumps [com.google.guava:guava](https://github.com/google/guava) from 33.2.0-jre to 33.2.1-jre.
- [Release notes](https://github.com/google/guava/releases)
- [Commits](https://github.com/google/guava/commits)

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

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

* fix: correct issues with deterministic sync order

* refactor: adjust base data type dependencies

* refactor: cleanup imports/trim whitespace

* docs: Document Identifier dependencies

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-27 01:11:02 +01:00
dependabot[bot]
2da9749b0c deps: bump net.kyori:adventure-api from 4.16.0 to 4.17.0 (#300)
Bumps [net.kyori:adventure-api](https://github.com/KyoriPowered/adventure) from 4.16.0 to 4.17.0.
- [Release notes](https://github.com/KyoriPowered/adventure/releases)
- [Commits](https://github.com/KyoriPowered/adventure/compare/v4.16.0...v4.17.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-27 01:10:54 +01:00
dependabot[bot]
d4d510e100 --- (#303)
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-27 01:10:29 +01:00
Preva1l
550ea26097 fix: MongoDB duplicate user table entries & not updating cached username (#301) 2024-05-13 10:50:58 +01:00
Preva1l
2b1e72a42e fix: wrong type in PostgreSQL schema (#299)
* Started impl for mongo

* fix silly mistake with postgresql

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 16:27:58 +01:00
William
043b51d812 test: update test suite to Paper 1.20.6 2024-05-01 12:35:19 +01:00
William
fa5cea2aa3 refactor: adjust way advancement messages are cleared, close #285 2024-05-01 12:30:18 +01:00
William
e35dcf3aad feat: Minecraft 1.20.5/6 support (#295)
* feat: start 1.20.5 update testing

nbt-api seems to work great already :)

* feat: add DFU support for legacy upgrade

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

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

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

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

* locales: credit French (fr-fr)

---------

Co-authored-by: William <will27528@gmail.com>
2024-04-23 16:56:45 +01:00
dependabot[bot]
70235963ba deps: bump org.apache.commons:commons-text from 1.11.0 to 1.12.0 (#291)
Bumps org.apache.commons:commons-text from 1.11.0 to 1.12.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-23 16:50:24 +01:00
William
245fbec80c fix: wrong check in legacy stats loading 2024-04-19 14:39:26 +01:00
Nhan Le
4d1a465c03 fix: add Settings packet to allow list (#287)
fix player skin and chunk loading actions get cancelled
2024-04-16 10:57:34 +01:00
dependabot[bot]
07dc0b8c12 deps: bump commons-io:commons-io from 2.16.0 to 2.16.1 (#284)
Bumps commons-io:commons-io from 2.16.0 to 2.16.1.

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-15 14:23:47 +01:00
William
525f15e65b locales: correct typo in error_invalid_data, fix #283 2024-04-14 16:44:26 +01:00
William
017d26673a refactor: adjust imports 2024-04-13 15:23:05 +01:00
138 changed files with 5608 additions and 1421 deletions

View File

@@ -41,4 +41,24 @@ jobs:
id: fetch-version
- name: Get Version
run: |
echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV
echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV
- name: 'Publish to William278.net 🚀'
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
fabric-1.20.1
distro-groups: |
paper
fabric
distro-descriptions: |
Paper
Fabric 1.20.1
files: |
target/HuskSync-Paper-${{ env.version_name }}.jar
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.20.1.jar

View File

@@ -30,4 +30,24 @@ jobs:
uses: mikepenz/action-junit-report@v4
if: success() || failure() # Continue on failure
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
report_paths: '**/build/test-results/test/TEST-*.xml'
- name: 'Publish to William278.net 🚀'
uses: WiIIiam278/bones-publish-action@v1
with:
api-key: ${{ secrets.BONES_API_KEY }}
project: 'husksync'
channel: 'release'
version: ${{ github.event.release.tag_name }}
changelog: ${{ github.event.release.body }}
distro-names: |
paper
fabric-1.20.1
distro-groups: |
paper
fabric
distro-descriptions: |
Paper
Fabric 1.20.1
files: |
target/HuskSync-Paper-${{ github.event.release.tag_name }}.jar
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.20.1.jar

View File

@@ -5,7 +5,7 @@
<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?color=00fb9a&name=Maven&prefix=v" />
<img src="https://repo.william278.net/api/badge/latest/releases/net/william278/husksync/husksync-common?color=00fb9a&name=Maven&prefix=v" />
</a>
<a href="https://discord.gg/tVYhJfyDWG">
<img src="https://img.shields.io/discord/818135932103557162.svg?label=&logo=discord&logoColor=fff&color=7389D8&labelColor=6A7EC2" />
@@ -44,15 +44,15 @@
**Ready?** [It's syncing time!](https://william278.net/docs/husksync/setup)
## Setup
Requires a MySQL/Mongo/PostgreSQL database, a Redis (v5.0+) server and any number of Spigot-based 1.17.1+ Minecraft servers, running Java 17+.
Requires a MySQL/Mongo/PostgreSQL database, a Redis (v5.0+) server and a network of Spigot (1.17.1+) or Fabric (1.20.1) Minecraft servers, running Java 17+.
1. Place the plugin jar file in the /plugins/ directory of each Spigot server. You do not need to install HuskSync as a proxy plugin.
1. Place the plugin jar file in the `/plugins` or `/mods` directory of each Spigot/Fabric server. You do not need to install HuskSync as a proxy plugin.
2. Start, then stop every server to let HuskSync generate the config file.
3. Navigate to the HuskSync config file on each server (~/plugins/HuskSync/config.yml) and fill in both your database and Redis server credentials.
3. Navigate to the HuskSync config file on each server and fill in both your database and Redis server credentials.
4. Start every server again and synchronization will begin.
## Development
To build HuskSync, simply run the following in the root of the repository:
To build HuskSync, simply run the following in the root of the repository (building requires Java 17). Builds will be output in `/target`:
```bash
./gradlew clean build
@@ -66,7 +66,7 @@ HuskSync is licensed under the Apache 2.0 license.
Contributions to the project are welcome&mdash;feel free to open a pull request with new features, improvements and/or fixes!
### Support
Due to its complexity, official binaries and customer support for HuskSync is provided through a paid model. This means that support is only available to users who have purchased a license to the plugin from Spigot, Polymart, Craftaro, or BuiltByBit and have provided proof of purchase. Please join our Discord server if you have done so and need help!
Due to its complexity, official binaries and customer support for HuskSync is provided through a paid model. This means that support is only available to users who have purchased a license to the plugin from Spigot, Polymart, or BuiltByBit and have provided proof of purchase. Please join our Discord server if you have done so and need help!
### Translations
Translations of the plugin locales are welcome to help make the plugin more accessible. Please submit a pull request with your translations as a `.yml` file.

View File

@@ -3,6 +3,7 @@ import org.apache.tools.ant.filters.ReplaceTokens
plugins {
id 'com.github.johnrengelman.shadow' version '8.1.1'
id 'org.cadixdev.licenser' version '0.6.1' apply false
id 'fabric-loom' version '1.7-SNAPSHOT' apply false
id 'org.ajoberstar.grgit' version '5.2.2'
id 'maven-publish'
id 'java'
@@ -69,6 +70,8 @@ allprojects {
repositories {
mavenLocal()
mavenCentral()
maven { url 'https://repo.william278.net/releases/' }
maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
maven { url "https://repo.dmulloy2.net/repository/public/" }
maven { url 'https://repo.codemc.io/repository/maven-public/' }
@@ -77,13 +80,12 @@ allprojects {
maven { url 'https://jitpack.io' }
maven { url 'https://mvn-repo.arim.space/lesser-gpl3/' }
maven { url 'https://libraries.minecraft.net/' }
maven { url 'https://repo.william278.net/releases/' }
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.3'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.3'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.3'
}
test {
@@ -97,14 +99,20 @@ allprojects {
}
processResources {
def tokenMap = rootProject.ext.properties
tokenMap.merge("grgit",'',(s, s2) -> s)
filesMatching(['**/*.json', '**/*.yml']) {
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
tokens: rootProject.ext.properties
tokens: tokenMap
}
}
}
subprojects {
if (['fabric'].contains(project.name)) {
apply plugin: 'fabric-loom'
}
version rootProject.version
archivesBaseName = "${rootProject.name}-${project.name.capitalize()}"
@@ -117,8 +125,13 @@ subprojects {
archiveClassifier.set('')
}
// Append the Minecraft to the version for Fabric projects
if (project.name == 'fabric') {
version += "+mc.${fabric_minecraft_version}"
}
// API publishing
if (['common', 'bukkit'].contains(project.name)) {
if (['common', 'bukkit', 'fabric'].contains(project.name)) {
java {
withSourcesJar()
withJavadocJar()
@@ -157,6 +170,19 @@ subprojects {
}
}
}
if (['fabric'].contains(project.name)) {
publications {
mavenJavaFabric(MavenPublication) {
groupId = 'net.william278.husksync'
artifactId = 'husksync-fabric'
version = "$rootProject.version+${fabric_minecraft_version}"
artifact remapJar
artifact sourcesJar
artifact javadocJar
}
}
}
}
}
@@ -168,11 +194,20 @@ logger.lifecycle("Building HuskSync ${version} by William278")
@SuppressWarnings('GrMethodMayBeStatic')
def versionMetadata() {
// Get the last commit hash and if it's a clean head
// Require grgit
if (grgit == null) {
return '-' + System.getenv("GITHUB_RUN_NUMBER") ? 'build.' + System.getenv("GITHUB_RUN_NUMBER") : 'unknown'
return '-unknown'
}
// If grgit DOES exist, get tag for this commit
// If unclean, return the last commit hash with -indev
if (!grgit.status().clean) {
return '-' + grgit.head().abbreviatedId + '-indev'
}
// Otherwise if this matches a tag, return nothing
def tag = grgit.tag.list().find { it.commit.id == grgit.head().id }
return '-' + grgit.head().abbreviatedId + (grgit.status().clean ? '' : '-indev')
if (tag != null) {
return ''
}
return '-' + grgit.head().abbreviatedId
}

View File

@@ -1,21 +1,21 @@
dependencies {
implementation project(path: ':common')
implementation 'org.bstats:bstats-bukkit:3.0.2'
implementation 'net.william278.uniform:uniform-bukkit:1.2.1'
implementation 'net.william278:mpdbdataconverter:1.0.1'
implementation 'net.william278:hsldataconverter:1.0'
implementation 'net.william278:mapdataapi:1.0.3'
implementation 'net.william278:andjam:1.0.2'
implementation 'me.lucko:commodore:2.2'
implementation 'net.kyori:adventure-platform-bukkit:4.3.2'
implementation 'dev.triumphteam:triumph-gui:3.1.7'
implementation 'org.bstats:bstats-bukkit:3.0.2'
implementation 'net.kyori:adventure-platform-bukkit:4.3.3'
implementation 'dev.triumphteam:triumph-gui:3.1.10'
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
implementation 'de.tr7zw:item-nbt-api:2.12.3'
implementation 'de.tr7zw:item-nbt-api:2.13.2'
compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT'
compileOnly 'com.github.retrooper.packetevents:spigot:2.3.0'
compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0'
compileOnly 'org.projectlombok:lombok:1.18.32'
compileOnly 'commons-io:commons-io:2.16.0'
compileOnly 'org.projectlombok:lombok:1.18.34'
compileOnly 'commons-io:commons-io:2.16.1'
compileOnly 'org.json:json:20240303'
compileOnly 'net.william278:minedown:1.8.2'
compileOnly 'de.exlll:configlib-yaml:4.5.0'
@@ -24,7 +24,7 @@ dependencies {
compileOnly 'net.william278:AdvancementAPI:97a9583413'
compileOnly "redis.clients:jedis:$jedis_version"
annotationProcessor 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.34'
}
shadowJar {
@@ -41,16 +41,15 @@ shadowJar {
relocate 'org.intellij', 'net.william278.husksync.libraries'
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.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
relocate 'net.william278.andjam', 'net.william278.husksync.libraries.andjam'
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
relocate 'org.json', 'net.william278.husksync.libraries.json'
relocate 'net.querz', 'net.william278.husksync.libraries.nbtparser'
relocate 'net.roxeez', 'net.william278.husksync.libraries'
relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore'
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib'

View File

@@ -34,7 +34,7 @@ import net.william278.husksync.adapter.DataAdapter;
import net.william278.husksync.adapter.GsonAdapter;
import net.william278.husksync.adapter.SnappyGsonAdapter;
import net.william278.husksync.api.BukkitHuskSyncAPI;
import net.william278.husksync.command.BukkitCommand;
import net.william278.husksync.command.PluginCommand;
import net.william278.husksync.config.Locales;
import net.william278.husksync.config.Server;
import net.william278.husksync.config.Settings;
@@ -46,7 +46,6 @@ import net.william278.husksync.database.PostgresDatabase;
import net.william278.husksync.event.BukkitEventDispatcher;
import net.william278.husksync.hook.PlanHook;
import net.william278.husksync.listener.BukkitEventListener;
import net.william278.husksync.listener.EventListener;
import net.william278.husksync.migrator.LegacyMigrator;
import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.migrator.MpdbMigrator;
@@ -58,14 +57,19 @@ 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.uniform.Uniform;
import net.william278.uniform.bukkit.BukkitUniform;
import org.bstats.bukkit.Metrics;
import org.bukkit.entity.Player;
import org.bukkit.map.MapView;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import space.arim.morepaperlib.MorePaperLib;
import space.arim.morepaperlib.commands.CommandRegistration;
import space.arim.morepaperlib.scheduling.*;
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
import space.arim.morepaperlib.scheduling.AttachedScheduler;
import space.arim.morepaperlib.scheduling.GracefulScheduling;
import space.arim.morepaperlib.scheduling.RegionalScheduler;
import java.nio.file.Path;
import java.util.*;
@@ -83,7 +87,9 @@ 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 Map<Identifier, Serializer<? extends Data>> serializers = Maps.newLinkedHashMap();
private final TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap(
SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR
);
private final Map<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap();
private final Map<Integer, MapView> mapViews = Maps.newConcurrentMap();
private final List<Migrator> availableMigrators = Lists.newArrayList();
@@ -95,7 +101,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
private MorePaperLib paperLib;
private Database database;
private RedisManager redisManager;
private EventListener eventListener;
private BukkitEventListener eventListener;
private DataAdapter dataAdapter;
private DataSyncer dataSyncer;
private LegacyConverter legacyConverter;
@@ -110,11 +116,10 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
private Server serverName;
@Override
public void onEnable() {
public void onLoad() {
// Initial plugin setup
this.disabling = false;
this.gson = createGson();
this.audiences = BukkitAudiences.create(this);
this.paperLib = new MorePaperLib(this);
// Load settings and locales
@@ -124,6 +129,17 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
loadServer();
});
this.eventListener = createEventListener();
eventListener.onLoad();
}
@Override
public void onEnable() {
this.audiences = BukkitAudiences.create(this);
// Register commands
initialize("commands", (plugin) -> getUniform().register(PluginCommand.Type.create(this)));
// Prepare data adapter
initialize("data adapter", (plugin) -> {
if (settings.getSynchronization().isCompressData()) {
@@ -135,19 +151,20 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
// Prepare serializers
initialize("data serializers", (plugin) -> {
registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this));
registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this));
registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(this));
registerSerializer(Identifier.LOCATION, new BukkitSerializer.Json<>(this, BukkitData.Location.class));
registerSerializer(Identifier.HEALTH, new BukkitSerializer.Json<>(this, BukkitData.Health.class));
registerSerializer(Identifier.HUNGER, new BukkitSerializer.Json<>(this, BukkitData.Hunger.class));
registerSerializer(Identifier.ATTRIBUTES, new BukkitSerializer.Json<>(this, BukkitData.Attributes.class));
registerSerializer(Identifier.GAME_MODE, new BukkitSerializer.Json<>(this, BukkitData.GameMode.class));
registerSerializer(Identifier.FLIGHT_STATUS, new BukkitSerializer.Json<>(this, BukkitData.FlightStatus.class));
registerSerializer(Identifier.STATISTICS, new Serializer.Json<>(this, BukkitData.Statistics.class));
registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this));
registerSerializer(Identifier.STATISTICS, new BukkitSerializer.Json<>(this, BukkitData.Statistics.class));
registerSerializer(Identifier.EXPERIENCE, new BukkitSerializer.Json<>(this, BukkitData.Experience.class));
registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
registerSerializer(Identifier.GAME_MODE, new Serializer.Json<>(this, BukkitData.GameMode.class));
registerSerializer(Identifier.FLIGHT_STATUS, new Serializer.Json<>(this, BukkitData.FlightStatus.class));
registerSerializer(Identifier.ATTRIBUTES, new Serializer.Json<>(this, BukkitData.Attributes.class));
registerSerializer(Identifier.HEALTH, new Serializer.Json<>(this, BukkitData.Health.class));
registerSerializer(Identifier.HUNGER, new Serializer.Json<>(this, BukkitData.Hunger.class));
registerSerializer(Identifier.EXPERIENCE, new Serializer.Json<>(this, BukkitData.Experience.class));
registerSerializer(Identifier.LOCATION, new Serializer.Json<>(this, BukkitData.Location.class));
validateDependencies();
});
// Setup available migrators
@@ -182,10 +199,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
});
// Register events
initialize("events", (plugin) -> this.eventListener = createEventListener());
// Register commands
initialize("commands", (plugin) -> BukkitCommand.Type.registerCommands(this));
initialize("events", (plugin) -> eventListener.onEnable());
// Register plugin hooks
initialize("hooks", (plugin) -> {
@@ -252,6 +266,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
this.dataSyncer = dataSyncer;
}
@Override
@NotNull
public Uniform getUniform() {
return BukkitUniform.getInstance(this);
}
@NotNull
@Override
public Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
@@ -269,7 +289,8 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@Override
public boolean isDependencyLoaded(@NotNull String name) {
return getServer().getPluginManager().getPlugin(name) != null;
final Plugin plugin = getServer().getPluginManager().getPlugin(name);
return plugin != null;
}
// Register bStats metrics
@@ -281,7 +302,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
try {
new Metrics(this, metricsId);
} catch (Throwable e) {
log(Level.WARNING, "Failed to register bStats metrics (" + e.getMessage() + ")");
log(Level.WARNING, "Failed to register bStats metrics (%s)".formatted(e.getMessage()));
}
}
@@ -312,6 +333,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
return PLATFORM_TYPE_ID;
}
@Override
@NotNull
public String getServerVersion() {
return String.format("%s/%s", getServer().getName(), getServer().getVersion());
}
@Override
public Optional<LegacyConverter> getLegacyConverter() {
return Optional.of(legacyConverter);
@@ -339,11 +366,6 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
return getScheduler().entitySpecificScheduler(((BukkitUser) user).getPlayer());
}
@NotNull
public CommandRegistration getCommandRegistrar() {
return paperLib.commandRegistration();
}
@Override
@NotNull
public Path getConfigDirectory() {

View File

@@ -28,6 +28,7 @@ import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
@@ -59,6 +60,10 @@ public class BukkitHuskSyncAPI extends HuskSyncAPI {
*/
@NotNull
public static BukkitHuskSyncAPI getInstance() {
if (!JavaPlugin.getProvidingPlugin(BukkitHuskSyncAPI.class).getName().equals("HuskSync")) {
throw new NotRegisteredException("This is likely because you have shaded HuskSync into your plugin JAR " +
"and need to fix your maven/gradle/build script so that it *compiles against* HuskSync instead.");
}
if (instance == null) {
throw new NotRegisteredException();
}

View File

@@ -1,60 +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.command;
import me.lucko.commodore.CommodoreProvider;
import me.lucko.commodore.file.CommodoreFileReader;
import net.william278.husksync.BukkitHuskSync;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
public class BrigadierUtil {
/**
* Uses commodore to register command completions.
*
* @param plugin instance of the registering Bukkit plugin
* @param bukkitCommand the Bukkit PluginCommand to register completions for
* @param command the {@link Command} to register completions for
*/
protected static void registerCommodore(@NotNull BukkitHuskSync plugin,
@NotNull org.bukkit.command.Command bukkitCommand,
@NotNull Command command) {
final InputStream commodoreFile = plugin.getResource(
"commodore/" + bukkitCommand.getName() + ".commodore"
);
if (commodoreFile == null) {
return;
}
try {
CommodoreProvider.getCommodore(plugin).register(bukkitCommand,
CommodoreFileReader.INSTANCE.parse(commodoreFile),
player -> player.hasPermission(command.getPermission()));
} catch (IOException e) {
plugin.log(Level.SEVERE, String.format(
"Failed to read command commodore completions for %s", bukkitCommand.getName()), e
);
}
}
}

View File

@@ -1,164 +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.command;
import me.lucko.commodore.CommodoreProvider;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.user.BukkitUser;
import net.william278.husksync.user.CommandUser;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
import org.bukkit.plugin.PluginManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Function;
public class BukkitCommand extends org.bukkit.command.Command {
private final BukkitHuskSync plugin;
private final Command command;
public BukkitCommand(@NotNull Command command, @NotNull BukkitHuskSync plugin) {
super(command.getName(), command.getDescription(), command.getUsage(), command.getAliases());
this.command = command;
this.plugin = plugin;
}
@Override
public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) {
this.command.onExecuted(sender instanceof Player p ? BukkitUser.adapt(p, plugin) : plugin.getConsole(), args);
return true;
}
@NotNull
@Override
public List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias,
@NotNull String[] args) throws IllegalArgumentException {
if (!(this.command instanceof TabProvider provider)) {
return List.of();
}
final CommandUser user = sender instanceof Player p ? BukkitUser.adapt(p, plugin) : plugin.getConsole();
if (getPermission() == null || user.hasPermission(getPermission())) {
return provider.getSuggestions(user, args);
}
return List.of();
}
public void register() {
// Register with bukkit
plugin.getCommandRegistrar().getServerCommandMap().register("husksync", this);
// Register permissions
BukkitCommand.addPermission(
plugin,
command.getPermission(),
command.getUsage(),
BukkitCommand.getPermissionDefault(command.isOperatorCommand())
);
final List<Permission> childNodes = command.getAdditionalPermissions()
.entrySet().stream()
.map((entry) -> BukkitCommand.addPermission(
plugin,
entry.getKey(),
"",
BukkitCommand.getPermissionDefault(entry.getValue()))
)
.filter(Objects::nonNull)
.toList();
if (!childNodes.isEmpty()) {
BukkitCommand.addPermission(
plugin,
command.getPermission("*"),
command.getUsage(),
PermissionDefault.FALSE,
childNodes.toArray(new Permission[0])
);
}
// Register commodore TAB completion
if (CommodoreProvider.isSupported() && plugin.getSettings().isBrigadierTabCompletion()) {
BrigadierUtil.registerCommodore(plugin, this, command);
}
}
@Nullable
protected static Permission addPermission(@NotNull BukkitHuskSync plugin, @NotNull String node,
@NotNull String description, @NotNull PermissionDefault permissionDefault,
@NotNull Permission... children) {
final Map<String, Boolean> childNodes = Arrays.stream(children)
.map(Permission::getName)
.collect(HashMap::new, (map, child) -> map.put(child, true), HashMap::putAll);
final PluginManager manager = plugin.getServer().getPluginManager();
if (manager.getPermission(node) != null) {
return null;
}
Permission permission;
if (description.isEmpty()) {
permission = new Permission(node, permissionDefault, childNodes);
} else {
permission = new Permission(node, description, permissionDefault, childNodes);
}
manager.addPermission(permission);
return permission;
}
@NotNull
protected static PermissionDefault getPermissionDefault(boolean isOperatorCommand) {
return isOperatorCommand ? PermissionDefault.OP : PermissionDefault.TRUE;
}
/**
* Commands available on the Bukkit HuskSync implementation
*/
public enum Type {
HUSKSYNC_COMMAND(HuskSyncCommand::new),
USERDATA_COMMAND(UserDataCommand::new),
INVENTORY_COMMAND(InventoryCommand::new),
ENDER_CHEST_COMMAND(EnderChestCommand::new);
public final Function<BukkitHuskSync, Command> commandSupplier;
Type(@NotNull Function<BukkitHuskSync, Command> supplier) {
this.commandSupplier = supplier;
}
@NotNull
public Command createCommand(@NotNull BukkitHuskSync plugin) {
return commandSupplier.apply(plugin);
}
public static void registerCommands(@NotNull BukkitHuskSync plugin) {
Arrays.stream(values())
.map((type) -> type.createCommand(plugin))
.forEach((command) -> new BukkitCommand(command, plugin).register());
}
}
}

View File

@@ -25,10 +25,13 @@ import com.google.gson.annotations.SerializedName;
import de.tr7zw.changeme.nbtapi.NBTCompound;
import de.tr7zw.changeme.nbtapi.NBTPersistentDataContainer;
import lombok.*;
import net.kyori.adventure.util.TriState;
import net.william278.desertwell.util.ThrowingConsumer;
import net.william278.desertwell.util.Version;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.Adaptable;
import net.william278.husksync.config.Settings.SynchronizationSettings.AttributeSettings;
import net.william278.husksync.user.BukkitUser;
import org.bukkit.*;
import org.bukkit.advancement.AdvancementProgress;
@@ -44,7 +47,9 @@ import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -66,7 +71,7 @@ public abstract class BukkitData implements Data {
private final @Nullable ItemStack @NotNull [] contents;
private Items(@Nullable ItemStack @NotNull [] contents) {
this.contents = Arrays.stream(contents)
this.contents = Arrays.stream(contents.clone())
.map(i -> i == null || i.getType() == Material.AIR ? null : i)
.toArray(ItemStack[]::new);
}
@@ -123,18 +128,16 @@ public abstract class BukkitData implements Data {
@Getter
public static class Inventory extends BukkitData.Items implements Data.Items.Inventory {
public static final int INVENTORY_SLOT_COUNT = 41;
@Range(from = 0, to = 8)
private int heldItemSlot;
private Inventory(@NotNull ItemStack[] contents, int heldItemSlot) {
private Inventory(@Nullable ItemStack @NotNull [] contents, int heldItemSlot) {
super(contents);
this.heldItemSlot = heldItemSlot;
}
@NotNull
public static BukkitData.Items.Inventory from(@NotNull ItemStack[] contents, int heldItemSlot) {
public static BukkitData.Items.Inventory from(@Nullable ItemStack @NotNull [] contents, int heldItemSlot) {
return new BukkitData.Items.Inventory(contents, heldItemSlot);
}
@@ -159,11 +162,15 @@ public abstract class BukkitData implements Data {
}
private void clearInventoryCraftingSlots(@NotNull Player player) {
final org.bukkit.inventory.Inventory inventory = player.getOpenInventory().getTopInventory();
if (inventory.getType() == InventoryType.CRAFTING) {
for (int slot = 0; slot < 5; slot++) {
inventory.setItem(slot, null);
try {
final org.bukkit.inventory.Inventory inventory = player.getOpenInventory().getTopInventory();
if (inventory.getType() == InventoryType.CRAFTING) {
for (int slot = 0; slot < 5; slot++) {
inventory.setItem(slot, null);
}
}
} catch (Throwable e) {
// Ignore any exceptions
}
}
@@ -171,15 +178,18 @@ public abstract class BukkitData implements Data {
public static class EnderChest extends BukkitData.Items implements Data.Items.EnderChest {
public static final int ENDER_CHEST_SLOT_COUNT = 27;
private EnderChest(@NotNull ItemStack[] contents) {
private EnderChest(@Nullable ItemStack @NotNull [] contents) {
super(contents);
}
@NotNull
public static BukkitData.Items.EnderChest adapt(@NotNull ItemStack[] items) {
return new BukkitData.Items.EnderChest(items);
public static BukkitData.Items.EnderChest adapt(@Nullable ItemStack @NotNull [] contents) {
return new BukkitData.Items.EnderChest(contents);
}
@NotNull
public static BukkitData.Items.EnderChest adapt(@NotNull Collection<ItemStack> items) {
return adapt(items.toArray(ItemStack[]::new));
}
@NotNull
@@ -196,7 +206,7 @@ public abstract class BukkitData implements Data {
public static class ItemArray extends BukkitData.Items implements Data.Items {
private ItemArray(@NotNull ItemStack[] contents) {
private ItemArray(@Nullable ItemStack @NotNull [] contents) {
super(contents);
}
@@ -206,7 +216,7 @@ public abstract class BukkitData implements Data {
}
@NotNull
public static ItemArray adapt(@NotNull ItemStack[] drops) {
public static ItemArray adapt(@Nullable ItemStack @NotNull [] drops) {
return new ItemArray(drops);
}
@@ -227,33 +237,33 @@ public abstract class BukkitData implements Data {
private final Collection<PotionEffect> effects;
@NotNull
public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> effects) {
return new BukkitData.PotionEffects(effects);
public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> sei) {
return new BukkitData.PotionEffects(Lists.newArrayList(sei.stream().filter(e -> !e.isAmbient()).toList()));
}
@NotNull
public static BukkitData.PotionEffects adapt(@NotNull Collection<Effect> effects) {
return from(
effects.stream()
.map(effect -> new PotionEffect(
Objects.requireNonNull(
PotionEffectType.getByName(effect.type()),
"Invalid potion effect type"
),
effect.duration(),
effect.amplifier(),
effect.isAmbient(),
effect.showParticles(),
effect.hasIcon()
))
.toList()
);
return from(effects.stream()
.map(effect -> {
final PotionEffectType type = matchEffectType(effect.type());
return type != null ? new PotionEffect(
type,
effect.duration(),
effect.amplifier(),
effect.isAmbient(),
effect.showParticles(),
effect.hasIcon()
) : null;
})
.filter(Objects::nonNull)
.toList());
}
@NotNull
@SuppressWarnings("unused")
public static BukkitData.PotionEffects empty() {
return new BukkitData.PotionEffects(List.of());
return new BukkitData.PotionEffects(Lists.newArrayList());
}
@Override
@@ -269,6 +279,7 @@ public abstract class BukkitData implements Data {
@NotNull
@Override
@Unmodifiable
public List<Effect> getActiveEffects() {
return effects.stream()
.map(potionEffect -> new Effect(
@@ -338,19 +349,16 @@ public abstract class BukkitData implements Data {
}));
}
private void setAdvancement(@NotNull HuskSync plugin, @NotNull org.bukkit.advancement.Advancement advancement,
@NotNull Player player, @NotNull BukkitUser user,
@NotNull Collection<String> toAward, @NotNull Collection<String> toRevoke) {
final boolean folia = ((BukkitHuskSync) plugin).getScheduler().isUsingFolia();
private void setAdvancement(@NotNull HuskSync plugin,
@NotNull org.bukkit.advancement.Advancement advancement,
@NotNull Player player,
@NotNull BukkitUser user,
@NotNull Collection<String> toAward,
@NotNull Collection<String> toRevoke) {
plugin.runSync(() -> {
// Track player exp level & progress
final int expLevel = player.getLevel();
final float expProgress = player.getExp();
boolean gameRuleUpdated = false;
if (!folia && Boolean.TRUE.equals(player.getWorld().getGameRuleValue(GameRule.ANNOUNCE_ADVANCEMENTS))) {
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false);
gameRuleUpdated = true;
}
// Award and revoke advancement criteria
final AdvancementProgress progress = player.getAdvancementProgress(advancement);
@@ -358,13 +366,11 @@ public abstract class BukkitData implements Data {
toRevoke.forEach(progress::revokeCriteria);
// Set player experience and level (prevent advancement awards applying twice), reset game rule
if (!toAward.isEmpty() && player.getLevel() != expLevel || player.getExp() != expProgress) {
if (!toAward.isEmpty()
&& (player.getLevel() != expLevel || player.getExp() != expProgress)) {
player.setLevel(expLevel);
player.setExp(expProgress);
}
if (gameRuleUpdated) {
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, true);
}
}, user);
}
@@ -562,19 +568,24 @@ public abstract class BukkitData implements Data {
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Attributes extends BukkitData implements Data.Attributes, Adaptable {
private static final String EQUIPMENT_SLOT_GROUP = "org.bukkit.inventory.EquipmentSlotGroup";
private static final String EQUIPMENT_SLOT_GROUP$ANY = "ANY";
private static final String EQUIPMENT_SLOT$getGroup = "getGroup";
private static TriState USE_KEYED_MODIFIERS = TriState.NOT_SET;
private List<Attribute> attributes;
@NotNull
public static BukkitData.Attributes adapt(@NotNull Player player, @NotNull HuskSync plugin) {
final List<Attribute> attributes = Lists.newArrayList();
final AttributeSettings settings = plugin.getSettings().getSynchronization().getAttributes();
Registry.ATTRIBUTE.forEach(id -> {
final AttributeInstance instance = player.getAttribute(id);
if (instance == null || instance.getValue() == instance.getDefaultValue() || plugin
.getSettings().getSynchronization().isIgnoredAttribute(id.getKey().toString())) {
// We don't sync unmodified or disabled attributes
return;
if (instance == null || Double.compare(instance.getValue(), instance.getDefaultValue()) == 0
|| settings.isIgnoredAttribute(id.getKey().toString())) {
return; // We don't sync unmodified or disabled attributes
}
attributes.add(adapt(instance));
attributes.add(adapt(instance, settings));
});
return new BukkitData.Attributes(attributes);
}
@@ -593,18 +604,20 @@ public abstract class BukkitData implements Data {
}
@NotNull
private static Attribute adapt(@NotNull AttributeInstance instance) {
private static Attribute adapt(@NotNull AttributeInstance instance, @NotNull AttributeSettings settings) {
return new Attribute(
instance.getAttribute().getKey().toString(),
instance.getBaseValue(),
instance.getModifiers().stream().map(BukkitData.Attributes::adapt).collect(Collectors.toSet())
instance.getModifiers().stream()
.filter(modifier -> !settings.isIgnoredModifier(modifier.getName()))
.map(BukkitData.Attributes::adapt).collect(Collectors.toSet())
);
}
@NotNull
private static Modifier adapt(@NotNull AttributeModifier modifier) {
return new Modifier(
modifier.getUniqueId(),
getModifierId(modifier),
modifier.getName(),
modifier.getAmount(),
modifier.getOperation().ordinal(),
@@ -612,28 +625,83 @@ public abstract class BukkitData implements Data {
);
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
Registry.ATTRIBUTE.forEach(id -> applyAttribute(user.getPlayer().getAttribute(id), getAttribute(id).orElse(null)));
@Nullable
private static UUID getModifierId(@NotNull AttributeModifier modifier) {
try {
return modifier.getUniqueId();
} catch (Throwable e) {
return null;
}
}
private static void applyAttribute(@Nullable AttributeInstance instance, @Nullable Attribute attribute) {
private static boolean useKeyedModifiers(@NotNull HuskSync plugin) {
if (USE_KEYED_MODIFIERS == TriState.NOT_SET) {
boolean is1_21 = plugin.getMinecraftVersion().compareTo(Version.fromString("1.21")) >= 0;
USE_KEYED_MODIFIERS = TriState.byBoolean(is1_21);
return is1_21;
}
return Boolean.TRUE.equals(USE_KEYED_MODIFIERS.toBoolean());
}
private static void applyAttribute(@Nullable AttributeInstance instance, @Nullable Attribute attribute,
@NotNull HuskSync plugin) {
if (instance == null) {
return;
}
instance.setBaseValue(attribute == null ? instance.getDefaultValue() : instance.getBaseValue());
instance.setBaseValue(attribute == null ? instance.getDefaultValue() : attribute.baseValue());
instance.getModifiers().forEach(instance::removeModifier);
if (attribute != null) {
attribute.modifiers().forEach(modifier -> instance.addModifier(new AttributeModifier(
modifier.uuid(),
modifier.name(),
modifier.amount(),
AttributeModifier.Operation.values()[modifier.operationType()],
modifier.equipmentSlot() != -1 ? EquipmentSlot.values()[modifier.equipmentSlot()] : null
)));
attribute.modifiers().stream()
.filter(mod -> instance.getModifiers().stream().map(AttributeModifier::getName)
.noneMatch(n -> n.equals(mod.name())))
.distinct()
.filter(mod -> useKeyedModifiers(plugin) == !mod.hasUuid())
.forEach(mod -> instance.addModifier(adapt(mod, plugin)));
}
}
@SuppressWarnings("JavaReflectionMemberAccess")
@NotNull
private static AttributeModifier adapt(@NotNull Modifier modifier, @NotNull HuskSync plugin) {
final int slotId = modifier.equipmentSlot();
if (useKeyedModifiers(plugin)) {
try {
// Reflexively create a modern keyed attribute modifier instance. Remove in favor of API long-term.
final EquipmentSlot slot = slotId != -1 ? EquipmentSlot.values()[slotId] : null;
final Class<?> slotGroup = Class.forName(EQUIPMENT_SLOT_GROUP);
final String modifierName = modifier.name() == null ? modifier.uuid().toString() : modifier.name();
final NamespacedKey modifierKey = Objects.requireNonNull(NamespacedKey.fromString(modifierName),
"Modifier key returned null");
final Constructor<AttributeModifier> constructor = AttributeModifier.class.getDeclaredConstructor(
NamespacedKey.class, double.class, AttributeModifier.Operation.class, slotGroup);
return constructor.newInstance(
modifierKey,
modifier.amount(),
AttributeModifier.Operation.values()[modifier.operationType()],
slot == null ? slotGroup.getField(EQUIPMENT_SLOT_GROUP$ANY).get(null)
: EquipmentSlot.class.getDeclaredMethod(EQUIPMENT_SLOT$getGroup).invoke(slot)
);
} catch (Throwable e) {
plugin.log(Level.WARNING, "Error reflectively creating keyed attribute modifier", e);
USE_KEYED_MODIFIERS = TriState.FALSE;
}
}
return new AttributeModifier(
modifier.uuid(),
modifier.name(),
modifier.amount(),
AttributeModifier.Operation.values()[modifier.operationType()],
slotId != -1 ? EquipmentSlot.values()[slotId] : null
);
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
Registry.ATTRIBUTE.forEach(id -> applyAttribute(
user.getPlayer().getAttribute(id), getAttribute(id).orElse(null), plugin
));
}
}
@Getter
@@ -645,24 +713,38 @@ public abstract class BukkitData implements Data {
private double health;
@SerializedName("health_scale")
private double healthScale;
@SerializedName("is_health_scaled")
private boolean isHealthScaled;
@NotNull
public static BukkitData.Health from(double health, double healthScale) {
return new BukkitData.Health(health, healthScale);
public static BukkitData.Health from(double health, double scale, boolean isScaled) {
return new BukkitData.Health(health, scale, isScaled);
}
/**
* @deprecated Use {@link #from(double, double, boolean)} instead
*/
@NotNull
@Deprecated(since = "3.5.4")
public static BukkitData.Health from(double health, double scale) {
return from(health, scale, false);
}
/**
* @deprecated Use {@link #from(double, double, boolean)} instead
*/
@NotNull
@Deprecated(forRemoval = true, since = "3.5")
@SuppressWarnings("unused")
public static BukkitData.Health from(double health, double maxHealth, double healthScale) {
return from(health, healthScale);
public static BukkitData.Health from(double health, @SuppressWarnings("unused") double max, double scale) {
return from(health, scale, false);
}
@NotNull
public static BukkitData.Health adapt(@NotNull Player player) {
return from(
player.getHealth(),
player.isHealthScaled() ? player.getHealthScale() : 0d
player.getHealthScale(),
player.isHealthScaled()
);
}
@@ -679,16 +761,12 @@ public abstract class BukkitData implements Data {
}
// Set health scale
double scale = healthScale <= 0 ? player.getMaxHealth() : healthScale;
try {
if (healthScale != 0d) {
player.setHealthScaled(true);
player.setHealthScale(healthScale);
} else {
player.setHealthScaled(false);
player.setHealthScale(player.getMaxHealth());
}
player.setHealthScale(scale);
player.setHealthScaled(isHealthScaled);
} catch (Throwable e) {
plugin.log(Level.WARNING, "Error setting %s's health scale to %s".formatted(player.getName(), healthScale), e);
plugin.log(Level.WARNING, "Error setting %s's health scale to %s".formatted(player.getName(), scale), e);
}
}

View File

@@ -21,20 +21,29 @@ package net.william278.husksync.data;
import com.google.gson.reflect.TypeToken;
import de.tr7zw.changeme.nbtapi.NBT;
import de.tr7zw.changeme.nbtapi.NBTCompound;
import de.tr7zw.changeme.nbtapi.NBTContainer;
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBTCompoundList;
import de.tr7zw.changeme.nbtapi.utils.DataFixerUtil;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import net.william278.desertwell.util.Version;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.Adaptable;
import net.william278.husksync.api.HuskSyncAPI;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import static net.william278.husksync.data.BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
import static net.william278.husksync.data.Data.Items.Inventory.HELD_ITEM_SLOT_TAG;
import static net.william278.husksync.data.Data.Items.Inventory.ITEMS_TAG;
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class BukkitSerializer {
@@ -52,25 +61,29 @@ public class BukkitSerializer {
return plugin;
}
public static class Inventory extends BukkitSerializer implements Serializer<BukkitData.Items.Inventory> {
private static final String ITEMS_TAG = "items";
private static final String HELD_ITEM_SLOT_TAG = "held_item_slot";
public static class Inventory extends BukkitSerializer implements Serializer<BukkitData.Items.Inventory>,
ItemDeserializer {
public Inventory(@NotNull HuskSync plugin) {
super(plugin);
}
@Override
public BukkitData.Items.Inventory deserialize(@NotNull String serialized) throws DeserializationException {
public BukkitData.Items.Inventory deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
throws DeserializationException {
final ReadWriteNBT root = NBT.parseNBT(serialized);
final ItemStack[] items = root.getItemStackArray(ITEMS_TAG);
final int heldItemSlot = root.getInteger(HELD_ITEM_SLOT_TAG);
final ReadWriteNBT items = root.hasTag(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null;
return BukkitData.Items.Inventory.from(
items == null ? new ItemStack[INVENTORY_SLOT_COUNT] : items,
heldItemSlot
items != null ? getItems(items, dataMcVersion) : new ItemStack[INVENTORY_SLOT_COUNT],
root.hasTag(HELD_ITEM_SLOT_TAG) ? root.getInteger(HELD_ITEM_SLOT_TAG) : 0
);
}
@Override
public BukkitData.Items.Inventory deserialize(@NotNull String serialized) {
return deserialize(serialized, plugin.getMinecraftVersion());
}
@NotNull
@Override
public String serialize(@NotNull BukkitData.Items.Inventory data) throws SerializationException {
@@ -82,18 +95,25 @@ public class BukkitSerializer {
}
public static class EnderChest extends BukkitSerializer implements Serializer<BukkitData.Items.EnderChest> {
public static class EnderChest extends BukkitSerializer implements Serializer<BukkitData.Items.EnderChest>,
ItemDeserializer {
public EnderChest(@NotNull HuskSync plugin) {
super(plugin);
}
@Override
public BukkitData.Items.EnderChest deserialize(@NotNull String serialized) throws DeserializationException {
final ItemStack[] items = NBT.itemStackArrayFromNBT(NBT.parseNBT(serialized));
public BukkitData.Items.EnderChest deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
throws DeserializationException {
final ItemStack[] items = getItems(NBT.parseNBT(serialized), dataMcVersion);
return items == null ? BukkitData.Items.EnderChest.empty() : BukkitData.Items.EnderChest.adapt(items);
}
@Override
public BukkitData.Items.EnderChest deserialize(@NotNull String serialized) {
return deserialize(serialized, plugin.getMinecraftVersion());
}
@NotNull
@Override
public String serialize(@NotNull BukkitData.Items.EnderChest data) throws SerializationException {
@@ -101,6 +121,59 @@ public class BukkitSerializer {
}
}
// Utility interface for deserializing and upgrading item stacks from legacy versions
private interface ItemDeserializer {
@Nullable
default ItemStack[] getItems(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion) {
if (mcVersion.compareTo(getPlugin().getMinecraftVersion()) < 0) {
return upgradeItemStacks((NBTCompound) tag, mcVersion);
}
return NBT.itemStackArrayFromNBT(tag);
}
@NotNull
private ItemStack @NotNull [] upgradeItemStacks(@NotNull NBTCompound itemsNbt, @NotNull Version mcVersion) {
final ReadWriteNBTCompoundList items = itemsNbt.getCompoundList("items");
final ItemStack[] itemStacks = new ItemStack[itemsNbt.getInteger("size")];
for (int i = 0; i < items.size(); i++) {
if (items.get(i) == null) {
itemStacks[i] = new ItemStack(Material.AIR);
continue;
}
try {
itemStacks[i] = NBT.itemStackFromNBT(upgradeItemData(items.get(i), mcVersion));
} catch (Throwable e) {
itemStacks[i] = new ItemStack(Material.AIR);
}
}
return itemStacks;
}
@NotNull
private ReadWriteNBT upgradeItemData(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion)
throws NoSuchFieldException, IllegalAccessException {
return DataFixerUtil.fixUpItemData(tag, getDataVersion(mcVersion), DataFixerUtil.getCurrentVersion());
}
private int getDataVersion(@NotNull Version mcVersion) {
return switch (mcVersion.toStringWithoutMetadata()) {
case "1.16", "1.16.1", "1.16.2", "1.16.3", "1.16.4", "1.16.5" -> DataFixerUtil.VERSION1_16_5;
case "1.17", "1.17.1" -> DataFixerUtil.VERSION1_17_1;
case "1.18", "1.18.1", "1.18.2" -> DataFixerUtil.VERSION1_18_2;
case "1.19", "1.19.1", "1.19.2" -> DataFixerUtil.VERSION1_19_2;
case "1.20", "1.20.1", "1.20.2" -> DataFixerUtil.VERSION1_20_2;
case "1.20.3", "1.20.4" -> DataFixerUtil.VERSION1_20_4;
case "1.20.5", "1.20.6" -> DataFixerUtil.VERSION1_20_5;
case "1.21" -> DataFixerUtil.VERSION1_21;
default -> DataFixerUtil.getCurrentVersion();
};
}
@NotNull
HuskSync getPlugin();
}
public static class PotionEffects extends BukkitSerializer implements Serializer<BukkitData.PotionEffects> {
private static final TypeToken<List<Data.PotionEffects.Effect>> TYPE = new TypeToken<>() {
@@ -167,24 +240,19 @@ public class BukkitSerializer {
}
public static class Json<T extends Data & Adaptable> extends BukkitSerializer implements Serializer<T> {
/**
* @deprecated Use {@link Serializer.Json} in the common module instead
*/
@Deprecated(since = "2.6")
public class Json<T extends Data & Adaptable> extends Serializer.Json<T> {
private final Class<T> type;
public Json(@NotNull HuskSync plugin, Class<T> type) {
super(plugin);
this.type = type;
}
@Override
public T deserialize(@NotNull String serialized) throws DeserializationException {
return plugin.getDataAdapter().fromJson(serialized, type);
public Json(@NotNull HuskSync plugin, @NotNull Class<T> type) {
super(plugin, type);
}
@NotNull
@Override
public String serialize(@NotNull T element) throws SerializationException {
return plugin.getDataAdapter().toJson(element);
public BukkitHuskSync getPlugin() {
return (BukkitHuskSync) plugin;
}
}

View File

@@ -67,9 +67,9 @@ public interface BukkitUserDataHolder extends UserDataHolder {
.isSyncDeadPlayersChangingServer())) {
return Optional.of(BukkitData.Items.Inventory.empty());
}
final PlayerInventory inventory = getBukkitPlayer().getInventory();
final PlayerInventory inventory = getPlayer().getInventory();
return Optional.of(BukkitData.Items.Inventory.from(
getMapPersister().persistLockedMaps(inventory.getContents(), getBukkitPlayer()),
getMapPersister().persistLockedMaps(inventory.getContents(), getPlayer()),
inventory.getHeldItemSlot()
));
}
@@ -78,80 +78,89 @@ public interface BukkitUserDataHolder extends UserDataHolder {
@Override
default Optional<Data.Items.EnderChest> getEnderChest() {
return Optional.of(BukkitData.Items.EnderChest.adapt(
getMapPersister().persistLockedMaps(getBukkitPlayer().getEnderChest().getContents(), getBukkitPlayer())
getMapPersister().persistLockedMaps(getPlayer().getEnderChest().getContents(), getPlayer())
));
}
@NotNull
@Override
default Optional<Data.PotionEffects> getPotionEffects() {
return Optional.of(BukkitData.PotionEffects.from(getBukkitPlayer().getActivePotionEffects()));
return Optional.of(BukkitData.PotionEffects.from(getPlayer().getActivePotionEffects()));
}
@NotNull
@Override
default Optional<Data.Advancements> getAdvancements() {
return Optional.of(BukkitData.Advancements.adapt(getBukkitPlayer()));
return Optional.of(BukkitData.Advancements.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.Location> getLocation() {
return Optional.of(BukkitData.Location.adapt(getBukkitPlayer().getLocation()));
return Optional.of(BukkitData.Location.adapt(getPlayer().getLocation()));
}
@NotNull
@Override
default Optional<Data.Statistics> getStatistics() {
return Optional.of(BukkitData.Statistics.adapt(getBukkitPlayer()));
return Optional.of(BukkitData.Statistics.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.Health> getHealth() {
return Optional.of(BukkitData.Health.adapt(getBukkitPlayer()));
return Optional.of(BukkitData.Health.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.Hunger> getHunger() {
return Optional.of(BukkitData.Hunger.adapt(getBukkitPlayer()));
return Optional.of(BukkitData.Hunger.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.Attributes> getAttributes() {
return Optional.of(BukkitData.Attributes.adapt(getBukkitPlayer(), getPlugin()));
return Optional.of(BukkitData.Attributes.adapt(getPlayer(), getPlugin()));
}
@NotNull
@Override
default Optional<Data.Experience> getExperience() {
return Optional.of(BukkitData.Experience.adapt(getBukkitPlayer()));
return Optional.of(BukkitData.Experience.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.GameMode> getGameMode() {
return Optional.of(BukkitData.GameMode.adapt(getBukkitPlayer()));
return Optional.of(BukkitData.GameMode.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.FlightStatus> getFlightStatus() {
return Optional.of(BukkitData.FlightStatus.adapt(getBukkitPlayer()));
return Optional.of(BukkitData.FlightStatus.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.PersistentData> getPersistentData() {
return Optional.of(BukkitData.PersistentData.adapt(getBukkitPlayer().getPersistentDataContainer()));
return Optional.of(BukkitData.PersistentData.adapt(getPlayer().getPersistentDataContainer()));
}
boolean isDead();
@NotNull
Player getBukkitPlayer();
Player getPlayer();
/**
* @deprecated Use {@link #getPlayer()} instead
*/
@Deprecated(since = "3.6")
@NotNull
default Player getBukkitPlayer() {
return getPlayer();
}
@NotNull
default BukkitMapPersister getMapPersister() {

View File

@@ -20,7 +20,6 @@
package net.william278.husksync.listener;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.BukkitData;
import net.william278.husksync.user.BukkitUser;
import net.william278.husksync.user.OnlineUser;
@@ -40,21 +39,38 @@ import java.util.stream.Collectors;
public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
BukkitDeathEventListener, Listener {
protected final LockedHandler lockedHandler;
protected LockedHandler lockedHandler;
public BukkitEventListener(@NotNull BukkitHuskSync plugin) {
super(plugin);
plugin.getServer().getPluginManager().registerEvents(this, plugin);
this.lockedHandler = createLockedHandler(plugin);
}
public void onLoad() {
this.lockedHandler = createLockedHandler((BukkitHuskSync) plugin);
}
public void onEnable() {
getPlugin().getServer().getPluginManager().registerEvents(this, getPlugin());
lockedHandler.onEnable();
}
public void handlePluginDisable() {
super.handlePluginDisable();
lockedHandler.onDisable();
}
@NotNull
private LockedHandler createLockedHandler(@NotNull BukkitHuskSync plugin) {
if (getPlugin().isDependencyLoaded("ProtocolLib") && getPlugin().getSettings().isCancelPackets()) {
return new BukkitLockedPacketListener(plugin);
} else {
if (!getPlugin().getSettings().isCancelPackets()) {
return new BukkitLockedEventListener(plugin);
}
if (getPlugin().isDependencyLoaded("PacketEvents")) {
return new BukkitPacketEventsLockedPacketListener(plugin);
} else if (getPlugin().isDependencyLoaded("ProtocolLib")) {
return new BukkitProtocolLibLockedPacketListener(plugin);
}
return new BukkitLockedEventListener(plugin);
}
@Override
@@ -134,8 +150,8 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
@NotNull
@Override
public HuskSync getPlugin() {
return plugin;
public BukkitHuskSync getPlugin() {
return (BukkitHuskSync) plugin;
}
}

View File

@@ -50,6 +50,10 @@ public class BukkitLockedEventListener implements LockedHandler, Listener {
protected BukkitLockedEventListener(@NotNull BukkitHuskSync plugin) {
this.plugin = plugin;
}
@Override
public void onEnable() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}

View File

@@ -0,0 +1,117 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.listener;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
import com.github.retrooper.packetevents.event.PacketListenerPriority;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.google.common.collect.Sets;
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
import net.william278.husksync.BukkitHuskSync;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
import java.util.logging.Level;
public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
protected BukkitPacketEventsLockedPacketListener(@NotNull BukkitHuskSync plugin) {
super(plugin);
}
@Override
public void onLoad() {
super.onLoad();
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(getPlugin()));
PacketEvents.getAPI().getSettings().reEncodeByDefault(false)
.checkForUpdates(false)
.bStats(true);
PacketEvents.getAPI().load();
}
@Override
public void onEnable() {
super.onEnable();
PacketEvents.getAPI().getEventManager().registerListener(new PlayerPacketAdapter(this));
PacketEvents.getAPI().init();
plugin.log(Level.INFO, "Using PacketEvents to cancel packets for locked players");
}
private static class PlayerPacketAdapter extends PacketListenerAbstract {
private static final Set<PacketType.Play.Client> ALLOWED_PACKETS = Set.of(
PacketType.Play.Client.KEEP_ALIVE, PacketType.Play.Client.PONG, PacketType.Play.Client.PLUGIN_MESSAGE, // Connection packets
PacketType.Play.Client.CHAT_MESSAGE, PacketType.Play.Client.CHAT_COMMAND, PacketType.Play.Client.CHAT_SESSION_UPDATE, // Chat / command packets
PacketType.Play.Client.PLAYER_POSITION, PacketType.Play.Client.PLAYER_POSITION_AND_ROTATION, PacketType.Play.Client.PLAYER_ROTATION, // Movement packets
PacketType.Play.Client.HELD_ITEM_CHANGE, PacketType.Play.Client.ANIMATION, PacketType.Play.Client.TELEPORT_CONFIRM, // Animation packets
PacketType.Play.Client.CLIENT_SETTINGS // Video setting packets
);
private static final Set<PacketType.Play.Client> CANCEL_PACKETS = getPacketsToListenFor();
private final BukkitPacketEventsLockedPacketListener listener;
public PlayerPacketAdapter(@NotNull BukkitPacketEventsLockedPacketListener listener) {
super(PacketListenerPriority.HIGH);
this.listener = listener;
}
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
return;
}
if (!CANCEL_PACKETS.contains(client)) {
return;
}
if (listener.cancelPlayerEvent(event.getUser().getUUID())) {
event.setCancelled(true);
}
}
@Override
public void onPacketSend(PacketSendEvent event) {
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
return;
}
if (!CANCEL_PACKETS.contains(client)) {
return;
}
if (listener.cancelPlayerEvent(event.getUser().getUUID())) {
event.setCancelled(true);
}
}
// Returns the set of ALL Server packets, excluding the set of allowed packets
@NotNull
private static Set<PacketType.Play.Client> getPacketsToListenFor() {
return Sets.difference(
Sets.newHashSet(PacketType.Play.Client.values()),
ALLOWED_PACKETS
);
}
}
}

View File

@@ -34,10 +34,15 @@ import java.util.stream.Collectors;
import static com.comphenix.protocol.PacketType.Play.Client;
public class BukkitLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
public class BukkitProtocolLibLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
protected BukkitLockedPacketListener(@NotNull BukkitHuskSync plugin) {
protected BukkitProtocolLibLockedPacketListener(@NotNull BukkitHuskSync plugin) {
super(plugin);
}
@Override
public void onEnable() {
super.onEnable();
ProtocolLibrary.getProtocolManager().addPacketListener(new PlayerPacketAdapter(this));
plugin.log(Level.INFO, "Using ProtocolLib to cancel packets for locked players");
}
@@ -49,12 +54,13 @@ public class BukkitLockedPacketListener extends BukkitLockedEventListener implem
Client.KEEP_ALIVE, Client.PONG, Client.CUSTOM_PAYLOAD, // Connection packets
Client.CHAT_COMMAND, Client.CLIENT_COMMAND, Client.CHAT, Client.CHAT_SESSION_UPDATE, // Chat / command packets
Client.POSITION, Client.POSITION_LOOK, Client.LOOK, // Movement packets
Client.HELD_ITEM_SLOT, Client.ARM_ANIMATION, Client.TELEPORT_ACCEPT // Animation packets
Client.HELD_ITEM_SLOT, Client.ARM_ANIMATION, Client.TELEPORT_ACCEPT, // Animation packets
Client.SETTINGS // Video setting packets
);
private final BukkitLockedPacketListener listener;
private final BukkitProtocolLibLockedPacketListener listener;
public PlayerPacketAdapter(@NotNull BukkitLockedPacketListener listener) {
public PlayerPacketAdapter(@NotNull BukkitProtocolLibLockedPacketListener listener) {
super(listener.getPlugin(), ListenerPriority.HIGHEST, getPacketsToListenFor());
this.listener = listener;
}

View File

@@ -204,10 +204,10 @@ public class LegacyMigrator extends Migrator {
}) {
plugin.log(Level.INFO, getHelpMenu());
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1]));
obfuscateDataString(args[1]));
} else {
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
obfuscateDataString(args[1]) + " (is it a valid option?)");
obfuscateDataString(args[1]) + " (is it a valid option?)");
}
} else {
plugin.log(Level.INFO, getHelpMenu());
@@ -330,7 +330,7 @@ public class LegacyMigrator extends Migrator {
))
// Health, hunger, experience & game mode
.health(BukkitData.Health.from(health, healthScale))
.health(BukkitData.Health.from(health, healthScale, false))
.hunger(BukkitData.Hunger.from(hunger, saturation, saturationExhaustion))
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
.gameMode(BukkitData.GameMode.from(gameMode))

View File

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

View File

@@ -23,14 +23,10 @@ import de.themoep.minedown.adventure.MineDown;
import dev.triumphteam.gui.builder.gui.StorageBuilder;
import dev.triumphteam.gui.guis.Gui;
import dev.triumphteam.gui.guis.StorageGui;
import net.roxeez.advancement.display.FrameType;
import net.william278.andjam.Toast;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.BukkitData;
import net.william278.husksync.data.BukkitUserDataHolder;
import net.william278.husksync.data.Data;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.ApiStatus;
@@ -40,8 +36,6 @@ import java.util.Arrays;
import java.util.function.Consumer;
import java.util.logging.Level;
import static net.william278.husksync.util.BukkitKeyedAdapter.matchMaterial;
/**
* Bukkit platform implementation of an {@link OnlineUser}
*/
@@ -62,37 +56,18 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
return new BukkitUser(player, plugin);
}
/**
* Get the Bukkit {@link Player} instance of this user
*
* @return the {@link Player} instance
* @since 3.0
*/
@NotNull
public Player getPlayer() {
return player;
}
@Override
public boolean isOffline() {
return player == null || !player.isOnline();
}
@Override
@Deprecated(since = "3.6.7")
public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
@NotNull String iconMaterial, @NotNull String backgroundType) {
try {
final Material material = matchMaterial(iconMaterial);
Toast.builder((BukkitHuskSync) plugin)
.setTitle(title.toComponent())
.setDescription(description.toComponent())
.setIcon(material != null ? material : Material.BARRIER)
.setFrameType(FrameType.valueOf(backgroundType))
.build()
.show(player);
} catch (Throwable e) {
plugin.log(Level.WARNING, "Failed to send toast to player " + player.getName(), e);
}
plugin.log(Level.WARNING, "Toast notifications are deprecated. " +
"Please change your notification display slot to CHAT, ACTION_BAR or NONE.");
this.sendActionBar(title);
}
@Override
@@ -132,9 +107,14 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
return player.hasMetadata("NPC");
}
/**
* Get the Bukkit {@link Player} instance of this user
*
* @return the {@link Player} instance
* @since 3.6
*/
@NotNull
@Override
public Player getBukkitPlayer() {
public Player getPlayer() {
return player;
}

View File

@@ -19,38 +19,44 @@
package net.william278.husksync.util;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.Statistic;
import org.bukkit.*;
import org.bukkit.attribute.Attribute;
import org.bukkit.entity.EntityType;
import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
// Utility class for adapting "Keyed" Bukkit objects
public final class BukkitKeyedAdapter {
@Nullable
public static Statistic matchStatistic(@NotNull String key) {
return Registry.STATISTIC.get(Objects.requireNonNull(NamespacedKey.fromString(key), "Null key"));
return getRegistryValue(Registry.STATISTIC, key);
}
@Nullable
public static EntityType matchEntityType(@NotNull String key) {
return Registry.ENTITY_TYPE.get(Objects.requireNonNull(NamespacedKey.fromString(key), "Null key"));
return getRegistryValue(Registry.ENTITY_TYPE, key);
}
@Nullable
public static Material matchMaterial(@NotNull String key) {
return Registry.MATERIAL.get(Objects.requireNonNull(NamespacedKey.fromString(key), "Null key"));
return getRegistryValue(Registry.MATERIAL, key);
}
@Nullable
public static Attribute matchAttribute(@NotNull String key) {
return Registry.ATTRIBUTE.get(Objects.requireNonNull(NamespacedKey.fromString(key), "Null key"));
return getRegistryValue(Registry.ATTRIBUTE, key);
}
@Nullable
public static PotionEffectType matchEffectType(@NotNull String key) {
return PotionEffectType.getByName(key); // No registry for this in 1.17 API
}
private static <T extends Keyed> T getRegistryValue(@NotNull Registry<T> registry, @NotNull String keyString) {
final NamespacedKey key = NamespacedKey.fromString(keyString);
return key != null ? registry.get(key) : null;
}
}

View File

@@ -82,32 +82,33 @@ public class BukkitLegacyConverter extends LegacyConverter {
final JSONObject status = object.getJSONObject("status_data");
final HashMap<Identifier, Data> containers = Maps.newHashMap();
if (shouldImport(Identifier.HEALTH)) {
if (Identifier.HEALTH.isEnabled()) {
containers.put(Identifier.HEALTH, BukkitData.Health.from(
status.getDouble("health"),
status.getDouble("health_scale")
status.getDouble("health_scale"),
false
));
}
if (shouldImport(Identifier.HUNGER)) {
if (Identifier.HUNGER.isEnabled()) {
containers.put(Identifier.HUNGER, BukkitData.Hunger.from(
status.getInt("hunger"),
status.getFloat("saturation"),
status.getFloat("saturation_exhaustion")
));
}
if (shouldImport(Identifier.EXPERIENCE)) {
if (Identifier.EXPERIENCE.isEnabled()) {
containers.put(Identifier.EXPERIENCE, BukkitData.Experience.from(
status.getInt("total_experience"),
status.getInt("experience_level"),
status.getFloat("experience_progress")
));
}
if (shouldImport(Identifier.GAME_MODE)) {
if (Identifier.GAME_MODE.isEnabled()) {
containers.put(Identifier.GAME_MODE, BukkitData.GameMode.from(
status.getString("game_mode")
));
}
if (shouldImport(Identifier.FLIGHT_STATUS)) {
if (Identifier.FLIGHT_STATUS.isEnabled()) {
containers.put(Identifier.FLIGHT_STATUS, BukkitData.FlightStatus.from(
status.getBoolean("is_flying"),
status.getBoolean("is_flying")
@@ -118,7 +119,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
@NotNull
private Optional<Data.Items.Inventory> readInventory(@NotNull JSONObject object) {
if (!object.has("inventory") || !shouldImport(Identifier.INVENTORY)) {
if (!object.has("inventory") || !Identifier.INVENTORY.isEnabled()) {
return Optional.empty();
}
@@ -130,7 +131,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
@NotNull
private Optional<Data.Items.EnderChest> readEnderChest(@NotNull JSONObject object) {
if (!object.has("ender_chest") || !shouldImport(Identifier.ENDER_CHEST)) {
if (!object.has("ender_chest") || !Identifier.ENDER_CHEST.isEnabled()) {
return Optional.empty();
}
@@ -142,7 +143,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
@NotNull
private Optional<Data.Location> readLocation(@NotNull JSONObject object) {
if (!object.has("location") || !shouldImport(Identifier.LOCATION)) {
if (!object.has("location") || !Identifier.LOCATION.isEnabled()) {
return Optional.empty();
}
@@ -163,7 +164,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
@NotNull
private Optional<Data.Advancements> readAdvancements(@NotNull JSONObject object) {
if (!object.has("advancements") || !shouldImport(Identifier.ADVANCEMENTS)) {
if (!object.has("advancements") || !Identifier.ADVANCEMENTS.isEnabled()) {
return Optional.empty();
}
@@ -186,7 +187,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
@NotNull
private Optional<Data.Statistics> readStatistics(@NotNull JSONObject object) {
if (!object.has("statistics") || !shouldImport(Identifier.ADVANCEMENTS)) {
if (!object.has("statistics") || !Identifier.STATISTICS.isEnabled()) {
return Optional.empty();
}
@@ -280,11 +281,6 @@ public class BukkitLegacyConverter extends LegacyConverter {
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
}
private boolean shouldImport(@NotNull Identifier type) {
return plugin.getSettings().getSynchronization().isFeatureEnabled(type);
}
@NotNull
private Date parseDate(@NotNull String dateString) {
try {

View File

@@ -31,8 +31,10 @@ import net.william278.mapdataapi.MapData;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.ShulkerBox;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BlockStateMeta;
import org.bukkit.inventory.meta.MapMeta;
import org.bukkit.map.*;
import org.jetbrains.annotations.ApiStatus;
@@ -94,6 +96,9 @@ public interface BukkitMapPersister {
}
if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) {
items[i] = function.apply(item);
} else if (item.getItemMeta() instanceof BlockStateMeta b && b.getBlockState() instanceof ShulkerBox box) {
forEachMap(box.getInventory().getContents(), function);
b.setBlockState(box);
}
}
return items;
@@ -419,19 +424,23 @@ 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().name().toLowerCase(Locale.ENGLISH);
if (type.startsWith(BANNER_PREFIX)) {
banners.add(new MapBanner(
type.replaceAll(BANNER_PREFIX, ""),
cursor.getCaption() == null ? "" : cursor.getCaption(),
cursor.getX(),
mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128,
cursor.getY()
));
try {
final String BANNER_PREFIX = "banner_";
for (int i = 0; i < getCursors().size(); i++) {
final MapCursor cursor = getCursors().getCursor(i);
final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH);
if (type.startsWith(BANNER_PREFIX)) {
banners.add(new MapBanner(
type.replaceAll(BANNER_PREFIX, ""),
cursor.getCaption() == null ? "" : cursor.getCaption(),
cursor.getX(),
mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128,
cursor.getY()
));
}
}
} catch (Throwable ignored) {
}
return MapData.fromPixels(pixels, getDimension(), (byte) 2, banners, List.of());
}

View File

@@ -1,3 +0,0 @@
inventory {
name brigadier:string single_word;
}

View File

@@ -1,6 +0,0 @@
husksync {
update;
about;
status;
reload;
}

View File

@@ -1,3 +0,0 @@
enderchest {
name brigadier:string single_word;
}

View File

@@ -1,35 +0,0 @@
userdata {
view {
name brigadier:string single_word {
version brigadier:string single_word;
}
}
list {
name brigadier:string single_word {
page brigadier:integer;
}
}
delete {
name brigadier:string single_word {
version brigadier:string single_word;
}
}
restore {
name brigadier:string single_word {
version brigadier:string single_word;
}
}
pin {
name brigadier:string single_word {
version brigadier:string single_word;
}
}
dump {
name brigadier:string single_word {
version brigadier:string single_word {
web;
file;
}
}
}
}

View File

@@ -7,6 +7,7 @@ description: '${description}'
website: 'https://william278.net'
folia-supported: true
softdepend:
- 'packetevents'
- 'ProtocolLib'
- 'MysqlPlayerDataBridge'
- 'Plan'

View File

@@ -3,11 +3,11 @@ plugins {
}
dependencies {
api 'commons-io:commons-io:2.16.0'
api 'org.apache.commons:commons-text:1.11.0'
api 'commons-io:commons-io:2.16.1'
api 'org.apache.commons:commons-text:1.12.0'
api 'net.william278:minedown:1.8.2'
api 'org.json:json:20240303'
api 'com.google.code.gson:gson:2.10.1'
api 'com.google.code.gson:gson:2.11.0'
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
api 'de.exlll:configlib-yaml:4.5.0'
api 'net.william278:paginedown:1.1.2'
@@ -16,11 +16,13 @@ dependencies {
exclude module: 'slf4j-api'
}
compileOnly 'org.projectlombok:lombok:1.18.32'
compileOnly 'net.william278.uniform:uniform-common:1.2.1'
compileOnly 'com.mojang:brigadier:1.1.8'
compileOnly 'org.projectlombok:lombok:1.18.34'
compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'net.kyori:adventure-api:4.16.0'
compileOnly 'net.kyori:adventure-platform-api:4.3.2'
compileOnly 'com.google.guava:guava:33.1.0-jre'
compileOnly 'net.kyori:adventure-api:4.17.0'
compileOnly 'net.kyori:adventure-platform-api:4.3.3'
compileOnly 'com.google.guava:guava:33.2.1-jre'
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
compileOnly "redis.clients:jedis:$jedis_version"
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
@@ -31,10 +33,10 @@ dependencies {
testImplementation "redis.clients:jedis:$jedis_version"
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
testImplementation 'com.google.guava:guava:33.1.0-jre'
testImplementation 'com.google.guava:guava:33.2.1-jre'
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
testCompileOnly 'de.exlll:configlib-yaml:4.5.0'
testCompileOnly 'org.jetbrains:annotations:24.1.0'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.34'
}

View File

@@ -31,7 +31,7 @@ import net.william278.husksync.adapter.DataAdapter;
import net.william278.husksync.config.ConfigProvider;
import net.william278.husksync.data.Data;
import net.william278.husksync.data.Identifier;
import net.william278.husksync.data.Serializer;
import net.william278.husksync.data.SerializerRegistry;
import net.william278.husksync.database.Database;
import net.william278.husksync.event.EventDispatcher;
import net.william278.husksync.migrator.Migrator;
@@ -41,9 +41,9 @@ import net.william278.husksync.user.ConsoleUser;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.util.LegacyConverter;
import net.william278.husksync.util.Task;
import net.william278.uniform.Uniform;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.*;
@@ -52,7 +52,7 @@ import java.util.logging.Level;
/**
* Abstract implementation of the HuskSync plugin.
*/
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider {
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider, SerializerRegistry {
int SPIGOT_RESOURCE_ID = 97144;
@@ -86,7 +86,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
*
* @return the {@link RedisManager} implementation
*/
@NotNull
RedisManager getRedisManager();
@@ -98,43 +97,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
@NotNull
DataAdapter getDataAdapter();
/**
* Returns the data serializer for the given {@link Identifier}
*/
@NotNull
<T extends Data> Map<Identifier, Serializer<T>> getSerializers();
/**
* Register a data serializer for the given {@link Identifier}
*
* @param identifier the {@link Identifier}
* @param serializer the {@link Serializer}
*/
default void registerSerializer(@NotNull Identifier identifier,
@NotNull Serializer<? extends Data> serializer) {
if (identifier.isCustom()) {
log(Level.INFO, String.format("Registered custom data type: %s", identifier));
}
getSerializers().put(identifier, (Serializer<Data>) serializer);
}
/**
* Get the {@link Identifier} for the given key
*/
default Optional<Identifier> getIdentifier(@NotNull String key) {
return getSerializers().keySet().stream().filter(identifier -> identifier.toString().equals(key)).findFirst();
}
/**
* Get the set of registered data types
*
* @return the set of registered data types
*/
@NotNull
default Set<Identifier> getRegisteredDataTypes() {
return getSerializers().keySet();
}
/**
* Returns the data syncer implementation
*
@@ -150,6 +112,14 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
*/
void setDataSyncer(@NotNull DataSyncer dataSyncer);
/**
* Get the uniform command provider
*
* @return the command provider
*/
@NotNull
Uniform getUniform();
/**
* Returns a list of available data {@link Migrator}s
*
@@ -159,7 +129,17 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
List<Migrator> getAvailableMigrators();
@NotNull
Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user);
Map<UUID, Map<Identifier, Data>> getPlayerCustomDataStore();
@NotNull
default Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
if (getPlayerCustomDataStore().containsKey(user.getUuid())) {
return getPlayerCustomDataStore().get(user.getUuid());
}
final Map<Identifier, Data> data = new HashMap<>();
getPlayerCustomDataStore().put(user.getUuid(), data);
return data;
}
/**
* Initialize a faucet of the plugin.
@@ -193,14 +173,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
*/
InputStream getResource(@NotNull String name);
/**
* Returns the plugin data folder
*
* @return the plugin data folder as a {@link File}
*/
@NotNull
File getDataFolder();
/**
* Log a message to the console
*
@@ -283,6 +255,14 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
@NotNull
String getPlatformType();
/**
* Returns the server software version
*
* @return the server software version string
*/
@NotNull
String getServerVersion();
/**
* Returns the legacy data converter if it exists
*

View File

@@ -49,7 +49,7 @@ public class GsonAdapter implements DataAdapter {
@Override
@NotNull
public <A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException {
public <A extends Adaptable> A fromBytes(byte[] data, @NotNull Class<A> type) throws AdaptionException {
return this.fromJson(new String(data, StandardCharsets.UTF_8), type);
}

View File

@@ -31,7 +31,6 @@ public class SnappyGsonAdapter extends GsonAdapter {
super(plugin);
}
@NotNull
@Override
public <A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException {
try {
@@ -43,7 +42,7 @@ public class SnappyGsonAdapter extends GsonAdapter {
@NotNull
@Override
public <A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException {
public <A extends Adaptable> A fromBytes(byte[] data, @NotNull Class<A> type) throws AdaptionException {
try {
return super.fromBytes(decompressBytes(data), type);
} catch (IOException e) {

View File

@@ -378,6 +378,17 @@ public class HuskSyncAPI {
plugin.registerSerializer(identifier, serializer);
}
/**
* Get a registered data serializer by its identifier
*
* @param identifier The identifier of the data type to get the serializer for
* @return The serializer for the given identifier, or an empty optional if the serializer isn't registered
* @since 3.5.4
*/
public Optional<Serializer<Data>> getDataSerializer(@NotNull Identifier identifier) {
return plugin.getSerializer(identifier);
}
/**
* Get a {@link DataSnapshot.Unpacked} from a {@link DataSnapshot.Packed}
*
@@ -500,17 +511,19 @@ public class HuskSyncAPI {
*/
static final class NotRegisteredException extends IllegalStateException {
private static final String MESSAGE = """
Could not access the HuskSync API as it has not yet been registered. This could be because:
private static final String REASONS = """
This may be because:
1) HuskSync has failed to enable successfully
2) Your plugin isn't set to load after HuskSync has
(Check if it set as a (soft)depend in plugin.yml or to load: BEFORE in paper-plugin.yml?)
3) You are attempting to access HuskSync on plugin construction/before your plugin has enabled.
4) You have shaded HuskSync into your plugin jar and need to fix your maven/gradle/build script
to only include HuskSync as a dependency and not as a shaded dependency.""";
3) You are attempting to access HuskSync on plugin construction/before your plugin has enabled.""";
NotRegisteredException(@NotNull String reasons) {
super("Could not access the HuskSync API as it has not yet been registered. %s".formatted(reasons));
}
NotRegisteredException() {
super(MESSAGE);
this(REASONS);
}
}

View File

@@ -1,84 +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.command;
import com.google.common.collect.Maps;
import net.william278.husksync.HuskSync;
import net.william278.husksync.user.CommandUser;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
public abstract class Command extends Node {
private final String usage;
private final Map<String, Boolean> additionalPermissions;
protected Command(@NotNull String name, @NotNull List<String> aliases, @NotNull String usage,
@NotNull HuskSync plugin) {
super(name, aliases, plugin);
this.usage = usage;
this.additionalPermissions = Maps.newHashMap();
}
@Override
public final void onExecuted(@NotNull CommandUser executor, @NotNull String[] args) {
if (!executor.hasPermission(getPermission())) {
plugin.getLocales().getLocale("error_no_permission")
.ifPresent(executor::sendMessage);
return;
}
plugin.runAsync(() -> this.execute(executor, args));
}
public abstract void execute(@NotNull CommandUser executor, @NotNull String[] args);
@NotNull
public final String getRawUsage() {
return usage;
}
@NotNull
public final String getUsage() {
return "/" + getName() + " " + getRawUsage();
}
public final void addAdditionalPermissions(@NotNull Map<String, Boolean> permissions) {
permissions.forEach((permission, value) -> this.additionalPermissions.put(getPermission(permission), value));
}
@NotNull
public final Map<String, Boolean> getAdditionalPermissions() {
return additionalPermissions;
}
@NotNull
public String getDescription() {
return plugin.getLocales().getRawLocale(getName() + "_command_description")
.orElse(getUsage());
}
@NotNull
public final HuskSync getPlugin() {
return plugin;
}
}

View File

@@ -37,7 +37,7 @@ import java.util.Optional;
public class EnderChestCommand extends ItemsCommand {
public EnderChestCommand(@NotNull HuskSync plugin) {
super(plugin, List.of("enderchest", "echest", "openechest"));
super("enderchest", List.of("echest", "openechest"), plugin);
}
@Override

View File

@@ -19,6 +19,8 @@
package net.william278.husksync.command;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import de.themoep.minedown.adventure.MineDown;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
@@ -31,33 +33,26 @@ import net.william278.husksync.HuskSync;
import net.william278.husksync.database.Database;
import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.user.OnlineUser;
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 org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.stream.Collectors;
public class HuskSyncCommand extends Command implements TabProvider {
private static final Map<String, Boolean> SUB_COMMANDS = Map.of(
"about", false,
"status", true,
"reload", true,
"migrate", true,
"update", true
);
public class HuskSyncCommand extends PluginCommand {
private final UpdateChecker updateChecker;
private final AboutMenu aboutMenu;
public HuskSyncCommand(@NotNull HuskSync plugin) {
super("husksync", List.of(), "[" + String.join("|", SUB_COMMANDS.keySet()) + "]", plugin);
addAdditionalPermissions(SUB_COMMANDS);
super("husksync", List.of(), Permission.Default.TRUE, ExecutionScope.ALL, plugin);
this.updateChecker = plugin.getUpdateChecker();
this.aboutMenu = AboutMenu.builder()
.title(Component.text("HuskSync"))
@@ -68,7 +63,9 @@ public class HuskSyncCommand extends Command implements TabProvider {
.credits("Contributors",
AboutMenu.Credit.of("HarvelsX").description("Code"),
AboutMenu.Credit.of("HookWoods").description("Code"),
AboutMenu.Credit.of("Preva1l").description("Code"))
AboutMenu.Credit.of("Preva1l").description("Code"),
AboutMenu.Credit.of("hanbings").description("Code (Fabric porting)"),
AboutMenu.Credit.of("Stampede2011").description("Code (Fabric mixins)"))
.credits("Translators",
AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"),
AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"),
@@ -83,7 +80,8 @@ public class HuskSyncCommand extends Command implements TabProvider {
AboutMenu.Credit.of("xF3d3").description("Italian (it-it)"),
AboutMenu.Credit.of("cada3141").description("Korean (ko-kr)"),
AboutMenu.Credit.of("Wirayuda5620").description("Indonesian (id-id)"),
AboutMenu.Credit.of("WinTone01").description("Turkish (tr-tr)"))
AboutMenu.Credit.of("WinTone01").description("Turkish (tr-tr)"),
AboutMenu.Credit.of("IbanEtchep").description("French (fr-fr)"))
.buttons(
AboutMenu.Link.of("https://william278.net/docs/husksync").text("Documentation").icon(""),
AboutMenu.Link.of("https://github.com/WiIIiam278/HuskSync/issues").text("Issues").icon("").color(TextColor.color(0xff9f0f)),
@@ -92,136 +90,137 @@ public class HuskSyncCommand extends Command implements TabProvider {
}
@Override
public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
final String subCommand = parseStringArg(args, 0).orElse("about").toLowerCase(Locale.ENGLISH);
if (SUB_COMMANDS.containsKey(subCommand) && !executor.hasPermission(getPermission(subCommand))) {
plugin.getLocales().getLocale("error_no_permission")
.ifPresent(executor::sendMessage);
return;
}
switch (subCommand) {
case "about" -> executor.sendMessage(aboutMenu.toComponent());
case "status" -> {
getPlugin().getLocales().getLocale("system_status_header").ifPresent(executor::sendMessage);
executor.sendMessage(Component.join(
JoinConfiguration.newlines(),
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList()
));
}
case "reload" -> {
try {
plugin.loadSettings();
plugin.loadLocales();
plugin.loadServer();
plugin.getLocales().getLocale("reload_complete").ifPresent(executor::sendMessage);
} catch (Throwable e) {
executor.sendMessage(new MineDown(
"[Error:](#ff3300) [Failed to reload the plugin. Check console for errors.](#ff7e5e)"
));
plugin.log(Level.SEVERE, "Failed to reload the plugin", e);
}
}
case "migrate" -> {
if (executor instanceof OnlineUser) {
plugin.getLocales().getLocale("error_console_command_only")
.ifPresent(executor::sendMessage);
return;
}
this.handleMigrationCommand(args);
}
case "update" -> updateChecker.check().thenAccept(checked -> {
if (checked.isUpToDate()) {
plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString())
.ifPresent(executor::sendMessage);
return;
}
plugin.getLocales().getLocale("update_available", checked.getLatestVersion().toString(),
plugin.getPluginVersion().toString()).ifPresent(executor::sendMessage);
});
default -> plugin.getLocales().getLocale("error_invalid_syntax", getUsage())
.ifPresent(executor::sendMessage);
}
public void provide(@NotNull BaseCommand<?> command) {
command.setDefaultExecutor((ctx) -> about(command, ctx));
command.addSubCommand("about", (sub) -> sub.setDefaultExecutor((ctx) -> about(command, ctx)));
command.addSubCommand("status", needsOp("status"), status());
command.addSubCommand("reload", needsOp("reload"), reload());
command.addSubCommand("update", needsOp("update"), update());
command.addSubCommand("migrate", migrate());
}
// Handle a migration console command input
private void handleMigrationCommand(@NotNull String[] args) {
if (args.length < 2) {
plugin.log(Level.INFO,
"Please choose a migrator, then run \"husksync migrate <migrator>\"");
this.logMigratorList();
return;
}
private void about(@NotNull BaseCommand<?> c, @NotNull CommandContext<?> ctx) {
user(c, ctx).getAudience().sendMessage(aboutMenu.toComponent());
}
final Optional<Migrator> selectedMigrator = plugin.getAvailableMigrators().stream()
.filter(available -> available.getIdentifier().equalsIgnoreCase(args[1]))
.findFirst();
selectedMigrator.ifPresentOrElse(migrator -> {
if (args.length < 3) {
plugin.log(Level.INFO, migrator.getHelpMenu());
@NotNull
private CommandProvider status() {
return (sub) -> sub.setDefaultExecutor((ctx) -> {
final CommandUser user = user(sub, ctx);
plugin.getLocales().getLocale("system_status_header").ifPresent(user::sendMessage);
user.sendMessage(Component.join(
JoinConfiguration.newlines(),
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList()
));
});
}
@NotNull
private CommandProvider reload() {
return (sub) -> sub.setDefaultExecutor((ctx) -> {
final CommandUser user = user(sub, ctx);
try {
plugin.loadSettings();
plugin.loadLocales();
plugin.loadServer();
plugin.getLocales().getLocale("reload_complete").ifPresent(user::sendMessage);
} catch (Throwable e) {
user.sendMessage(new MineDown(
"[Error:](#ff3300) [Failed to reload the plugin. Check console for errors.](#ff7e5e)"
));
plugin.log(Level.SEVERE, "Failed to reload the plugin", e);
}
});
}
@NotNull
private CommandProvider update() {
return (sub) -> sub.setDefaultExecutor((ctx) -> updateChecker.check().thenAccept(checked -> {
final CommandUser user = user(sub, ctx);
if (checked.isUpToDate()) {
plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString())
.ifPresent(user::sendMessage);
return;
}
switch (args[2]) {
case "start" -> migrator.start().thenAccept(succeeded -> {
plugin.getLocales().getLocale("update_available", checked.getLatestVersion().toString(),
plugin.getPluginVersion().toString()).ifPresent(user::sendMessage);
}));
}
@NotNull
private CommandProvider migrate() {
return (sub) -> {
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
sub.setDefaultExecutor((ctx) -> {
plugin.log(Level.INFO, "Please choose a migrator, then run \"husksync migrate start <migrator>\"");
plugin.log(Level.INFO, String.format(
"List of available migrators:\nMigrator ID / Migrator Name:\n%s",
plugin.getAvailableMigrators().stream()
.map(migrator -> String.format("%s - %s", migrator.getIdentifier(), migrator.getName()))
.collect(Collectors.joining("\n"))
));
});
sub.addSubCommand("help", (help) -> help.addSyntax((cmd) -> {
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
plugin.log(Level.INFO, migrator.getHelpMenu());
}, migrator()));
sub.addSubCommand("start", (start) -> start.addSyntax((cmd) -> {
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
migrator.start().thenAccept(succeeded -> {
if (succeeded) {
plugin.log(Level.INFO, "Migration completed successfully!");
} else {
plugin.log(Level.WARNING, "Migration failed!");
}
});
case "set" -> migrator.handleConfigurationCommand(Arrays.copyOfRange(args, 3, args.length));
default -> plugin.log(Level.INFO, String.format(
"Invalid syntax. Console usage: \"husksync migrate %s <start/set>", args[1]
));
}
}, () -> {
plugin.log(Level.INFO,
"Please specify a valid migrator.\n" +
"If a migrator is not available, please verify that you meet the prerequisites to use it.");
this.logMigratorList();
});
}
// Log the list of available migrators
private void logMigratorList() {
plugin.log(Level.INFO, String.format(
"List of available migrators:\nMigrator ID / Migrator Name:\n%s",
plugin.getAvailableMigrators().stream()
.map(migrator -> String.format("%s - %s", migrator.getIdentifier(), migrator.getName()))
.collect(Collectors.joining("\n"))
));
}
@Nullable
@Override
public List<String> suggest(@NotNull CommandUser user, @NotNull String[] args) {
return switch (args.length) {
case 0, 1 -> SUB_COMMANDS.keySet().stream().sorted().toList();
default -> null;
}, migrator()));
sub.addSubCommand("set", (set) -> set.addSyntax((cmd) -> {
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
final String[] args = cmd.getArgument("args", String.class).split(" ");
migrator.handleConfigurationCommand(args);
}, migrator(), BaseCommand.greedyString("args")));
};
}
@NotNull
private <S> ArgumentElement<S, Migrator> migrator() {
return new ArgumentElement<>("migrator", reader -> {
final String id = reader.readString();
final Migrator migrator = plugin.getAvailableMigrators().stream()
.filter(m -> m.getIdentifier().equalsIgnoreCase(id)).findFirst().orElse(null);
if (migrator == null) {
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
}
return migrator;
}, (context, builder) -> {
for (Migrator material : plugin.getAvailableMigrators()) {
builder.suggest(material.getIdentifier());
}
return builder.buildFuture();
});
}
private enum StatusLine {
PLUGIN_VERSION(plugin -> Component.text("v" + plugin.getPluginVersion().toStringWithoutMetadata())
.appendSpace().append(plugin.getPluginVersion().getMetadata().isBlank() ? Component.empty()
: Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))),
PLATFORM_TYPE(plugin -> Component.text(WordUtils.capitalizeFully(plugin.getPlatformType()))),
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"
)),
SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())),
DATABASE_TYPE(plugin ->
Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() +
(plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ?
(plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : ""))
(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(
@@ -238,13 +237,19 @@ public class HuskSyncCommand extends Command implements TabProvider {
)),
DATA_TYPES(plugin -> Component.join(
JoinConfiguration.commas(true),
plugin.getRegisteredDataTypes().stream().map(i -> {
boolean enabled = plugin.getSettings().getSynchronization().isFeatureEnabled(i);
return Component.textOfChildren(Component
.text(i.toString()).appendSpace().append(Component.text(enabled ? '✔' : '❌')))
.color(enabled ? NamedTextColor.GREEN : NamedTextColor.RED)
.hoverEvent(HoverEvent.showText(Component.text(enabled ? "Enabled" : "Disabled")));
}).toList()
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;
@@ -273,7 +278,7 @@ public class HuskSyncCommand extends Command implements TabProvider {
@NotNull
private static Component getLocalhostBoolean(@NotNull String value) {
return getBoolean(value.equals("127.0.0.1") || value.equals("0.0.0.0")
|| value.equals("localhost") || value.equals("::1"));
|| value.equals("localhost") || value.equals("::1"));
}
}

View File

@@ -37,7 +37,7 @@ import java.util.Optional;
public class InventoryCommand extends ItemsCommand {
public InventoryCommand(@NotNull HuskSync plugin) {
super(plugin, List.of("inventory", "invsee", "openinv"));
super("inventory", List.of("invsee", "openinv"), plugin);
}
@Override
@@ -45,6 +45,7 @@ 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;

View File

@@ -24,47 +24,43 @@ import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
import net.william278.uniform.BaseCommand;
import net.william278.uniform.Permission;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
public abstract class ItemsCommand extends Command implements TabProvider {
public abstract class ItemsCommand extends PluginCommand {
protected ItemsCommand(@NotNull HuskSync plugin, @NotNull List<String> aliases) {
super(aliases.get(0), aliases.subList(1, aliases.size()), "<player> [version_uuid]", plugin);
setOperatorCommand(true);
addAdditionalPermissions(Map.of("edit", true));
protected ItemsCommand(@NotNull String name, @NotNull List<String> aliases, @NotNull HuskSync plugin) {
super(name, aliases, Permission.Default.IF_OP, ExecutionScope.IN_GAME, plugin);
}
@Override
public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
if (!(executor instanceof OnlineUser player)) {
plugin.getLocales().getLocale("error_in_game_command_only")
.ifPresent(executor::sendMessage);
return;
}
// Find the user to view the items for
final Optional<User> optionalUser = parseStringArg(args, 0)
.flatMap(name -> plugin.getDatabase().getUserByName(name));
if (optionalUser.isEmpty()) {
plugin.getLocales().getLocale(
args.length >= 1 ? "error_invalid_player" : "error_invalid_syntax", getUsage()
).ifPresent(player::sendMessage);
return;
}
// Show the user data
final User user = optionalUser.get();
parseUUIDArg(args, 1).ifPresentOrElse(
version -> this.showSnapshotItems(player, user, version),
() -> this.showLatestItems(player, user)
);
public void provide(@NotNull BaseCommand<?> command) {
command.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
final CommandUser executor = user(command, ctx);
if (!(executor instanceof OnlineUser online)) {
plugin.getLocales().getLocale("error_in_game_command_only")
.ifPresent(executor::sendMessage);
return;
}
this.showSnapshotItems(online, user, version);
}, user("username"), uuid("version"));
command.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final CommandUser executor = user(command, ctx);
if (!(executor instanceof OnlineUser online)) {
plugin.getLocales().getLocale("error_in_game_command_only")
.ifPresent(executor::sendMessage);
return;
}
this.showLatestItems(online, user);
}, user("username"));
}
// View (and edit) the latest user data
@@ -114,12 +110,4 @@ public abstract class ItemsCommand extends Command implements TabProvider {
protected abstract void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
@NotNull User user, boolean allowEdit);
@Nullable
@Override
public List<String> suggest(@NotNull CommandUser executor, @NotNull String[] args) {
return switch (args.length) {
case 0, 1 -> plugin.getOnlineUsers().stream().map(User::getUsername).toList();
default -> null;
};
}
}

View File

@@ -1,105 +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.command;
import net.william278.husksync.HuskSync;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.UUID;
public abstract class Node implements Executable {
protected static final String PERMISSION_PREFIX = "husksync.command";
protected final HuskSync plugin;
private final String name;
private final List<String> aliases;
private boolean operatorCommand = false;
protected Node(@NotNull String name, @NotNull List<String> aliases, @NotNull HuskSync plugin) {
if (name.isBlank()) {
throw new IllegalArgumentException("Command name cannot be blank");
}
this.name = name;
this.aliases = aliases;
this.plugin = plugin;
}
@NotNull
public String getName() {
return name;
}
@NotNull
public List<String> getAliases() {
return aliases;
}
@NotNull
public String getPermission(@NotNull String... child) {
final StringJoiner joiner = new StringJoiner(".")
.add(PERMISSION_PREFIX)
.add(getName());
for (final String node : child) {
joiner.add(node);
}
return joiner.toString().trim();
}
public boolean isOperatorCommand() {
return operatorCommand;
}
public void setOperatorCommand(boolean operatorCommand) {
this.operatorCommand = operatorCommand;
}
protected Optional<String> parseStringArg(@NotNull String[] args, int index) {
if (args.length > index) {
return Optional.of(args[index]);
}
return Optional.empty();
}
protected Optional<Integer> parseIntArg(@NotNull String[] args, int index) {
return parseStringArg(args, index).flatMap(arg -> {
try {
return Optional.of(Integer.parseInt(arg));
} catch (NumberFormatException e) {
return Optional.empty();
}
});
}
protected Optional<UUID> parseUUIDArg(@NotNull String[] args, int index) {
return parseStringArg(args, index).flatMap(arg -> {
try {
return Optional.of(UUID.fromString(arg));
} catch (IllegalArgumentException e) {
return Optional.empty();
}
});
}
}

View File

@@ -0,0 +1,129 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.command;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.william278.husksync.HuskSync;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.user.User;
import net.william278.uniform.BaseCommand;
import net.william278.uniform.Command;
import net.william278.uniform.Permission;
import net.william278.uniform.element.ArgumentElement;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.function.Function;
public abstract class PluginCommand extends Command {
protected final HuskSync plugin;
protected PluginCommand(@NotNull String name, @NotNull List<String> aliases, @NotNull Permission.Default defPerm,
@NotNull ExecutionScope scope, @NotNull HuskSync plugin) {
super(name, aliases, getDescription(plugin, name), new Permission(createPermission(name), defPerm), scope);
this.plugin = plugin;
}
private static String getDescription(@NotNull HuskSync plugin, @NotNull String name) {
return plugin.getLocales().getRawLocale("%s_command_description".formatted(name)).orElse("");
}
@NotNull
private static String createPermission(@NotNull String name, @NotNull String... sub) {
return "husksync.command." + name + (sub.length > 0 ? "." + String.join(".", sub) : "");
}
@NotNull
protected String getPermission(@NotNull String... sub) {
return createPermission(this.getName(), sub);
}
@NotNull
@SuppressWarnings("rawtypes")
protected CommandUser user(@NotNull BaseCommand base, @NotNull CommandContext context) {
return adapt(base.getUser(context.getSource()));
}
@NotNull
protected Permission needsOp(@NotNull String... nodes) {
return new Permission(getPermission(nodes), Permission.Default.IF_OP);
}
@NotNull
protected CommandUser adapt(net.william278.uniform.CommandUser user) {
return user.getUuid() == null ? plugin.getConsole() : plugin.getOnlineUser(user.getUuid()).orElseThrow();
}
@NotNull
protected <S> ArgumentElement<S, User> user(@NotNull String name) {
return new ArgumentElement<>(name, reader -> {
final String username = reader.readString();
return plugin.getDatabase().getUserByName(username).orElseThrow(
() -> CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader)
);
}, (context, builder) -> {
plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getUsername()));
return builder.buildFuture();
});
}
@NotNull
protected <S> ArgumentElement<S, UUID> uuid(@NotNull String name) {
return new ArgumentElement<>(name, reader -> {
try {
return UUID.fromString(reader.readString());
} catch (IllegalArgumentException e) {
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
}
}, (context, builder) -> builder.buildFuture());
}
public enum Type {
HUSKSYNC_COMMAND(HuskSyncCommand::new),
USERDATA_COMMAND(UserDataCommand::new),
INVENTORY_COMMAND(InventoryCommand::new),
ENDER_CHEST_COMMAND(EnderChestCommand::new);
public final Function<HuskSync, PluginCommand> commandSupplier;
Type(@NotNull Function<HuskSync, PluginCommand> supplier) {
this.commandSupplier = supplier;
}
@NotNull
public PluginCommand supply(@NotNull HuskSync plugin) {
return commandSupplier.apply(plugin);
}
@NotNull
public static PluginCommand[] create(@NotNull HuskSync plugin) {
return Arrays.stream(values()).map(type -> type.supply(plugin))
.filter(command -> !plugin.getSettings().isCommandDisabled(command))
.toArray(PluginCommand[]::new);
}
}
}

View File

@@ -1,50 +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.command;
import net.william278.husksync.user.CommandUser;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public interface TabProvider {
@Nullable
List<String> suggest(@NotNull CommandUser user, @NotNull String[] args);
@NotNull
default List<String> getSuggestions(@NotNull CommandUser user, @NotNull String[] args) {
List<String> suggestions = suggest(user, args);
if (suggestions == null) {
suggestions = List.of();
}
return filter(suggestions, args);
}
@NotNull
default List<String> filter(@NotNull List<String> suggestions, @NotNull String[] args) {
return suggestions.stream()
.filter(suggestion -> args.length == 0 || suggestion.toLowerCase()
.startsWith(args[args.length - 1].toLowerCase().trim()))
.toList();
}
}

View File

@@ -19,6 +19,7 @@
package net.william278.husksync.command;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.redis.RedisKeyType;
@@ -28,83 +29,32 @@ 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.uniform.BaseCommand;
import net.william278.uniform.CommandProvider;
import net.william278.uniform.Permission;
import net.william278.uniform.element.ArgumentElement;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Level;
public class UserDataCommand extends Command implements TabProvider {
private static final Map<String, Boolean> SUB_COMMANDS = Map.of(
"view", false,
"list", false,
"delete", true,
"restore", true,
"pin", true,
"dump", true
);
public class UserDataCommand extends PluginCommand {
public UserDataCommand(@NotNull HuskSync plugin) {
super("userdata", List.of("playerdata"), String.format(
"<%s> [username] [version_uuid]", String.join("/", SUB_COMMANDS.keySet())
), plugin);
setOperatorCommand(true);
addAdditionalPermissions(SUB_COMMANDS);
super("userdata", List.of("playerdata"), Permission.Default.IF_OP, ExecutionScope.ALL, plugin);
}
@Override
public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
final String subCommand = parseStringArg(args, 0).orElse("view").toLowerCase(Locale.ENGLISH);
final Optional<User> optionalUser = parseStringArg(args, 1)
.flatMap(name -> plugin.getDatabase().getUserByName(name))
.or(() -> parseStringArg(args, 0).flatMap(name -> plugin.getDatabase().getUserByName(name)))
.or(() -> args.length < 2 && executor instanceof User userExecutor
? Optional.of(userExecutor) : Optional.empty());
final Optional<UUID> uuid = parseUUIDArg(args, 2).or(() -> parseUUIDArg(args, 1));
if (optionalUser.isEmpty()) {
plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(executor::sendMessage);
return;
}
final User user = optionalUser.get();
switch (subCommand) {
case "view" -> uuid.ifPresentOrElse(
version -> viewSnapshot(executor, user, version),
() -> viewLatestSnapshot(executor, user)
);
case "list" -> listSnapshots(
executor, user, parseIntArg(args, 2).or(() -> parseIntArg(args, 1)).orElse(1)
);
case "delete" -> uuid.ifPresentOrElse(
version -> deleteSnapshot(executor, user, version),
() -> plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata delete <username> <version_uuid>")
.ifPresent(executor::sendMessage)
);
case "restore" -> uuid.ifPresentOrElse(
version -> restoreSnapshot(executor, user, version),
() -> plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata restore <username> <version_uuid>")
.ifPresent(executor::sendMessage)
);
case "pin" -> uuid.ifPresentOrElse(
version -> pinSnapshot(executor, user, version),
() -> plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata pin <username> <version_uuid>")
.ifPresent(executor::sendMessage)
);
case "dump" -> uuid.ifPresentOrElse(
version -> dumpSnapshot(executor, user, version, parseStringArg(args, 3)
.map(arg -> arg.equalsIgnoreCase("web")).orElse(false)),
() -> plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata dump <web/file> <username> <version_uuid>")
.ifPresent(executor::sendMessage)
);
default -> plugin.getLocales().getLocale("error_invalid_syntax", getUsage())
.ifPresent(executor::sendMessage);
}
public void provide(@NotNull BaseCommand<?> command) {
command.addSubCommand("view", needsOp("view"), view());
command.addSubCommand("list", needsOp("list"), list());
command.addSubCommand("delete", needsOp("delete"), delete());
command.addSubCommand("restore", needsOp("restore"), restore());
command.addSubCommand("pin", needsOp("pin"), pin());
command.addSubCommand("dump", needsOp("dump"), dump());
}
// Show the latest snapshot
@@ -224,7 +174,8 @@ public class UserDataCommand extends Command implements TabProvider {
}
// Dump a snapshot
private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version, boolean webDump) {
private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version,
@NotNull DumpType type) {
final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, version);
if (data.isEmpty()) {
plugin.getLocales().getLocale("error_invalid_version_uuid")
@@ -237,22 +188,99 @@ public class UserDataCommand extends Command implements TabProvider {
final DataDumper dumper = DataDumper.create(userData, user, plugin);
try {
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getUsername(),
(webDump ? dumper.toWeb() : dumper.toFile())).ifPresent(executor::sendMessage);
(type == DumpType.WEB ? dumper.toWeb() : dumper.toFile()))
.ifPresent(executor::sendMessage);
} catch (Throwable e) {
plugin.log(Level.SEVERE, "Failed to dump user data", e);
}
}
@Nullable
@Override
public List<String> suggest(@NotNull CommandUser executor, @NotNull String[] args) {
return switch (args.length) {
case 0, 1 -> SUB_COMMANDS.keySet().stream().sorted().toList();
case 2 -> plugin.getOnlineUsers().stream().map(User::getUsername).toList();
case 4 -> parseStringArg(args, 0)
.map(arg -> arg.equalsIgnoreCase("dump") ? List.of("web", "file") : null)
.orElse(null);
default -> null;
@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"));
};
}
@NotNull
private CommandProvider list() {
return (sub) -> {
sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
listSnapshots(user(sub, ctx), user, 1);
}, user("username"));
sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final int page = ctx.getArgument("page", Integer.class);
listSnapshots(user(sub, ctx), user, page);
}, user("username"), BaseCommand.intNum("page", 1));
};
}
@NotNull
private CommandProvider delete() {
return (sub) -> sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
deleteSnapshot(user(sub, ctx), user, version);
}, user("username"), uuid("version"));
}
@NotNull
private CommandProvider restore() {
return (sub) -> sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
restoreSnapshot(user(sub, ctx), user, version);
}, user("username"), uuid("version"));
}
@NotNull
private CommandProvider pin() {
return (sub) -> sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
pinSnapshot(user(sub, ctx), user, version);
}, user("username"), uuid("version"));
}
@NotNull
private CommandProvider dump() {
return (sub) -> 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());
}
private <S> ArgumentElement<S, DumpType> dumpType() {
return new ArgumentElement<>("type", reader -> {
final String type = reader.readString();
return switch (type.toLowerCase(Locale.ENGLISH)) {
case "web" -> DumpType.WEB;
case "file" -> DumpType.FILE;
default -> throw CommandSyntaxException.BUILT_IN_EXCEPTIONS
.dispatcherUnknownArgument().createWithContext(reader);
};
}, (context, builder) -> {
builder.suggest("web");
builder.suggest("file");
return builder.buildFuture();
});
}
enum DumpType {
WEB,
FILE
}
}

View File

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

View File

@@ -25,6 +25,7 @@ import de.exlll.configlib.Configuration;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.william278.husksync.command.PluginCommand;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.data.Identifier;
import net.william278.husksync.database.Database;
@@ -63,21 +64,21 @@ public class Settings {
private boolean checkForUpdates = true;
@Comment("Specify a common ID for grouping servers running HuskSync. "
+ "Don't modify this unless you know what you're doing!")
+ "Don't modify this unless you know what you're doing!")
private String clusterId = "";
@Comment("Enable development debug logging")
private boolean debugLogging = false;
@Comment("Whether to provide modern, rich TAB suggestions for commands (if available)")
private boolean brigadierTabCompletion = false;
@Comment({"Whether to enable the Player Analytics hook.", "Docs: https://william278.net/docs/husksync/plan-hook"})
private boolean enablePlanHook = true;
@Comment("Whether to cancel game event packets directly when handling locked players if ProtocolLib is installed")
@Comment("Whether to cancel game event packets directly when handling locked players if ProtocolLib or PacketEvents is installed")
private boolean cancelPackets = true;
@Comment("Add HuskSync commands to this list to prevent them from being registered (e.g. ['userdata'])")
@Getter(AccessLevel.NONE)
private List<String> disabledCommands = Lists.newArrayList();
// Database settings
@Comment("Database settings")
@@ -185,7 +186,7 @@ public class Settings {
}
// Synchronization settings
@Comment("Redis settings")
@Comment("Data syncing settings")
private SynchronizationSettings synchronization = new SynchronizationSettings();
@Getter
@@ -228,7 +229,7 @@ public class Settings {
private boolean enabled = false;
@Comment("What items to save in death snapshots? (DROPS or ITEMS_TO_KEEP). "
+ "Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server.")
+ "Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server.")
private DeathItemsMode itemsToSave = DeathItemsMode.DROPS;
@Comment("Should a death snapshot still be created even if the items to save on the player's death are empty?")
@@ -249,14 +250,14 @@ public class Settings {
@Comment("Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing")
private boolean compressData = true;
@Comment("Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)")
@Comment("Where to display sync notifications (ACTION_BAR, CHAT or NONE)")
private Locales.NotificationSlot notificationDisplaySlot = Locales.NotificationSlot.ACTION_BAR;
@Comment("Persist maps locked in a Cartography Table to let them be viewed on any server")
private boolean persistLockedMaps = true;
@Comment("If using the DELAY sync method, how long should this server listen for Redis key data updates before "
+ "pulling data from the database instead (i.e., if the user did not change servers).")
+ "pulling data from the database instead (i.e., if the user did not change servers).")
private int networkLatencyMilliseconds = 500;
@Comment({"Which data types to synchronize.", "Docs: https://william278.net/docs/husksync/sync-features"})
@@ -266,10 +267,45 @@ public class Settings {
@Comment("Commands which should be blocked before a player has finished syncing (Use * to block all commands)")
private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*"));
@Comment({"For attribute syncing, which attributes should be ignored/skipped when syncing",
"(e.g. ['minecraft:generic.max_health', 'minecraft:generic.attack_damage'])"})
@Getter(AccessLevel.NONE)
private List<String> ignoredAttributes = new ArrayList<>(List.of(""));
@Comment("Configuration for how to sync attributes")
private AttributeSettings attributes = new AttributeSettings();
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class AttributeSettings {
@Comment({"Which attributes should not be saved when syncing users. Supports wildcard matching.",
"(e.g. ['minecraft:generic.max_health', 'minecraft:generic.*'])"})
@Getter(AccessLevel.NONE)
private List<String> ignoredAttributes = new ArrayList<>(List.of(""));
@Comment({"Which modifiers should not be saved when syncing users. Supports wildcard matching.",
"(e.g. ['minecraft:effect.speed', 'minecraft:effect.*'])"})
@Getter(AccessLevel.NONE)
private List<String> ignoredModifiers = new ArrayList<>(List.of(
"minecraft:effect.*", "minecraft:creative_mode_*"
));
private boolean matchesWildcard(@NotNull String pat, @NotNull String value) {
if (!pat.contains(":")) {
pat = "minecraft:%s".formatted(pat);
}
if (!value.contains(":")) {
value = "minecraft:%s".formatted(value);
}
return pat.contains("*") ? value.matches(pat.replace("*", ".*")) : pat.equals(value);
}
public boolean isIgnoredAttribute(@NotNull String attribute) {
return ignoredAttributes.stream().anyMatch(wildcard -> matchesWildcard(wildcard, attribute));
}
public boolean isIgnoredModifier(@NotNull String modifier) {
return ignoredModifiers.stream().anyMatch(wildcard -> matchesWildcard(wildcard, modifier));
}
}
@Comment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts")
@Getter(AccessLevel.NONE)
@@ -283,10 +319,6 @@ public class Settings {
return id.isCustom() || features.getOrDefault(id.getKeyValue(), id.isEnabledByDefault());
}
public boolean isIgnoredAttribute(@NotNull String attribute) {
return ignoredAttributes.contains(attribute);
}
@NotNull
public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) {
try {
@@ -297,4 +329,10 @@ public class Settings {
}
}
public boolean isCommandDisabled(@NotNull PluginCommand command) {
return disabledCommands.stream().map(c -> c.startsWith("/") ? c.substring(1) : c)
.anyMatch(c -> c.equalsIgnoreCase(command.getName()) || command.getAliases().contains(c));
}
}

View File

@@ -21,6 +21,11 @@ package net.william278.husksync.data;
import com.google.common.collect.Sets;
import com.google.gson.annotations.SerializedName;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import net.kyori.adventure.key.Key;
import net.william278.husksync.HuskSync;
import net.william278.husksync.user.OnlineUser;
@@ -78,6 +83,7 @@ public interface Data {
*/
interface Inventory extends Items {
int INVENTORY_SLOT_COUNT = 41;
String ITEMS_TAG = "items";
String HELD_ITEM_SLOT_TAG = "held_item_slot";
@@ -110,7 +116,7 @@ public interface Data {
* Data container holding data for ender chests
*/
interface EnderChest extends Items {
int ENDER_CHEST_SLOT_COUNT = 27;
}
}
@@ -149,14 +155,14 @@ public interface Data {
*/
interface Advancements extends Data {
String RECIPE_ADVANCEMENT = "minecraft:recipe";
@NotNull
List<Advancement> getCompleted();
@NotNull
default List<Advancement> getCompletedExcludingRecipes() {
return getCompleted().stream()
.filter(advancement -> !advancement.getKey().startsWith("minecraft:recipe"))
.collect(Collectors.toList());
return getCompleted().stream().filter(adv -> !adv.getKey().startsWith(RECIPE_ADVANCEMENT)).toList();
}
void setCompleted(@NotNull List<Advancement> completed);
@@ -333,17 +339,40 @@ public interface Data {
}
record Modifier(
@NotNull UUID uuid,
@NotNull String name,
double amount,
@SerializedName("operation") int operationType,
@SerializedName("equipment_slot") int equipmentSlot
) {
@Getter
@Accessors(fluent = true)
@AllArgsConstructor
@NoArgsConstructor
final class Modifier {
@Getter(AccessLevel.NONE)
@Nullable
@SerializedName("uuid")
private UUID uuid;
@SerializedName("name")
private String name;
@SerializedName("amount")
private double amount;
@SerializedName("operation")
private int operationType;
@SerializedName("equipment_slot")
private int equipmentSlot;
public Modifier(@NotNull String name, double amount, int operationType, int equipmentSlot) {
this.name = name;
this.amount = amount;
this.operationType = operationType;
this.equipmentSlot = equipmentSlot;
}
@Override
public boolean equals(Object obj) {
return obj instanceof Modifier modifier && modifier.uuid.equals(uuid);
if (obj instanceof Modifier other) {
if (uuid == null || other.uuid == null) {
return name.equals(other.name);
}
return uuid.equals(other.uuid);
}
return super.equals(obj);
}
public double modify(double value) {
@@ -354,6 +383,16 @@ public interface Data {
default -> value;
};
}
public boolean hasUuid() {
return uuid != null;
}
@NotNull
public UUID uuid() {
return uuid != null ? uuid : UUID.nameUUIDFromBytes(name.getBytes());
}
}
default Optional<Attribute> getAttribute(@NotNull Key key) {

View File

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

View File

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

View File

@@ -370,7 +370,7 @@ public class DataSnapshot {
public static class Unpacked extends DataSnapshot implements DataHolder {
@Expose(serialize = false, deserialize = false)
private final Map<Identifier, Data> deserialized;
private final TreeMap<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 Map<Identifier, Data> data,
@NotNull String saveCause, @NotNull String serverName, @NotNull TreeMap<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,25 +389,25 @@ public class DataSnapshot {
@NotNull
@ApiStatus.Internal
private Map<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
private TreeMap<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
return data.entrySet().stream()
.map((entry) -> plugin.getIdentifier(entry.getKey()).map(id -> Map.entry(
id, plugin.getSerializers().get(id).deserialize(entry.getValue())
)).orElse(null))
.filter(Objects::nonNull)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
.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)
));
}
@NotNull
@ApiStatus.Internal
private Map<String, String> serializeData(@NotNull HuskSync plugin) {
return deserialized.entrySet().stream()
.map((entry) -> Map.entry(entry.getKey().toString(),
Objects.requireNonNull(
plugin.getSerializers().get(entry.getKey()),
String.format("No serializer found for %s", entry.getKey())
).serialize(entry.getValue())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
.collect(Collectors.toMap(
entry -> entry.getKey().toString(),
entry -> plugin.serializeData(entry.getKey(), entry.getValue())
));
}
/**
@@ -453,12 +453,12 @@ public class DataSnapshot {
private String serverName;
private boolean pinned;
private OffsetDateTime timestamp;
private final Map<Identifier, Data> data;
private final TreeMap<Identifier, Data> data;
private Builder(@NotNull HuskSync plugin) {
this.plugin = plugin;
this.pinned = false;
this.data = Maps.newHashMap();
this.data = Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR);
this.timestamp = OffsetDateTime.now();
this.id = UUID.randomUUID();
this.serverName = plugin.getServerName();
@@ -535,9 +535,9 @@ public class DataSnapshot {
public Builder timestamp(@NotNull OffsetDateTime timestamp) {
if (timestamp.isAfter(OffsetDateTime.now())) {
throw new IllegalArgumentException("Data snapshots cannot have a timestamp set in the future! "
+ "Make sure your database server time matches the server time.\n"
+ "Current game server timestamp: " + OffsetDateTime.now() + " / "
+ "Snapshot timestamp: " + timestamp);
+ "Make sure your database server time matches the server time.\n"
+ "Current game server timestamp: " + OffsetDateTime.now() + " / "
+ "Snapshot timestamp: " + timestamp);
}
this.timestamp = timestamp;
return this;

View File

@@ -19,40 +19,83 @@
package net.william278.husksync.data;
import lombok.*;
import net.kyori.adventure.key.InvalidKeyException;
import net.kyori.adventure.key.Key;
import org.intellij.lang.annotations.Subst;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
/**
* Identifiers of different types of {@link Data}s
*/
@Getter
public class Identifier {
public static Identifier INVENTORY = huskSync("inventory", true);
public static Identifier ENDER_CHEST = huskSync("ender_chest", true);
public static Identifier POTION_EFFECTS = huskSync("potion_effects", true);
public static Identifier ADVANCEMENTS = huskSync("advancements", true);
public static Identifier LOCATION = huskSync("location", false);
public static Identifier STATISTICS = huskSync("statistics", true);
public static Identifier HEALTH = huskSync("health", true);
public static Identifier HUNGER = huskSync("hunger", true);
public static Identifier ATTRIBUTES = huskSync("attributes", true);
public static Identifier EXPERIENCE = huskSync("experience", true);
public static Identifier GAME_MODE = huskSync("game_mode", true);
public static Identifier FLIGHT_STATUS = huskSync("flight_status", true);
public static Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
// Built-in identifiers
public static final Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
public static final Identifier INVENTORY = huskSync("inventory", true);
public static final Identifier ENDER_CHEST = huskSync("ender_chest", true);
public static final Identifier ADVANCEMENTS = huskSync("advancements", true);
public static final Identifier STATISTICS = huskSync("statistics", true);
public static final Identifier POTION_EFFECTS = huskSync("potion_effects", true);
public static final Identifier GAME_MODE = huskSync("game_mode", true);
public static final Identifier FLIGHT_STATUS = huskSync("flight_status", true,
Dependency.optional("game_mode")
);
public static final Identifier ATTRIBUTES = huskSync("attributes", true,
Dependency.optional("inventory"),
Dependency.optional("potion_effects")
);
public static final Identifier HEALTH = huskSync("health", true,
Dependency.optional("attributes")
);
public static final Identifier HUNGER = huskSync("hunger", true,
Dependency.optional("attributes")
);
public static final Identifier EXPERIENCE = huskSync("experience", true,
Dependency.optional("advancements")
);
public static final Identifier LOCATION = huskSync("location", false,
Dependency.optional("flight_status"),
Dependency.optional("potion_effects")
);
private final Key key;
private final boolean configDefault;
private final boolean enabledByDefault;
@Getter
private final Set<Dependency> dependencies;
@Setter
@Getter
public boolean enabled;
private Identifier(@NotNull Key key, boolean configDefault) {
private Identifier(@NotNull Key key, boolean enabledByDefault, @NotNull Set<Dependency> dependencies) {
this.key = key;
this.configDefault = configDefault;
this.enabledByDefault = enabledByDefault;
this.enabled = enabledByDefault;
this.dependencies = dependencies;
}
/**
* Create an identifier from a {@link Key}
*
* @param key the key
* @param dependencies the dependencies
* @return the identifier
* @since 3.5.4
*/
@NotNull
public static Identifier from(@NotNull Key key, @NotNull Set<Dependency> dependencies) {
if (key.namespace().equals("husksync")) {
throw new IllegalArgumentException("You cannot register a key with \"husksync\" as the namespace!");
}
return new Identifier(key, true, dependencies);
}
/**
@@ -64,10 +107,7 @@ public class Identifier {
*/
@NotNull
public static Identifier from(@NotNull Key key) {
if (key.namespace().equals("husksync")) {
throw new IllegalArgumentException("You cannot register a key with \"husksync\" as the namespace!");
}
return new Identifier(key, true);
return from(key, Collections.emptySet());
}
/**
@@ -83,25 +123,34 @@ public class Identifier {
return from(Key.key(plugin, name));
}
/**
* Create an identifier from a namespace, value, and dependencies
*
* @param plugin the namespace
* @param name the value
* @param dependencies the dependencies
* @return the identifier
* @since 3.5.4
*/
@NotNull
public static Identifier from(@Subst("plugin") @NotNull String plugin, @Subst("null") @NotNull String name,
@NotNull Set<Dependency> dependencies) {
return from(Key.key(plugin, name), dependencies);
}
// Return an identifier with a HuskSync namespace
@NotNull
private static Identifier huskSync(@Subst("null") @NotNull String name,
boolean configDefault) throws InvalidKeyException {
return new Identifier(Key.key("husksync", name), configDefault);
return new Identifier(Key.key("husksync", name), configDefault, Collections.emptySet());
}
// Return an identifier with a HuskSync namespace
@NotNull
@SuppressWarnings("unused")
private static Identifier parse(@NotNull String key) throws InvalidKeyException {
return huskSync(key, true);
}
public boolean isEnabledByDefault() {
return configDefault;
}
@NotNull
private Map.Entry<String, Boolean> getConfigEntry() {
return Map.entry(getKeyValue(), configDefault);
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));
}
/**
@@ -122,6 +171,17 @@ public class Identifier {
.toArray(Map.Entry[]::new));
}
/**
* Returns {@code true} if the identifier depends on the given identifier
*
* @param identifier the identifier to check
* @return {@code true} if the identifier depends on the given identifier
* @since 3.5.4
*/
public boolean dependsOn(@NotNull Identifier identifier) {
return dependencies.contains(Dependency.required(identifier.key));
}
/**
* Get the namespace of the identifier
*
@@ -176,4 +236,85 @@ public class Identifier {
return false;
}
// Get the config entry for the identifier
@NotNull
private Map.Entry<String, Boolean> getConfigEntry() {
return Map.entry(getKeyValue(), enabledByDefault);
}
/**
* Compares two identifiers based on their dependencies.
* <p>
* If this identifier contains a dependency on the other, it should come after & vice versa
*
* @since 3.5.4
*/
@NoArgsConstructor(access = AccessLevel.PACKAGE)
static class DependencyOrderComparator implements Comparator<Identifier> {
@Override
public int compare(@NotNull Identifier i1, @NotNull Identifier i2) {
if (i1.equals(i2)) {
return 0;
}
if (i1.dependsOn(i2)) {
if (i2.dependsOn(i1)) {
throw new IllegalArgumentException(
"Found circular dependency between %s and %s".formatted(i1.getKey(), i2.getKey())
);
}
return 1;
}
return -1;
}
}
/**
* Represents a data dependency of an identifier, used to determine the order in which data is applied to users
*
* @since 3.5.4
*/
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class Dependency {
/**
* Key of the data dependency see {@code Identifier#key()}
*/
private Key key;
/**
* Whether the data dependency is required to be present & enabled for the dependant data to enabled
*/
private boolean required;
@NotNull
protected static Dependency required(@NotNull Key identifier) {
return new Dependency(identifier, true);
}
@NotNull
public static Dependency optional(@NotNull Key identifier) {
return new Dependency(identifier, false);
}
@NotNull
@SuppressWarnings("SameParameterValue")
private static Dependency required(@Subst("null") @NotNull String identifier) {
return required(Key.key("husksync", identifier));
}
@NotNull
private static Dependency optional(@Subst("null") @NotNull String identifier) {
return optional(Key.key("husksync", identifier));
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Dependency other) {
return key.equals(other.key);
}
return false;
}
}
}

View File

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

View File

@@ -0,0 +1,174 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.data;
import net.william278.desertwell.util.Version;
import net.william278.husksync.HuskSync;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.logging.Level;
public interface SerializerRegistry {
// Comparator for ordering identifiers based on dependency
@NotNull
@ApiStatus.Internal
Comparator<Identifier> DEPENDENCY_ORDER_COMPARATOR = new Identifier.DependencyOrderComparator();
/**
* Returns the data serializer for the given {@link Identifier}
*
* @since 3.0
*/
@NotNull
<T extends Data> TreeMap<Identifier, Serializer<T>> getSerializers();
/**
* Register a data serializer for the given {@link Identifier}
*
* @param id the {@link Identifier}
* @param serializer the {@link Serializer}
* @since 3.0
*/
@SuppressWarnings("unchecked")
default void registerSerializer(@NotNull Identifier id, @NotNull Serializer<? extends Data> serializer) {
if (id.isCustom()) {
getPlugin().log(Level.INFO, "Registered custom data type: %s".formatted(id));
}
id.setEnabled(id.isCustom() || getPlugin().getSettings().getSynchronization().isFeatureEnabled(id));
getSerializers().put(id, (Serializer<Data>) serializer);
}
/**
* Ensure dependencies for identifiers that have required dependencies are met
* <p>
* This checks the dependencies of all registered identifiers and throws an {@link IllegalStateException}
* if a dependency has not been registered or enabled via the config
*
* @since 3.5.4
*/
default void validateDependencies() throws IllegalStateException {
getSerializers().keySet().stream().filter(Identifier::isEnabled)
.forEach(identifier -> {
final List<String> unmet = identifier.getDependencies().stream()
.filter(Identifier.Dependency::isRequired)
.filter(dep -> !isDataTypeAvailable(dep.getKey().asString()))
.map(dep -> dep.getKey().asString()).toList();
if (!unmet.isEmpty()) {
identifier.setEnabled(false);
getPlugin().log(Level.WARNING, "Disabled %s syncing as the following types need to be on: %s"
.formatted(identifier, String.join(", ", unmet)));
}
});
}
/**
* Get the {@link Identifier} for the given key
*
* @since 3.0
*/
default Optional<Identifier> getIdentifier(@NotNull String key) {
return getSerializers().keySet().stream()
.filter(id -> id.getKey().asString().equals(key)).findFirst();
}
/**
* Get a data serializer for the given {@link Identifier}
*
* @param identifier the {@link Identifier} to get the serializer for
* @return the {@link Serializer} for the given {@link Identifier}
* @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();
}
/**
* Serialize data for the given {@link Identifier}
*
* @param identifier the {@link Identifier} to serialize data for
* @param data the data to serialize
* @return the serialized data
* @throws IllegalArgumentException if no serializer is found for the given {@link Identifier}
* @since 3.5.4
*/
@NotNull
default String serializeData(@NotNull Identifier identifier, @NotNull Data data) throws IllegalStateException {
return getSerializer(identifier).map(serializer -> serializer.serialize(data))
.orElseThrow(() -> new IllegalStateException("No serializer found for %s".formatted(identifier)));
}
/**
* Deserialize data of a given {@link Version Minecraft version} for the given {@link Identifier data identifier}
*
* @param identifier the {@link Identifier} to deserialize data for
* @param data the data to deserialize
* @param dataMcVersion the Minecraft version of the data
* @return the deserialized data
* @throws IllegalStateException if no serializer is found for the given {@link Identifier}
* @since 3.6.4
*/
@NotNull
default Data deserializeData(@NotNull Identifier identifier, @NotNull String data,
@NotNull Version dataMcVersion) throws IllegalStateException {
return getSerializer(identifier).map(serializer -> serializer.deserialize(data, dataMcVersion)).orElseThrow(
() -> new IllegalStateException("No serializer found for %s".formatted(identifier))
);
}
/**
* Deserialize data for the given {@link Identifier data identifier}
*
* @param identifier the {@link Identifier} to deserialize data for
* @param data the data to deserialize
* @return the deserialized data
* @since 3.5.4
* @deprecated Use {@link #deserializeData(Identifier, String, Version)} instead
*/
@NotNull
@Deprecated(since = "3.6.5")
default Data deserializeData(@NotNull Identifier identifier, @NotNull String data) {
return deserializeData(identifier, data, getPlugin().getMinecraftVersion());
}
/**
* Get the set of registered data types
*
* @return the set of registered data types
* @since 3.0
*/
@NotNull
default Set<Identifier> getRegisteredDataTypes() {
return getSerializers().keySet();
}
// Returns if a data type is available and enabled in the config
private boolean isDataTypeAvailable(@NotNull String key) {
return getIdentifier(key).map(Identifier::isEnabled).orElse(false);
}
@NotNull
HuskSync getPlugin();
}

View File

@@ -34,7 +34,7 @@ import java.util.logging.Level;
public interface UserDataHolder extends DataHolder {
/**
* Get the data that is enabled for syncing in the config
* Get the data enabled for syncing in the config
*
* @return the data that is enabled for syncing
* @since 3.0
@@ -43,7 +43,7 @@ public interface UserDataHolder extends DataHolder {
@NotNull
default Map<Identifier, Data> getData() {
return getPlugin().getRegisteredDataTypes().stream()
.filter(type -> type.isCustom() || getPlugin().getSettings().getSynchronization().isFeatureEnabled(type))
.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);
@@ -79,7 +79,8 @@ public interface UserDataHolder extends DataHolder {
* Deserialize and apply a data snapshot to this data owner
* <p>
* This method will deserialize the data on the current thread, then synchronously apply it on
* the main server thread.
* the main server thread. The order data will be applied is determined based on the dependencies of
* each data type (see {@link Identifier.Dependency}).
* </p>
* The {@code runAfter} callback function will be run after the snapshot has been applied.
*
@@ -106,12 +107,15 @@ public interface UserDataHolder extends DataHolder {
try {
for (Map.Entry<Identifier, Data> entry : unpacked.getData().entrySet()) {
final Identifier identifier = entry.getKey();
if (plugin.getSettings().getSynchronization().isFeatureEnabled(identifier)) {
if (identifier.isCustom()) {
getCustomDataStore().put(identifier, entry.getValue());
}
entry.getValue().apply(this, plugin);
if (!identifier.isEnabled()) {
continue;
}
// Apply the identified data
if (identifier.isCustom()) {
getCustomDataStore().put(identifier, entry.getValue());
}
entry.getValue().apply(this, plugin);
}
} catch (Throwable e) {
plugin.log(Level.SEVERE, String.format("Failed to apply data snapshot to %s", getUsername()), e);

View File

@@ -50,6 +50,7 @@ public class MongoDbDatabase extends Database {
private final String usersTable;
private final String userDataTable;
public MongoDbDatabase(@NotNull HuskSync plugin) {
super(plugin);
this.usersTable = plugin.getSettings().getDatabase().getTableName(TableName.USERS);
@@ -76,7 +77,7 @@ public class MongoDbDatabase extends Database {
}
} catch (Exception e) {
throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " +
"Please check the supplied database credentials in the config file", e);
"Please check the supplied database credentials in the config file", e);
}
}
@@ -107,13 +108,13 @@ public class MongoDbDatabase extends Database {
if (!existingUser.getUsername().equals(user.getUsername())) {
// Update a user's name if it has changed in the database
try {
Document filter = new Document("uuid", existingUser.getUuid().toString());
Document filter = new Document("uuid", existingUser.getUuid());
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
if (doc == null) {
throw new MongoException("User document returned null!");
}
Bson updates = Updates.set("uuid", user.getUuid().toString());
Bson updates = Updates.set("username", user.getUsername());
mongoCollectionHelper.updateDocument(usersTable, doc, updates);
} catch (MongoException e) {
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
@@ -123,7 +124,7 @@ public class MongoDbDatabase extends Database {
() -> {
// Insert new player data into the database
try {
Document doc = new Document("uuid", user.getUuid().toString()).append("username", user.getUsername());
Document doc = new Document("uuid", user.getUuid()).append("username", user.getUsername());
mongoCollectionHelper.insertDocument(usersTable, doc);
} catch (MongoException e) {
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
@@ -148,8 +149,7 @@ public class MongoDbDatabase extends Database {
Document filter = new Document("uuid", uuid);
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
if (doc != null) {
return Optional.of(new User(UUID.fromString(doc.getString("uuid")),
doc.getString("username")));
return Optional.of(new User(uuid, doc.getString("username")));
}
return Optional.empty();
} catch (MongoException e) {
@@ -171,7 +171,7 @@ public class MongoDbDatabase extends Database {
Document filter = new Document("username", username);
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
if (doc != null) {
return Optional.of(new User(UUID.fromString(doc.getString("uuid")),
return Optional.of(new User(doc.get("uuid", UUID.class),
doc.getString("username")));
}
return Optional.empty();
@@ -191,12 +191,12 @@ public class MongoDbDatabase extends Database {
@Override
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
try {
Document filter = new Document("player_uuid", user.getUuid().toString());
Document filter = new Document("player_uuid", user.getUuid());
Document sort = new Document("timestamp", -1); // -1 = Descending
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
Document doc = iterable.first();
if (doc != null) {
final UUID versionUuid = UUID.fromString(doc.getString("version_uuid"));
final UUID versionUuid = doc.get("version_uuid", UUID.class);
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
final Binary bin = doc.get("data", Binary.class);
final byte[] dataByteArray = bin.getData();
@@ -221,11 +221,11 @@ public class MongoDbDatabase extends Database {
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
try {
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
Document filter = new Document("player_uuid", user.getUuid().toString());
Document filter = new Document("player_uuid", user.getUuid());
Document sort = new Document("timestamp", -1); // -1 = Descending
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
for (Document doc : iterable) {
final UUID versionUuid = UUID.fromString(doc.getString("version_uuid"));
final UUID versionUuid = doc.get("version_uuid", UUID.class);
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
final Binary bin = doc.get("data", Binary.class);
final byte[] dataByteArray = bin.getData();
@@ -249,7 +249,7 @@ public class MongoDbDatabase extends Database {
@Override
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
try {
Document filter = new Document("player_uuid", user.getUuid().toString()).append("version_uuid", versionUuid.toString());
Document filter = new Document("player_uuid", user.getUuid()).append("version_uuid", versionUuid);
Document sort = new Document("timestamp", -1); // -1 = Descending
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
Document doc = iterable.first();
@@ -281,7 +281,7 @@ public class MongoDbDatabase extends Database {
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
if (unpinnedUserData.size() > maxSnapshots) {
Document filter = new Document("player_uuid", user.getUuid().toString()).append("pinned", false);
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)
@@ -307,7 +307,7 @@ public class MongoDbDatabase extends Database {
@Override
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
try {
Document filter = new Document("player_uuid", user.getUuid().toString()).append("version_uuid", versionUuid.toString());
Document filter = new Document("player_uuid", user.getUuid()).append("version_uuid", versionUuid);
Document doc = mongoCollectionHelper.getCollection(userDataTable).find(filter).first();
if (doc == null) {
return false;
@@ -332,7 +332,7 @@ public class MongoDbDatabase extends Database {
@Override
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
try {
Document filter = new Document("player_uuid", user.getUuid().toString()).append("pinned", false);
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)
@@ -362,8 +362,8 @@ public class MongoDbDatabase extends Database {
@Override
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
try {
Document doc = new Document("player_uuid", user.getUuid().toString())
.append("version_uuid", data.getId().toString())
Document doc = new Document("player_uuid", user.getUuid())
.append("version_uuid", data.getId())
.append("timestamp", data.getTimestamp().toInstant().toEpochMilli())
.append("save_cause", data.getSaveCause().name())
.append("pinned", data.isPinned())
@@ -377,14 +377,14 @@ public class MongoDbDatabase extends Database {
/**
* Update a saved {@link DataSnapshot} by given version UUID
*
* @param user The user whose data snapshot
* @param user The user whose data snapshot
* @param data The {@link DataSnapshot} to update
*/
@Blocking
@Override
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
try {
Document doc = new Document("player_uuid", user.getUuid().toString()).append("version_uuid", data.getId().toString());
Document doc = new Document("player_uuid", user.getUuid()).append("version_uuid", data.getId());
Bson updates = Updates.combine(
Updates.set("save_cause", data.getSaveCause().name()),
Updates.set("pinned", data.isPinned()),

View File

@@ -124,11 +124,11 @@ public class MySqlDatabase extends Database {
}
} catch (SQLException e) {
throw new IllegalStateException("Failed to create database tables. Please ensure you are running MySQL v8.0+ " +
"and that your connecting user account has privileges to create tables.", e);
"and that your connecting user account has privileges to create tables.", e);
}
} catch (SQLException | IOException e) {
throw new IllegalStateException("Failed to establish a connection to the MySQL database. " +
"Please check the supplied database credentials in the config file", e);
"Please check the supplied database credentials in the config file", e);
}
}

View File

@@ -123,11 +123,11 @@ public class PostgresDatabase extends Database {
}
} catch (SQLException e) {
throw new IllegalStateException("Failed to create database tables. Please ensure you are running PostgreSQL " +
"and that your connecting user account has privileges to create tables.", e);
"and that your connecting user account has privileges to create tables.", e);
}
} catch (SQLException | IOException e) {
throw new IllegalStateException("Failed to establish a connection to the PostgreSQL database. " +
"Please check the supplied database credentials in the config file", e);
"Please check the supplied database credentials in the config file", e);
}
}

View File

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

View File

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

View File

@@ -19,7 +19,6 @@
package net.william278.husksync.event;
@SuppressWarnings("unused")
public interface Cancellable extends Event {
default boolean isCancelled() {

View File

@@ -27,7 +27,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer;
@SuppressWarnings("unused")
public interface PreSyncEvent extends PlayerEvent {
public interface PreSyncEvent extends PlayerEvent, Cancellable {
@NotNull
DataSnapshot.Packed getData();

View File

@@ -53,7 +53,7 @@ public abstract class EventListener {
return;
}
plugin.lockPlayer(user.getUuid());
plugin.getDataSyncer().setUserData(user);
plugin.getDataSyncer().syncApplyUserData(user);
}
/**
@@ -66,7 +66,7 @@ public abstract class EventListener {
return;
}
plugin.lockPlayer(user.getUuid());
plugin.getDataSyncer().saveUserData(user);
plugin.getDataSyncer().syncSaveUserData(user);
}
/**
@@ -94,7 +94,7 @@ public abstract class EventListener {
protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull Data.Items items) {
final SaveOnDeathSettings settings = plugin.getSettings().getSynchronization().getSaveOnDeath();
if (plugin.isDisabling() || !settings.isEnabled() || plugin.isLocked(user.getUuid())
|| user.isNpc() || (!settings.isSaveEmptyItems() && items.isEmpty())) {
|| user.isNpc() || (!settings.isSaveEmptyItems() && items.isEmpty())) {
return;
}
@@ -107,7 +107,7 @@ public abstract class EventListener {
/**
* Handle the plugin disabling
*/
public final void handlePluginDisable() {
public void handlePluginDisable() {
// Save for all online players
plugin.getOnlineUsers().stream()
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())

View File

@@ -54,4 +54,16 @@ public interface LockedHandler {
@ApiStatus.Internal
HuskSync getPlugin();
default void onLoad() {
}
default void onEnable() {
}
default void onDisable() {
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -42,4 +42,6 @@ public final class ConsoleUser implements CommandUser {
public boolean hasPermission(@NotNull String permission) {
return true;
}
}

View File

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

View File

@@ -31,6 +31,8 @@ import java.net.HttpURLConnection;
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;
@@ -133,16 +135,13 @@ public class DataDumper {
*/
@NotNull
public String toFile() throws IOException {
final File filePath = getFilePath();
// Write the data from #getString to the file using a writer
try (final FileWriter writer = new FileWriter(filePath, StandardCharsets.UTF_8, false)) {
writer.write(toString());
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);
}
return "~/plugins/HuskSync/dumps/" + filePath.getName();
}
/**
@@ -152,8 +151,8 @@ public class DataDumper {
* @throws IOException if the prerequisite dumps parent folder could not be created
*/
@NotNull
private File getFilePath() throws IOException {
return new File(getDumpsFolder(), getFileName());
private Path getFilePath() throws IOException {
return getDumpsFolder().resolve(getFileName());
}
/**
@@ -163,14 +162,12 @@ public class DataDumper {
* @throws IOException if the folder could not be created
*/
@NotNull
private File getDumpsFolder() throws IOException {
final File dumpsFolder = new File(plugin.getDataFolder(), "dumps");
if (!dumpsFolder.exists()) {
if (!dumpsFolder.mkdirs()) {
throw new IOException("Failed to create user data dumps folder");
}
private Path getDumpsFolder() throws IOException {
final Path dumps = plugin.getConfigDirectory().resolve("dumps");
if (!Files.exists(dumps)) {
Files.createDirectory(dumps);
}
return dumpsFolder;
return dumps;
}
/**
@@ -181,11 +178,11 @@ public class DataDumper {
@NotNull
private String getFileName() {
return new StringJoiner("_")
.add(user.getUsername())
.add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")))
.add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH))
.add(snapshot.getShortId())
+ ".json";
.add(user.getUsername())
.add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")))
.add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH))
.add(snapshot.getShortId())
+ ".json";
}
}

View File

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

View File

@@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS "%user_data_table%"
timestamp timestamp NOT NULL,
save_cause varchar(32) NOT NULL,
pinned boolean NOT NULL DEFAULT FALSE,
data longblob NOT NULL,
data bytea NOT NULL,
PRIMARY KEY (version_uuid, player_uuid),
FOREIGN KEY (player_uuid) REFERENCES "%users_table%" (uuid) ON DELETE CASCADE

View File

@@ -53,7 +53,7 @@ locales:
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
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: '[Грешка:](#ff3300) [Нямате право да използвате тази команда](#ff7e5e)'
error_console_command_only: '[Грешка:](#ff3300) [Тази команда може да бъде използвана единствено през конзолата](#ff7e5e)'
error_in_game_command_only: 'Грешка: Тази команда може да бъде използвана само от играта.'

View File

@@ -53,7 +53,7 @@ locales:
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
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: '[Fehler:](#ff3300) [Du hast nicht die benötigten Berechtigungen um diesen Befehl auszuführen](#ff7e5e)'
error_console_command_only: '[Fehler:](#ff3300) [Dieser Befehl kann nur über die Konsole ausgeführt werden.](#ff7e5e)'
error_in_game_command_only: 'Fehler: Dieser Befehl kann nur im Spiel genutzt werden.'

View File

@@ -53,7 +53,7 @@ locales:
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
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) [You do not have permission to execute this command](#ff7e5e)'
error_console_command_only: '[Error:](#ff3300) [That command can only be run through console](#ff7e5e)'
error_in_game_command_only: 'Error: That command can only be used in-game.'

View File

@@ -53,7 +53,7 @@ locales:
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
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) [No tienes permisos para ejecutar este comando.](#ff7e5e)'
error_console_command_only: '[Error:](#ff3300) [Este comando solo se puede ejecutar desde la consola.](#ff7e5e)'
error_in_game_command_only: 'Error: Ese comando solo se puede utilizar desde el juego.'

View File

@@ -0,0 +1,65 @@
locales:
synchronization_complete: '[⏵ Données synchronisées!](#00fb9a)'
synchronization_failed: '[⏵ Impossible de synchroniser vos données! Veuillez contacter un administrateur.](#ff7e5e)'
inventory_viewer_menu_title: '&0Inventaire de %1%'
ender_chest_viewer_menu_title: '&0Coffre de l''Ender de %1%'
inventory_viewer_opened: '[Visualisation de l''instantané de](#00fb9a) [%1%](#00fb9a bold)[''s inventaire à partir de ⌚ %2%](#00fb9a)'
ender_chest_viewer_opened: '[Visualisation de l''instantané du](#00fb9a) [%1%](#00fb9a bold)[''s coffre de l''Ender à partir de ⌚ %2%](#00fb9a)'
data_update_complete: '[🔔 Vos données ont été mises à jour!](#00fb9a)'
data_update_failed: '[🔔 Échec de la mise à jour de vos données! Veuillez contacter un administrateur.](#ff7e5e)'
user_registration_complete: '[⭐ Inscription de l''utilisateur complète!](#00fb9a)'
data_manager_title: '[Visualisation de l''instantané des données utilisateur](#00fb9a) [%1%](#00fb9a show_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%](#00fb9a bold show_text=&7UUID du joueur:\n&8%4%)[:](#00fb9a)'
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Horodatage de la version:\n&8Quand les données ont été enregistrées)'
data_manager_pinned: '[※ Instantané épinglé](#d8ff2b show_text=&7Épinglé:\n&8Cet instantané des données utilisateur ne sera pas automatiquement supprimé.)'
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Cause de la sauvegarde:\n&8Ce qui a causé l''enregistrement des données)'
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Serveur:\n&8Nom du serveur sur lequel les données ont été enregistrées)'
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Taille de l''instantané:\n&8Taille du fichier estimée de l''instantané (en KiB))\n'
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Points de vie) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Points de faim) [ʟᴠ](green)[.](gray)[%4%](greenshow_text=&7Niveau XP) [🏹 %5%](dark_aqua show_text=&7Mode de jeu)'
data_manager_advancements_statistics: '[⭐ Avancements: %1%](color=#ffc43b-#f5c962show_text=&7Avancements dans lesquels vous avez progressé:\n&8%2%) [⌛ Temps de jeu: %3%ʜʀs](color=#62a9f5-#7ab8fashow_text=&7Temps de jeu en jeu\n&8⚠ Basé sur les statistiques en jeu)\n'
data_manager_item_buttons: '[Voir:](gray) [[🪣 Inventaire…]](color=#a17b5f-#f5b98cshow_text=&7Cliquez pour voir run_command=/inventory %1% %2%) [[⌀ Coffre de l''Ender…]](#b649c4-#d254ffshow_text=&7Cliquez pour voir run_command=/enderchest %1% %2%)'
data_manager_management_buttons: '[Gérer:](gray) [[❌ Supprimer…]](#ff3300 show_text=&7Cliquezpour supprimer cet instantané des données utilisateur.\n&8Cela n''affectera pas les données actuelles de l''utilisateur.\n&#ff3300&⚠ Cette action est irréversible! suggest_command=/husksync:userdata delete%1% %2%) [[⏪ Restaurer…]](#00fb9a show_text=&7Cliquez pour restaurer ces données utilisateur.\n&8Cela définira les données de l''utilisateur sur cet instantané.\n&#ff3300&⚠ Les données actuelles de %1% serontremplacées! suggest_command=/husksync:userdata restore %1% %2%) [[※ Épingler/Détacher…]](#d8ff2bshow_text=&7Cliquez pour épingler ou détacher cet instantané des données utilisateur\n&8Les instantanés épinglés ne seront pas automatiquement supprimés run_command=/userdata pin %1% %2%)'
data_manager_system_buttons: '[Système:](gray) [[⏷ Export fichier…]](dark_gray show_text=&7Cliquezpour exporter cet instantané des données utilisateur à un fichier.\n&8Les exports de données peuvent être trouvés dans ~/plugins/HuskSync/dumps/run_command=/husksync:userdata dump %1% %2% file) [[☂ Export web…]](dark_grayshow_text=&7Cliquez pour exporter cet instantané des données utilisateur au service mc-logs\n&8Vousobtiendrez une URL contenant les données. run_command=/husksync:userdatadump %1% %2% web)'
data_manager_advancements_preview_remaining: 'et %1% autres…'
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_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%'
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%)'
list_page_jumpers: '(%1%)'
list_page_jumper_button: '[%1%](show_text=&7Aller à la page %1% run_command=%2% %1%)'
list_page_jumper_current_page: '[%1%](#00fb9a)'
list_page_jumper_separator: ' '
list_page_jumper_group_separator: '…'
save_cause_disconnect: 'déconnexion'
save_cause_world_save: 'sauvegarde du monde'
save_cause_death: 'mort'
save_cause_server_shutdown: 'arrêt du serveur'
save_cause_inventory_command: 'commande d''inventaire'
save_cause_enderchest_command: 'commande du coffre de l''Ender'
save_cause_backup_restore: 'restauration de sauvegarde'
save_cause_api: 'API'
save_cause_mpdb_migration: 'migration MPDB'
save_cause_legacy_migration: 'migration legacy'
save_cause_converted_from_v2: 'converti de v2'
up_to_date: '[HuskSync](#00fb9a bold) [| Vous utilisez la dernière version de HuskSync(v%1%).](#00fb9a)'
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)'
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%)'
error_no_permission: '[Erreur:](#ff3300) [Vous n''avez pas la permission d''exécuter cettecommande](#ff7e5e)'
error_console_command_only: '[Erreur:](#ff3300) [Cette commande peut seulement être exécutée via la console](#ff7e5e)'
error_in_game_command_only: 'Erreur: Cette commande peut uniquement être utilisée en jeu.'
error_no_data_to_display: '[Erreur:](#ff3300) [Impossible de trouver des données utilisateur à afficher.](#ff7e5e)'
error_invalid_version_uuid: '[Erreur:](#ff3300) [Impossible de trouver des données utilisateur pour cet UUID de version.](#ff7e5e)'
husksync_command_description: 'Gérer le plugin HuskSync'
userdata_command_description: 'Voir, gérer & restaurer les données utilisateur des joueurs'
inventory_command_description: 'Voir & modifier l''inventaire d''un joueur'
enderchest_command_description: 'Voir & modifier le Coffre de l''Ender d''un joueur'

View File

@@ -53,7 +53,7 @@ locales:
system_status_header: '[HuskSync](#00fb9a bold) [| Laporan status sistem:](#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) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
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: '[Kesalahan:](#ff3300) [Kamu tidak memiliki izin untuk menjalankan perintah ini](#ff7e5e)'
error_console_command_only: '[Kesalahan:](#ff3300) [Perintah itu hanya dapat dijalankan melalui konsol](#ff7e5e)'
error_in_game_command_only: 'Kesalahan: Perintah itu hanya dapat dijalankan dalam game.'

View File

@@ -53,7 +53,7 @@ locales:
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
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: '[Errore:](#ff3300) [Non hai il permesso di usare questo comando](#ff7e5e)'
error_console_command_only: '[Errore:](#ff3300) [Questo comando può essere eseguito solo dalla](#ff7e5e)'
error_in_game_command_only: 'Errore: Questo comando può essere utilizzato solo in gioco.'

View File

@@ -53,7 +53,7 @@ locales:
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) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
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: そのコマンドはゲーム内でしか使えません。'

View File

@@ -53,7 +53,7 @@ locales:
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
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: '[오류:](#ff3300) [해당 명령어를 사용할 권한이 없습니다.](#ff7e5e)'
error_console_command_only: '[오류:](#ff3300) [해당 명령어는 콘솔을 통해서만 사용할 수 있습니다.](#ff7e5e)'
error_in_game_command_only: '오류: 해당 명령어는 게임 내부에서만 사용할 수 있습니다.'

View File

@@ -53,7 +53,7 @@ locales:
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
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) [Je hebt geen toestemming om deze opdracht uit te voeren](#ff7e5e)'
error_console_command_only: '[Error:](#ff3300) [Dat command kan alleen via de console worden uitgevoerd](#ff7e5e)'
error_in_game_command_only: 'Error: Dat command kan alleen in-game worden gebruikt.'

View File

@@ -53,7 +53,7 @@ locales:
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
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) [Você não tem permissão para executar este comando](#ff7e5e)'
error_console_command_only: '[Error:](#ff3300) [Esse comando só pode ser executado através do console](#ff7e5e)'
error_in_game_command_only: 'Error: Esse comando só pode ser usado dentro do jogo.'

View File

@@ -53,7 +53,7 @@ locales:
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
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: '[Ошибка:](#ff3300) [У вас недостаточно прав для выполнения данной команды.](#ff7e5e)'
error_console_command_only: '[Ошибка:](#ff3300) [Данная команда может быть выполнена только из консоли.](#ff7e5e)'
error_in_game_command_only: 'Ошибка: Данная команда может быть выполнена только в игре.'

View File

@@ -53,7 +53,7 @@ locales:
system_status_header: '[HuskSync](#00fb9a bold) [| Sistem durumu raporu:](#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) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
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: '[Hata:](#ff3300) [Bu komutu gerçekleştirmek için izniniz yok](#ff7e5e)'
error_console_command_only: '[Hata:](#ff3300) [Bu komut yalnızca konsoldan çalıştırılabilir](#ff7e5e)'
error_in_game_command_only: 'Hata: Bu komut yalnızca oyun içinde kullanılabilir.'

View File

@@ -53,7 +53,7 @@ locales:
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#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) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
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: '[Помилка:](#ff3300) [Ввас немає дозволу на використання цієї команди](#ff7e5e)'
error_console_command_only: '[Помилка:](#ff3300) [Ця команда може бути використана лише з допомогою %1% консолі](#ff7e5e)'
error_in_game_command_only: 'Error: That command can only be used in-game.'

View File

@@ -1,34 +1,34 @@
locales:
synchronization_complete: '[⏵ 資料已同步!](#00fb9a)'
synchronization_complete: '[⏵資料已同步!](#00fb9a)'
synchronization_failed: '[⏵ 無法同步您的資料! 請聯繫管理員](#ff7e5e)'
inventory_viewer_menu_title: '&0%1% 的背包'
ender_chest_viewer_menu_title: '&0%1% 的終界箱'
inventory_viewer_opened: '[查看備份](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的背包備份](#00fb9a)'
ender_chest_viewer_opened: '[查看備份](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的終界箱備份](#00fb9a)'
data_update_complete: '[🔔 你的數據資料已更新!](#00fb9a)'
data_update_failed: '[🔔 無法更新你的數據資料! 請聯繫管理員](#ff7e5e)'
user_registration_complete: '[⭐ 用戶註冊完成!](#00fb9a)'
data_manager_title: '[正在查看玩家](#00fb9a) [%3%](#00fb9a bold show_text=&7玩家UUID:\n&8%4%) [的數據備份](#00fb9a) [%1%](#00fb9a show_text=&7備份版本UUID:\n&8%2%)[:](#00fb9a)'
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=&7伺服器:\n&8數據保存所在伺服器名稱)'
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7備份大小:\n&8備份的估計文件大小(以KiB為單位))\n'
inventory_viewer_opened: '[查看](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的背包快照資料](#00fb9a)'
ender_chest_viewer_opened: '[查看](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的終界箱快照資料](#00fb9a)'
data_update_complete: '[🔔 你的資料已更新!](#00fb9a)'
data_update_failed: '[🔔 無法更新您的資料 請聯繫管理員](#ff7e5e)'
user_registration_complete: '[⭐ 使用者註冊成功!](#00fb9a)'
data_manager_title: '[查看](#00fb9a) [%3%](#00fb9a bold show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [:](#00fb9a)'
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=&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%小時](color=#62a9f5-#7ab8fa show_text=&7遊戲遊玩時間\n&8⚠ 基於遊戲内的統計訊息)\n'
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'
data_manager_item_buttons: '[查看:](gray) [[🪣 背包…]](color=#a17b5f-#f5b98c show_text=&7點擊查看 run_command=/inventory %1% %2%) [[⌀ 終界箱…]](#b649c4-#d254ff show_text=&7點擊查看 run_command=/enderchest %1% %2%)'
data_manager_management_buttons: '[管理:](gray) [[❌ 刪除…]](#ff3300 show_text=&7點擊刪除此玩家數據的備份.\n&8這不會影響玩家的當前數據.\n&#ff3300&⚠ 此操作無法撤銷! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ 恢復…]](#00fb9a show_text=&7點擊還原此玩家的數據.\n&8這將會讓用户的數據恢復到此備份.\n&#ff3300&⚠ %1%當前的數據將被覆蓋! suggest_command=/husksync:userdata restore %1% %2%) [[※ 標記/取消標記…]](#d8ff2b show_text=&7點擊標記或取消標記此玩家數據備份\n&8標記的備份不會按照備份時間自動排序 run_command=/userdata pin %1% %2%)'
data_manager_system_buttons: '[系統:](gray) [[⏷ 本地轉存…]](dark_gray show_text=&7點擊將此玩家數據資料轉存到本地文件中.\n&8轉存的資料可以在以下路徑找到~/plugins/HuskSync/dumps/中找到 run_command=/husksync:userdata dump %1% %2% file) [[☂ 雲端轉存…]](dark_gray show_text=&7點擊將此玩家數據資料轉存到 mc-logs 服務\n&8您將獲得一個包含資料的URL. run_command=/husksync:userdata dump %1% %2% web)'
data_manager_advancements_preview_remaining: '以及其他 %1%…'
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=&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_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%'
list_footer: '\n%1%[頁](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
data_manager_management_buttons: '[管理:](gray) [[❌ 刪除…]](#ff3300 show_text=&7點擊刪除這個快照\n&8這不會影像目前玩家的資料\n&#ff3300&⚠ 此操作不能取消! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ 恢復…]](#00fb9a show_text=&7點擊將玩家資料覆蓋為此快照\n&8這將導致玩家的資料會被此快照覆蓋\n&#ff3300&⚠ %1% 當前的資料將被覆蓋! suggest_command=/husksync:userdata restore %1% %2%) [[※ 標記…]](#d8ff2b show_text=&7點擊切換標記狀態\n&8標記的快照將不會自動輪換更新 run_command=/userdata pin %1% %2%)'
data_manager_system_buttons: '[系統:](gray) [[⏷ 本地轉存…]](dark_gray show_text=&7點擊將此玩家資料快照轉存到本地文件中\n&8轉存的資料可以在以下路徑找到 ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ 雲端轉存…]](dark_gray show_text=&7點擊將此玩家資料快照轉存到 mc-logs 服務\n&8您將獲得一個包含資料的 URL. run_command=/husksync:userdata dump %1% %2% web)'
data_manager_advancements_preview_remaining: '還有 %1% …'
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_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%'
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%)'
list_page_jumpers: '(%1%)'
@@ -36,30 +36,30 @@ locales:
list_page_jumper_current_page: '[%1%](#00fb9a)'
list_page_jumper_separator: ' '
list_page_jumper_group_separator: '…'
save_cause_disconnect: '斷開連接'
save_cause_world_save: '保存世界'
save_cause_disconnect: '離線'
save_cause_world_save: '世界儲存'
save_cause_death: '死亡'
save_cause_server_shutdown: '伺服器關閉'
save_cause_inventory_command: '背包指令'
save_cause_enderchest_command: '終界箱指令'
save_cause_backup_restore: '備份還原'
save_cause_api: 'API'
save_cause_mpdb_migration: 'MPDB遷移'
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) [| 發現可用的新版本:v%1%(當前版本:v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| 已重新載入配置和訊息文件.](#00fb9a)\n[⚠ 確保所有伺服器上的配置文件都是最新的!](#00fb9a)\n[需要重新啟動配置更改才能生效.](#00fb9a italic)'
save_cause_converted_from_v2: '從 v2 轉換'
up_to_date: '[HuskSync](#00fb9a bold) [| 您運行的是最新版本的 HuskSync (v%1%).](#00fb9a)'
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)'
error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&點擊建議 suggest_command=%1%)'
error_invalid_player: '[錯誤:](#ff3300) [找不到這位玩家.](#ff7e5e)'
error_invalid_data: '[Error:](#ff3300) [Could not unpack snapshot data as it invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %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) [該指令只能透過 控制台 執行](#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: '查看、管理和恢復玩家用户數據'
inventory_command_description: '查看和編輯玩家的背包'
error_in_game_command_only: '[錯誤:](#ff3300) [該指令只能在遊戲內執行](#ff7e5e)'
error_no_data_to_display: '[錯誤:](#ff3300) [找不到任何可顯示的用戶資料.](#ff7e5e)'
error_invalid_version_uuid: '[錯誤:](#ff3300) [找不到正確的 Version UUID.](#ff7e5e)'
husksync_command_description: '管理 HuskSync 插件'
userdata_command_description: '查看、管理和還原玩家資料'
inventory_command_description: '查看和編輯玩家的物品欄'
enderchest_command_description: '查看和編輯玩家的終界箱'

View File

@@ -4,11 +4,20 @@ Consult the Javadocs for more information. Please note that carrying out expensi
## Bukkit Platform Events
> **Tip:** Don't forget to register your listener when listening for these event calls.
>
| Bukkit Event class | Cancellable | Description |
|---------------------------|:-----------:|---------------------------------------------------------------------------------------------|
| `BukkitDataSaveEvent` | ✅ | Called when player data snapshot is created, saved and cached due to a DataSaveCause |
| `BukkitPreSyncEvent` | ✅ | Called before a player has their data updated from the cache or database, just after login |
| `BukkitSyncCompleteEvent` | ❌ | Called once a player has completed their data synchronization on login successfully&dagger; |
## Fabric Platform Callbacks
> Access the callback via the static EVENT field in each interface class.
| Fabric Callback | Cancellable | Description |
|------------------------------|:-----------:|---------------------------------------------------------------------------------------------|
| `FabricDataSaveCallback` | ✅ | Called when player data snapshot is created, saved and cached due to a DataSaveCause |
| `FabricPreSyncCallback` | ✅ | Called before a player has their data updated from the cache or database, just after login |
| `FabricSyncCompleteCallback` | ❌ | Called once a player has completed their data synchronization on login successfully&dagger; |
&dagger;This can also fire when a user's data is updated while the player is logged in; i.e., when an admin rolls back the user, updates their inventory or Ender Chest through the respective commands, or when an API call is made forcing the user to have their data updated.

View File

@@ -17,6 +17,7 @@ The HuskSync API shares version numbering with the plugin itself for consistency
The HuskSync API is available for the following platforms:
* `bukkit` - Bukkit, Spigot, Paper, etc. Provides Bukkit API event listeners and adapters to `org.bukkit` objects.
* `fabric` - Fabric API for Minecraft. Provides Fabric API event listeners and adapters to `net.minecraft` objects.
* `common` - Common API for all platforms.
@@ -52,7 +53,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 `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)
Add the dependency to your `pom.xml` as per below. Replace `VERSION` with the latest version of HuskSync (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/HuskSync?color=%23282828&label=%20&style=flat-square). Note for Fabric you must append the target Minecraft version to the version number (e.g. `3.6.1+1.20.1`).
```xml
<dependency>
<groupId>net.william278.husksync</groupId>
@@ -75,7 +76,7 @@ allprojects {
}
}
```
Add the dependency as per below. Replace `VERSION` with the latest version of HuskSync (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/HuskSync?color=%23282828&label=%20&style=flat-square)
Add the dependency as per below. Replace `VERSION` with the latest version of HuskSync (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/HuskSync?color=%23282828&label=%20&style=flat-square). Note for Fabric you must append the target Minecraft version to the version number (e.g. `3.6.1+1.20.1`).
```groovy
dependencies {

View File

@@ -28,13 +28,13 @@ check_for_updates: true
cluster_id: ''
# Enable development debug logging
debug_logging: true
# Whether to provide modern, rich TAB suggestions for commands (if available)
brigadier_tab_completion: false
# Whether to enable the Player Analytics hook.
# Docs: https://william278.net/docs/husksync/plan-hook
enable_plan_hook: true
# Whether to cancel game event packets directly when handling locked players if ProtocolLib is installed
# Whether to cancel game event packets directly when handling locked players if ProtocolLib or PacketEvents is installed
cancel_packets: true
# Add HuskSync commands to this list to prevent them from being registered (e.g. ['userdata'])
disabled_commands: []
# Database settings
database:
# Type of database to use (MYSQL, MARIADB, POSTGRES, MONGO)
@@ -78,7 +78,7 @@ redis:
# List of host:port pairs
nodes: []
password: ''
# Redis settings
# Data syncing settings
synchronization:
# The data synchronization mode to use (LOCKSTEP or DELAY). LOCKSTEP is recommended for most networks.
# Docs: https://william278.net/docs/husksync/sync-modes
@@ -109,7 +109,7 @@ synchronization:
sync_dead_players_changing_server: true
# Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing
compress_data: true
# Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)
# Where to display sync notifications (ACTION_BAR, CHAT or NONE)
notification_display_slot: ACTION_BAR
# Persist maps locked in a Cartography Table to let them be viewed on any server
persist_locked_maps: true
@@ -134,9 +134,14 @@ synchronization:
# Commands which should be blocked before a player has finished syncing (Use * to block all commands)
blacklisted_commands_while_locked:
- '*'
# For attribute syncing, which attributes should be ignored/skipped when syncing
# (e.g. ['minecraft:generic.max_health', 'minecraft:generic.attack_damage'])
ignored_attributes: []
# Configuration for how to sync attributes
attributes:
# Which attributes should not be saved when syncing users. Supports wildcard matching.
# (e.g. ['minecraft:generic.max_health', 'minecraft:generic.*'])
ignored_attributes: []
# Which modifiers should not be saved when syncing users. Supports wildcard matching.
# (e.g. ['minecraft:effect.speed', 'minecraft:effect.*'])
ignored_modifiers: ['minecraft:effect.*', 'minecraft:creative_mode_*']
# Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts
event_priorities:
quit_listener: LOWEST

View File

@@ -93,7 +93,7 @@ public class LoginParticleData extends BukkitData implements Adaptable {
public class LoginParticleSerializer extends BukkitSerializer.Json<LoginParticleData> implements Serializer<LoginParticleData> {
// We need to create a constructor that takes our instance of the API
public GameMode(@NotNull HuskSyncAPI api) {
public LoginParticleSerializer(@NotNull HuskSyncAPI api) {
super(api, LoginParticleData.class); // We pass the class type here so that Gson knows what class we're serializing
}
@@ -116,12 +116,26 @@ public static Identifier LOGIN_PARTICLES_ID = Identifier.from("myplugin", "login
huskSyncAPI.registerSerializer(LOGIN_PARTICLES_ID, new LoginParticleSerializer(HuskSyncAPI.getInstance()));
```
### 3.1 Identifier dependencies
* HuskSync lets you specify a set of `Dependency` objects when creating an `Identifier`. These are used to deterministically apply data in a specific order.
* Dependencies are references to other data type identifiers. HuskSync will apply data in dependency-order; that is, it will apply the data of the dependencies before applying the data of the dependent.
* This is useful when you have data that relies on other data to be applied first; for example, if you're writing an add-on for additional modded inventory data and you need to apply the base inventory data first.
* You can specify whether a dependency is required or optional. HuskSync will not sync data of a type that has a required dependency that is missing (for instance, if it is disabled in the config, or - if provided by another plugin - has failed to register).
* Use `Identifer#from(String, String, Set<Dependency>)` or `Identifier#from(Key, Set<Dependency>)` to create an identifier with dependencies
* Dependencies can be created with `Dependency.optional(Identifier)` or `Dependency.required(Identifier)` for optional or required dependencies respectively.
## 4. Setting and getting our Data to/from a User
* Now that we've registered our `Data` and `Serializer` classes, we can set our data to a user, applying it to them.
* To do this, we use the `OnlineUser#setData(Identifier, Data)` method.
* This method will apply the data to the user, and store the data to the plugin player custom data map, to allow the data to be retrieved later and be saved to snapshots.
* Snapshots created on servers where the data type is registered will now contain our data and synchronise between instances!
```java
// Create an identifier for our data requiring the user's location to have been set first
public static Identifier LOGIN_PARTICLES_ID = Identifier.from("myplugin", "login_particles", Set.of(Dependency.optional(Key.key("husksync", "location"))));
// We can then register this as we did previously (...)
```
```java
// Create an instance of our data
LoginParticleData loginParticleData = new LoginParticleData("FIREWORKS_SPARK", 10);
@@ -134,7 +148,7 @@ LoginParticleData loginParticleData = (LoginParticleData) huskSyncAPI.getUser(pl
```
### 4.1 Persisting custom data on the DataSaveEvent
Add an EventListener to the `DataSaveEvent` and use the `#editData` consumer method to apply custom data during standard DataSaves. This will persist data to users any time the data save routine executes (on user logout, server shutdownm, world save, etc).
Add an EventListener to the `DataSaveEvent` and use the `#editData` consumer method to apply custom data during standard DataSaves. This will persist data to users any time the data save routine executes (on user logout, server shutdown, world save, etc.)
```java
@EventHandler

View File

@@ -213,8 +213,8 @@ huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> {
// Get the health data
Data.Health health = healthOptional.get();
double currentHealth = health.getCurrentHealth(); // Current health
double healthScale = health.getHealthScale(); // Health scale (e.g., 20 for 20 hearts)
snapshot.setHealth(BukkitData.Health.from(20, 20));
double healthScale = health.getHealthScale(); // Health scale (used to determine health/damage display hearts)
snapshot.setHealth(BukkitData.Health.from(20, 20, true));
// Need max health? Look at the Attributes data type.
// Get the game mode data

View File

@@ -12,23 +12,29 @@ HuskSync supports synchronising a wide range of different data elements, each of
<details>
<summary>&nbsp;<b>Are modded items supported?</b></summary>
If you're running HuskSync on Arclight or similar, please note we will not be able to provide you with support, but have been reported to save & sync correctly with HuskSync v3.x+.
On Fabric, modded items should usually sync as you would expect with HuskSync. Note that mods which store additional data separate from item NBT on each server may not work as expected. Mod developers &mdash; check out the [[Custom Data API]] for information on how to get your mod's data syncing!
**TL;DR** &mdash; modded items may work, but since we can't guarantee compatibility, we do not officially mark them as supported. Be sure to test thoroughly before deploying on production!
On Spigot, if you're running HuskSync on Arclight or similar, please note we will not be able to provide you with support, but have been reported to save & sync correctly with HuskSync v3.x+.
Please note we cannot guarantee compatibility with everything &mdash; test thoroughly!
</details>
<details>
<summary>&nbsp;<b>Are MMOItems / SlimeFun / ItemsAdder items supported?</b></summary>
These plugins, which provide custom items, should be supported as of HuskSync v3.x+; but do note we cannot guarantee compatibility with all methods of injecting custom data to create custom items. Be sure to test thoroughly before deploying on production!
These custom item Spigot plugins should work as expected provided they inject data into item NBT in a standard way.
Please note we cannot guarantee compatibility with everything &mdash; test thoroughly!
</details>
<details>
<summary>&nbsp;<b>Is Redis required? What is Redis?</b></summary>
HuskSync requires Redis to operate (for reasons demonstrated below). Redis is an in-memory database server used for caching data at scale and sending messages across a network. You have a Redis server in a similar fashion to the way you have a MySQL database server. If you're using a Minecraft hosting company, you'll want to contact their support and ask if they offer Redis. If you're looking for a host, I have a list of some popular hosts and whether they support Redis [available to read here.](https://william278.net/redis-hosts)
Yes! HuskSync requires Redis to operate (for reasons demonstrated below).
Redis is an in-memory database server used for caching data at scale and sending messages across a network. You have a Redis server in a similar fashion to the way you have a MySQL database server. If you're using a Minecraft hosting company, you'll want to contact their support and ask if they offer Redis. If you're looking for a host, I have a list of some popular hosts and whether they support Redis [available to read here.](https://william278.net/redis-hosts)
</details>

View File

@@ -30,5 +30,5 @@ Welcome! This is the plugin documentation for HuskSync v3.x+. Please click throu
* 📂 [Buy HuskSync](https://william278.net/project/husksync/)
* 🚰 [Spigot](https://www.spigotmc.org/resources/husksync.97144/)
* 🛒 [Polymart](https://polymart.org/resource/husksync.1634)
* ⚒️ [Craftaro](https://craftaro.com/marketplace/product/husksync.758)
* ⚒️ [BuiltByBit](https://craftaro.com/marketplace/product/husksync.758)
* 💬 [Discord Support](https://discord.gg/tVYhJfyDWG)

View File

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

View File

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

View File

@@ -1,50 +1,63 @@
This will walk you through installing HuskSync on your network of Spigot servers.
> **Warning:** Fabric support is currently in beta and is not production ready yet. Customers can get in touch on Discord to request the Fabric build, or you can self-compile.
This will walk you through installing HuskSync on your network of Spigot or Fabric servers.
## Requirements
> **Note:** If the plugin fails to load, please check that you are not running an [incompatible version combination](Unsupported-Versions)
> **Warning:** Mixing and matching Fabric/Spigot servers is not supported, and all servers must be running the same Minecraft version.
* A MySQL Database (v8.0+) (MariaDB, PostrgreSQL or MongoDB are also supported)
> **Note:** Please also note some specific legacy Paper/Purpur versions are [not compatible](Unsupported-Versions) with HuskSync.
* A MySQL Database (v8.0+)
* **OR** a MariaDB, PostrgreSQL or MongoDB database, which are also supported
* A Redis Database (v5.0+) &mdash; see [[FAQs]] for more details.
* Any number of Spigot servers, connected by a BungeeCord or Velocity-based proxy (Minecraft v1.17.1+, running Java 17+)
* **OR** a network of Fabric servers, connected by a Fabric proxy (Minecraft v1.20.1, running Java 17+)
## Setup Instructions
### 1. Install the jar
- Place the plugin jar file in the `/plugins/` directory of each Spigot server.
- Place the plugin jar file in the `/plugins/` or `/mods/` directory of each Spigot/Fabric server respectively.
- You do not need to install HuskSync as a proxy plugin.
- You can additionally install [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) for better locked user handling, and [Plan](https://www.spigotmc.org/resources/plan-player-analytics.32536/) for analytics.
- _Spigot users_: You can additionally install [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) or [PacketEvents](https://www.spigotmc.org/resources/packetevents-api.80279/) for better locked user handling.
- _Fabric users_: Ensure the latest Fabric API mod jar is installed!
### 2. Restart servers
- Start, then stop every server to let HuskSync generate the [[config file]].
- HuskSync will throw an error in the console and disable itself as it is unable to connect to the database. You haven't set the credentials yet, so this is expected.
- Advanced users: If you'd prefer, you can create one config.yml file and create symbolic links in each `/plugins/HuskSync/` folder to it to make updating it easier.
### 3. Enter Mysql & Redis database credentials
- Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`)
- Navigate to the new config file on each server (`~/plugins/HuskSync/config.yml` on Spigot, `~/config/husksync/config.yml` on Fabric)
- Under `credentials` in the `database` section, enter the credentials of your (MySQL/MariaDB/MongoDB/PostgreSQL) Database. You shouldn't touch the `connection_pool` properties.
- Under `credentials` in the `redis` section, enter the credentials of your Redis Database. If your Redis server doesn't have a password, leave the password blank as it is.
- Unless you want to have multiple clusters of servers within your network, each with separate user data, you should not change the value of `cluster_id`.
<details>
<summary>Important &mdash; MongoDB Users</summary>
<summary>MongoDB users &mdash; additional instructions</summary>
- Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`)
- Set `type` in the `database` section to `MONGO`
- Under `credentials` in the `database` section, enter the credentials of your MongoDB Database. You shouldn't touch the `connection_pool` properties.
<details>
- Under `parameters` in the `mongo_settings` section, ensure the specified `&authSource=` matches the database you are using (default is `HuskSync`).
<summary>Additional configuration for MongoDB Atlas users</summary>
#### Additional setup for MongoDB Atlas
- Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`)
- Set `using_atlas` in the `mongo_settings` section to `true`.
- Remove `&authSource=HuskSync` from `parameters` in the `mongo_settings`.
(The `port` setting in `credentials` is disregarded when using Atlas.)
</details>
<details>
<summary>Pterodactyl self-hosts &mdash; Redis setup instructions</summary>
If you are hosting your Redis server on the same node as your servers, you need to use `172.18.0.1` as your host (or equivalent if you changed your network settings), and bind it in the Redis config `nano /etc/redis/redis.conf`.
You will also need to uncomment the `requirepass` directive and set a password to allow outside connections, or disable `protected-mode`. Once a password is set and Redis is restarted `systemctl restart redis`, you will also need to update the password in your pterodactyl `.env` (`nano /var/www/pterodactyl/.env`) and refresh the cache `cd /var/www/pterodactyl && php artisan config:clear`.
You may also need to allow connections from your firewall depending on your distribution.
</details>
### 4. Set server names in server.yml files
- Navigate to the HuskSync server name file on each server (`~/plugins/HuskSync/server.yml`)
- Navigate to the server name file on each server (`~/plugins/HuskSync/server.yml` on Spigot, `~/config/husksync/server.yml` on Fabric)
- Set the `name:` of the server in this file to the ID of this server as defined in the config of your proxy (e.g., if this is the "hub" server you access with `/server hub`, put `'hub'` here)
### 5. Start every server again

View File

@@ -38,7 +38,7 @@ Although it's a common request, HuskSync doesn't synchronize economy data for a
I strongly recommend making use of economy plugins that have cross-server economy balance synchronization built-in, of which there are a multitude of options available. Please see our [[FAQs]] section for more details on this decision.
## Toggling Sync Features
All synchronization features, except location and locked map synchronising, are enabled by default. To toggle a feature, navigate to the `features:` section in the `synchronization:` part of your `config.yml` file, and change the option to `true`/`false` respectively.
All synchronization features, except location and locked map synchronizing, are enabled by default. To toggle a feature, navigate to the `features:` section in the `synchronization:` part of your `config.yml` file, and change the option to `true`/`false` respectively.
<details>
<summary>Example in config.yml</summary>

View File

@@ -27,6 +27,5 @@
* 📂 [Buy HuskSync](https://william278.net/project/husksync/)
* 🚰 [Spigot](https://www.spigotmc.org/resources/husksync.97144/)
* 🛒 [Polymart](https://polymart.org/resource/husksync.1634)
* ⚒️ [Craftaro](https://craftaro.com/marketplace/product/husksync.758)
* 🛒 [BuiltByBit](https://craftaro.com/marketplace/product/husksync.758)
* ⚒️ [BuiltByBit](https://craftaro.com/marketplace/product/husksync.758)
* 💬 [Discord Support](https://discord.gg/tVYhJfyDWG)

73
fabric/build.gradle Normal file
View File

@@ -0,0 +1,73 @@
plugins {
id 'fabric-loom' version '1.7-SNAPSHOT'
}
apply plugin: 'fabric-loom'
loom.serverOnlyMinecraftJar()
repositories {
maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' }
maven { url 'https://maven.nucleoid.xyz' }
}
dependencies {
minecraft "com.mojang:minecraft:${fabric_minecraft_version}"
mappings "net.fabricmc:yarn:${fabric_yarn_mappings}:v2"
modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}"
modImplementation include("net.kyori:adventure-platform-fabric:${adventure_platform_fabric_version}")
modImplementation include("me.lucko:fabric-permissions-api:${fabric_permissions_api_version}")
modImplementation include("eu.pb4:sgui:${sgui_version}")
modImplementation include('net.william278.uniform:uniform-fabric:1.2.1+1.20.1')
modCompileOnly "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}"
implementation include('org.apache.commons:commons-pool2:2.12.0')
implementation include("redis.clients:jedis:$jedis_version")
implementation include("com.mysql:mysql-connector-j:$mysql_driver_version")
implementation include("org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version")
implementation include("org.postgresql:postgresql:$postgres_driver_version")
implementation include("org.xerial.snappy:snappy-java:$snappy_version")
compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'net.william278:DesertWell:2.0.4'
compileOnly 'org.projectlombok:lombok:1.18.34'
annotationProcessor 'org.projectlombok:lombok:1.18.34'
shadow project(path: ":common")
}
shadowJar {
configurations = [project.configurations.shadow]
destinationDirectory.set(file("$projectDir/build/libs"))
exclude('net.fabricmc:.*')
exclude('net.kyori:.*')
exclude '/mappings/*'
relocate 'org.apache.commons.io', 'net.william278.husksync.libraries.commons.io'
relocate 'org.apache.commons.text', 'net.william278.husksync.libraries.commons.text'
relocate 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3'
relocate 'com.google.gson', 'net.william278.husksync.libraries.gson'
relocate 'com.fatboyindustrial', 'net.william278.husksync.libraries'
relocate 'de.themoep', 'net.william278.husksync.libraries'
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
relocate 'org.intellij', 'net.william278.husksync.libraries'
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
relocate 'de.exlll', 'net.william278.husksync.libraries'
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'org.json', 'net.william278.husksync.libraries.json'
}
remapJar {
dependsOn tasks.shadowJar
mustRunAfter tasks.shadowJar
inputFile = shadowJar.archiveFile.get()
addNestedDependencies = true
destinationDirectory.set(file("$rootDir/target/"))
archiveClassifier.set('')
}
shadowJar.finalizedBy(remapJar)

View File

@@ -0,0 +1,361 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.fabricmc.api.DedicatedServerModInitializer;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.kyori.adventure.platform.AudienceProvider;
import net.kyori.adventure.platform.fabric.FabricServerAudiences;
import net.minecraft.server.MinecraftServer;
import net.william278.desertwell.util.Version;
import net.william278.husksync.adapter.DataAdapter;
import net.william278.husksync.adapter.GsonAdapter;
import net.william278.husksync.adapter.SnappyGsonAdapter;
import net.william278.husksync.api.FabricHuskSyncAPI;
import net.william278.husksync.command.PluginCommand;
import net.william278.husksync.config.Locales;
import net.william278.husksync.config.Server;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.*;
import net.william278.husksync.database.Database;
import net.william278.husksync.database.MongoDbDatabase;
import net.william278.husksync.database.MySqlDatabase;
import net.william278.husksync.database.PostgresDatabase;
import net.william278.husksync.event.FabricEventDispatcher;
import net.william278.husksync.event.ModLoadedCallback;
import net.william278.husksync.hook.PlanHook;
import net.william278.husksync.listener.EventListener;
import net.william278.husksync.listener.FabricEventListener;
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.FabricUser;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.util.FabricTask;
import net.william278.husksync.util.LegacyConverter;
import net.william278.uniform.Uniform;
import net.william278.uniform.fabric.FabricUniform;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.spi.LoggingEventBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.logging.Level;
@Getter
@NoArgsConstructor
public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync, FabricTask.Supplier,
FabricEventDispatcher {
private static final String PLATFORM_TYPE_ID = "fabric";
private final TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap(
SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR
);
private final Map<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap();
private final Map<String, Boolean> permissions = Maps.newHashMap();
private final List<Migrator> availableMigrators = Lists.newArrayList();
private final Set<UUID> lockedPlayers = Sets.newConcurrentHashSet();
private final Map<UUID, FabricUser> playerMap = Maps.newConcurrentMap();
private Logger logger;
private ModContainer mod;
private MinecraftServer minecraftServer;
private boolean disabling;
private Gson gson;
private AudienceProvider audiences;
private Database database;
private RedisManager redisManager;
private EventListener eventListener;
private DataAdapter dataAdapter;
@Setter
private DataSyncer dataSyncer;
@Setter
private Settings settings;
@Setter
private Locales locales;
@Setter
@Getter(AccessLevel.NONE)
private Server serverName;
@Override
public void onInitializeServer() {
// Get the logger and mod container
this.logger = LoggerFactory.getLogger("HuskSync");
this.mod = FabricLoader.getInstance().getModContainer("husksync").orElseThrow();
this.disabling = false;
this.gson = createGson();
// Load settings and locales
initialize("plugin config & locale files", (plugin) -> {
loadSettings();
loadLocales();
loadServer();
});
// Register commands
initialize("commands", (plugin) -> getUniform().register(PluginCommand.Type.create(this)));
// Load HuskSync after server startup
ServerLifecycleEvents.SERVER_STARTED.register(server -> {
this.minecraftServer = server;
this.onEnable();
});
// Unload HuskSync before server shutdown
ServerLifecycleEvents.SERVER_STOPPING.register(server -> this.onDisable());
}
private void onEnable() {
// Initial plugin setup
this.audiences = FabricServerAudiences.of(minecraftServer);
// Prepare data adapter
initialize("data adapter", (plugin) -> {
if (getSettings().getSynchronization().isCompressData()) {
this.dataAdapter = new SnappyGsonAdapter(this);
} else {
this.dataAdapter = new GsonAdapter(this);
}
});
initialize("data serializers", (plugin) -> {
// PERSISTENT_DATA is not registered / available on the Fabric platform
registerSerializer(Identifier.INVENTORY, new FabricSerializer.Inventory(this));
registerSerializer(Identifier.ENDER_CHEST, new FabricSerializer.EnderChest(this));
registerSerializer(Identifier.ADVANCEMENTS, new FabricSerializer.Advancements(this));
registerSerializer(Identifier.STATISTICS, new Serializer.Json<>(this, FabricData.Statistics.class));
registerSerializer(Identifier.POTION_EFFECTS, new FabricSerializer.PotionEffects(this));
registerSerializer(Identifier.GAME_MODE, new Serializer.Json<>(this, FabricData.GameMode.class));
registerSerializer(Identifier.FLIGHT_STATUS, new Serializer.Json<>(this, FabricData.FlightStatus.class));
registerSerializer(Identifier.ATTRIBUTES, new Serializer.Json<>(this, FabricData.Attributes.class));
registerSerializer(Identifier.HEALTH, new Serializer.Json<>(this, FabricData.Health.class));
registerSerializer(Identifier.HUNGER, new Serializer.Json<>(this, FabricData.Hunger.class));
registerSerializer(Identifier.EXPERIENCE, new Serializer.Json<>(this, FabricData.Experience.class));
registerSerializer(Identifier.LOCATION, new Serializer.Json<>(this, FabricData.Location.class));
validateDependencies();
});
// Initialize the database
initialize(getSettings().getDatabase().getType().getDisplayName() + " database connection", (plugin) -> {
this.database = switch (settings.getDatabase().getType()) {
case MYSQL, MARIADB -> new MySqlDatabase(this);
case POSTGRES -> new PostgresDatabase(this);
case MONGO -> new MongoDbDatabase(this);
};
this.database.initialize();
});
// Prepare redis connection
initialize("Redis server connection", (plugin) -> {
this.redisManager = new RedisManager(this);
this.redisManager.initialize();
});
// Prepare data syncer
initialize("data syncer", (plugin) -> {
dataSyncer = getSettings().getSynchronization().getMode().create(this);
dataSyncer.initialize();
});
// Register events
initialize("events", (plugin) -> this.eventListener = new FabricEventListener(this));
// Register plugin hooks
initialize("hooks", (plugin) -> {
if (isDependencyLoaded("Plan") && getSettings().isEnablePlanHook()) {
new PlanHook(this).hookIntoPlan();
}
});
// Register API
initialize("api", (plugin) -> FabricHuskSyncAPI.register(this));
// Check for updates
this.checkForUpdates();
log(Level.WARNING, """
**************
WARNING:
HuskSync for Fabric is still in an alpha state and is
not considered production ready.
**************""");
ModLoadedCallback.EVENT.invoker().post(FabricHuskSyncAPI.getInstance());
}
private void onDisable() {
// Handle shutdown
this.disabling = true;
// Close the event listener / data syncer
if (this.dataSyncer != null) {
this.dataSyncer.terminate();
}
if (this.eventListener != null) {
this.eventListener.handlePluginDisable();
}
// Cancel tasks, close audiences
if (audiences != null) {
this.audiences.close();
}
this.cancelTasks();
// Complete shutdown
log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion());
}
@NotNull
@Override
public String getServerName() {
return serverName.getName();
}
@Override
public boolean isDependencyLoaded(@NotNull String name) {
return FabricLoader.getInstance().isModLoaded(name);
}
@Override
@NotNull
public Set<OnlineUser> getOnlineUsers() {
return Sets.newHashSet(playerMap.values());
}
@Override
@NotNull
public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
return Optional.ofNullable(playerMap.get(uuid));
}
@Override
@NotNull
public Uniform getUniform() {
return FabricUniform.getInstance(mod.getMetadata().getId());
}
@Override
@Nullable
public InputStream getResource(@NotNull String name) {
return this.mod.findPath(name)
.map(path -> {
try {
return Files.newInputStream(path);
} catch (IOException e) {
log(Level.WARNING, "Failed to load resource: " + name, e);
}
return null;
})
.orElse(this.getClass().getClassLoader().getResourceAsStream(name));
}
@Override
@NotNull
public Path getConfigDirectory() {
final Path path = FabricLoader.getInstance().getConfigDir().resolve("husksync");
if (!Files.isDirectory(path)) {
try {
Files.createDirectory(path);
} catch (IOException e) {
log(Level.SEVERE, "Failed to create config directory", e);
}
}
return path;
}
@Override
public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable) {
LoggingEventBuilder logEvent = logger.makeLoggingEventBuilder(
switch (level.getName()) {
case "WARNING" -> org.slf4j.event.Level.WARN;
case "SEVERE" -> org.slf4j.event.Level.ERROR;
default -> org.slf4j.event.Level.INFO;
}
);
if (throwable.length >= 1) {
logEvent = logEvent.setCause(throwable[0]);
}
logEvent.log(message);
}
@NotNull
@Override
public ConsoleUser getConsole() {
return new ConsoleUser(audiences);
}
@Override
@NotNull
public Version getPluginVersion() {
return Version.fromString(mod.getMetadata().getVersion().getFriendlyString(), "-");
}
@Override
@NotNull
public Version getMinecraftVersion() {
return Version.fromString(minecraftServer.getVersion());
}
@NotNull
@Override
public String getPlatformType() {
return PLATFORM_TYPE_ID;
}
@Override
@NotNull
public String getServerVersion() {
return String.format("%s %s/%s", getPlatformType(), FabricLoader.getInstance()
.getModContainer("fabricloader").map(l -> l.getMetadata().getVersion().getFriendlyString())
.orElse("unknown"), minecraftServer.getVersion());
}
@Override
public Optional<LegacyConverter> getLegacyConverter() {
return Optional.empty();
}
@Override
@NotNull
public FabricHuskSync getPlugin() {
return this;
}
}

View File

@@ -0,0 +1,260 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.api;
import net.minecraft.item.ItemStack;
import net.minecraft.server.network.ServerPlayerEntity;
import net.william278.desertwell.util.ThrowingConsumer;
import net.william278.husksync.FabricHuskSync;
import net.william278.husksync.data.DataHolder;
import net.william278.husksync.data.FabricData;
import net.william278.husksync.user.FabricUser;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
/**
* The HuskSync API implementation for the Fabric platform
* </p>
* Retrieve an instance of the API class via {@link #getInstance()}.
*/
@SuppressWarnings("unused")
public class FabricHuskSyncAPI extends HuskSyncAPI {
/**
* <b>(Internal use only)</b> - Constructor, instantiating the API.
*/
@ApiStatus.Internal
private FabricHuskSyncAPI(@NotNull FabricHuskSync plugin) {
super(plugin);
}
/**
* Entrypoint to the HuskSync API on the Fabric platform - returns an instance of the API
*
* @return instance of the HuskSync API
* @since 3.6
*/
@NotNull
public static FabricHuskSyncAPI getInstance() {
if (instance == null) {
throw new NotRegisteredException();
}
return (FabricHuskSyncAPI) instance;
}
/**
* <b>(Internal use only)</b> - Register the API for this platform.
*
* @param plugin the plugin instance
* @since 3.6
*/
@ApiStatus.Internal
public static void register(@NotNull FabricHuskSync plugin) {
instance = new FabricHuskSyncAPI(plugin);
}
/**
* Returns a {@link OnlineUser} instance for the given Fabric {@link ServerPlayerEntity}.
*
* @param player the Fabric player to get the {@link OnlineUser} instance for
* @return the {@link OnlineUser} instance for the given Fabric {@link ServerPlayerEntity}
* @since 2.0
*/
@NotNull
public FabricUser getUser(@NotNull ServerPlayerEntity player) {
return FabricUser.adapt(player, plugin);
}
/**
* Get the current {@link FabricData.Items.Inventory} of the given {@link User}
*
* @param user the user to get the inventory of
* @return the {@link FabricData.Items.Inventory} of the given {@link User}
* @since 3.6
*/
public CompletableFuture<Optional<FabricData.Items.Inventory>> getCurrentInventory(@NotNull User user) {
return getCurrentData(user).thenApply(data -> data.flatMap(DataHolder::getInventory)
.map(FabricData.Items.Inventory.class::cast));
}
/**
* Get the current {@link FabricData.Items.Inventory} of the given {@link ServerPlayerEntity}
*
* @param user the user to get the inventory of
* @return the {@link FabricData.Items.Inventory} of the given {@link ServerPlayerEntity}
* @since 3.6
*/
public CompletableFuture<Optional<ItemStack[]>> getCurrentInventoryContents(@NotNull User user) {
return getCurrentInventory(user)
.thenApply(inventory -> inventory.map(FabricData.Items.Inventory::getContents));
}
/**
* Set the current {@link FabricData.Items.Inventory} of the given {@link User}
*
* @param user the user to set the inventory of
* @param contents the contents to set the inventory to
* @since 3.6
*/
public void setCurrentInventory(@NotNull User user, @NotNull FabricData.Items.Inventory contents) {
editCurrentData(user, dataHolder -> dataHolder.setInventory(contents));
}
/**
* Set the current {@link FabricData.Items.Inventory} of the given {@link User}
*
* @param user the user to set the inventory of
* @param contents the contents to set the inventory to
* @since 3.6
*/
public void setCurrentInventoryContents(@NotNull User user, @NotNull ItemStack[] contents) {
editCurrentData(
user,
dataHolder -> dataHolder.getInventory().ifPresent(
inv -> inv.setContents(adaptItems(contents))
)
);
}
/**
* Edit the current {@link FabricData.Items.Inventory} of the given {@link User}
*
* @param user the user to edit the inventory of
* @param editor the editor to apply to the inventory
* @since 3.6
*/
public void editCurrentInventory(@NotNull User user, ThrowingConsumer<FabricData.Items.Inventory> editor) {
editCurrentData(user, dataHolder -> dataHolder.getInventory()
.map(FabricData.Items.Inventory.class::cast)
.ifPresent(editor));
}
/**
* Edit the current {@link FabricData.Items.Inventory} of the given {@link User}
*
* @param user the user to edit the inventory of
* @param editor the editor to apply to the inventory
* @since 3.6
*/
public void editCurrentInventoryContents(@NotNull User user, ThrowingConsumer<ItemStack[]> editor) {
editCurrentData(user, dataHolder -> dataHolder.getInventory()
.map(FabricData.Items.Inventory.class::cast)
.ifPresent(inventory -> editor.accept(inventory.getContents())));
}
/**
* Get the current {@link FabricData.Items.EnderChest} of the given {@link User}
*
* @param user the user to get the ender chest of
* @return the {@link FabricData.Items.EnderChest} of the given {@link User}, or {@link Optional#empty()} if the
* user data could not be found
* @since 3.6
*/
public CompletableFuture<Optional<FabricData.Items.EnderChest>> getCurrentEnderChest(@NotNull User user) {
return getCurrentData(user).thenApply(data -> data.flatMap(DataHolder::getEnderChest)
.map(FabricData.Items.EnderChest.class::cast));
}
/**
* Get the current {@link FabricData.Items.EnderChest} of the given {@link ServerPlayerEntity}
*
* @param user the user to get the ender chest of
* @return the {@link FabricData.Items.EnderChest} of the given {@link ServerPlayerEntity}, or {@link Optional#empty()} if the
* user data could not be found
* @since 3.6
*/
public CompletableFuture<Optional<ItemStack[]>> getCurrentEnderChestContents(@NotNull User user) {
return getCurrentEnderChest(user)
.thenApply(enderChest -> enderChest.map(FabricData.Items.EnderChest::getContents));
}
/**
* Set the current {@link FabricData.Items.EnderChest} of the given {@link User}
*
* @param user the user to set the ender chest of
* @param contents the contents to set the ender chest to
* @since 3.6
*/
public void setCurrentEnderChest(@NotNull User user, @NotNull FabricData.Items.EnderChest contents) {
editCurrentData(user, dataHolder -> dataHolder.setEnderChest(contents));
}
/**
* Set the current {@link FabricData.Items.EnderChest} of the given {@link User}
*
* @param user the user to set the ender chest of
* @param contents the contents to set the ender chest to
* @since 3.6
*/
public void setCurrentEnderChestContents(@NotNull User user, @NotNull ItemStack[] contents) {
editCurrentData(
user,
dataHolder -> dataHolder.getEnderChest().ifPresent(
enderChest -> enderChest.setContents(adaptItems(contents))
)
);
}
/**
* Edit the current {@link FabricData.Items.EnderChest} of the given {@link User}
*
* @param user the user to edit the ender chest of
* @param editor the editor to apply to the ender chest
* @since 3.6
*/
public void editCurrentEnderChest(@NotNull User user, Consumer<FabricData.Items.EnderChest> editor) {
editCurrentData(user, dataHolder -> dataHolder.getEnderChest()
.map(FabricData.Items.EnderChest.class::cast)
.ifPresent(editor));
}
/**
* Edit the current {@link FabricData.Items.EnderChest} of the given {@link User}
*
* @param user the user to edit the ender chest of
* @param editor the editor to apply to the ender chest
* @since 3.6
*/
public void editCurrentEnderChestContents(@NotNull User user, Consumer<ItemStack[]> editor) {
editCurrentData(user, dataHolder -> dataHolder.getEnderChest()
.map(FabricData.Items.EnderChest.class::cast)
.ifPresent(enderChest -> editor.accept(enderChest.getContents())));
}
/**
* Adapts an array of {@link ItemStack} to a {@link FabricData.Items} instance
*
* @param contents the contents to adapt
* @return the adapted {@link FabricData.Items} instance
* @since 3.6
*/
@NotNull
public FabricData.Items adaptItems(@NotNull ItemStack[] contents) {
return FabricData.Items.ItemArray.adapt(contents);
}
}

View File

@@ -0,0 +1,806 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.data;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gson.annotations.SerializedName;
import lombok.*;
import net.fabricmc.fabric.api.dimension.v1.FabricDimensions;
import net.minecraft.advancement.AdvancementProgress;
import net.minecraft.advancement.PlayerAdvancementTracker;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.entity.attribute.EntityAttribute;
import net.minecraft.entity.attribute.EntityAttributeInstance;
import net.minecraft.entity.attribute.EntityAttributeModifier;
import net.minecraft.entity.effect.StatusEffect;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.entity.player.HungerManager;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.stat.StatType;
import net.minecraft.stat.Stats;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.TeleportTarget;
import net.william278.desertwell.util.ThrowingConsumer;
import net.william278.husksync.FabricHuskSync;
import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.Adaptable;
import net.william278.husksync.user.FabricUser;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable;
import java.util.*;
import static net.william278.husksync.util.FabricKeyedAdapter.*;
public abstract class FabricData implements Data {
@Override
public void apply(@NotNull UserDataHolder user, @NotNull HuskSync plugin) {
this.apply((FabricUser) user, (FabricHuskSync) plugin);
}
protected abstract void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin);
@Getter
public static abstract class Items extends FabricData implements Data.Items {
private final @Nullable ItemStack @NotNull [] contents;
private Items(@Nullable ItemStack @NotNull [] contents) {
this.contents = Arrays.stream(contents.clone())
.map(i -> i == null || i.isEmpty() ? null : i)
.toArray(ItemStack[]::new);
}
@Nullable
@Override
public Stack @NotNull [] getStack() {
return Arrays.stream(contents)
.map(stack -> stack != null ? new Stack(
stack.getItem().toString(),
stack.getCount(),
stack.getName().getString(),
Optional.ofNullable(stack.getSubNbt(ItemStack.DISPLAY_KEY))
.flatMap(display -> Optional.ofNullable(display.get(ItemStack.LORE_KEY))
.map(lore -> ((List<String>) lore).stream().toList())) //todo check this is ok
.orElse(null),
stack.getEnchantments().stream()
.map(element -> EnchantmentHelper.getIdFromNbt((NbtCompound) element))
.filter(Objects::nonNull).map(Identifier::toString)
.toList()
) : null)
.toArray(Stack[]::new);
}
@Override
public void clear() {
Arrays.fill(contents, null);
}
@Override
public void setContents(@NotNull Data.Items contents) {
this.setContents(((FabricData.Items) contents).getContents());
}
public void setContents(@Nullable ItemStack @NotNull [] contents) {
// Ensure the array is the correct length for the inventory
if (contents.length != this.contents.length) {
contents = Arrays.copyOf(contents, this.contents.length);
}
System.arraycopy(contents, 0, this.contents, 0, this.contents.length);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof FabricData.Items items) {
return Arrays.equals(contents, items.getContents());
}
return false;
}
@Setter
@Getter
public static class Inventory extends FabricData.Items implements Data.Items.Inventory {
@Range(from = 0, to = 8)
private int heldItemSlot;
public Inventory(@Nullable ItemStack @NotNull [] contents, int heldItemSlot) {
super(contents);
this.heldItemSlot = heldItemSlot;
}
@NotNull
public static FabricData.Items.Inventory from(@Nullable ItemStack @NotNull [] contents, int heldItemSlot) {
return new FabricData.Items.Inventory(contents, heldItemSlot);
}
@NotNull
public static FabricData.Items.Inventory from(@NotNull Collection<ItemStack> contents, int heldItemSlot) {
return from(contents.toArray(ItemStack[]::new), heldItemSlot);
}
@NotNull
public static FabricData.Items.Inventory empty() {
return new FabricData.Items.Inventory(new ItemStack[INVENTORY_SLOT_COUNT], 0);
}
@Override
public int getSlotCount() {
return getContents().length;
}
@Override
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
final ServerPlayerEntity player = user.getPlayer();
player.playerScreenHandler.clearCraftingSlots();
player.currentScreenHandler.setCursorStack(ItemStack.EMPTY);
final ItemStack[] items = getContents();
for (int slot = 0; slot < player.getInventory().size(); slot++) {
player.getInventory().setStack(slot, items[slot] == null ? ItemStack.EMPTY : items[slot]);
}
player.getInventory().selectedSlot = heldItemSlot;
player.playerScreenHandler.sendContentUpdates();
player.getInventory().updateItems();
}
}
public static class EnderChest extends FabricData.Items implements Data.Items.EnderChest {
private EnderChest(@Nullable ItemStack @NotNull [] contents) {
super(contents);
}
@NotNull
public static FabricData.Items.EnderChest adapt(@Nullable ItemStack @NotNull [] contents) {
return new FabricData.Items.EnderChest(contents);
}
@NotNull
public static FabricData.Items.EnderChest adapt(@NotNull Collection<ItemStack> items) {
return adapt(items.toArray(ItemStack[]::new));
}
@NotNull
public static FabricData.Items.EnderChest empty() {
return new FabricData.Items.EnderChest(new ItemStack[ENDER_CHEST_SLOT_COUNT]);
}
@Override
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
final ItemStack[] items = getContents();
for (int slot = 0; slot < user.getPlayer().getEnderChestInventory().size(); slot++) {
user.getPlayer().getEnderChestInventory().setStack(
slot, items[slot] == null ? ItemStack.EMPTY : items[slot]
);
}
}
}
public static class ItemArray extends FabricData.Items implements Data.Items {
private ItemArray(@Nullable ItemStack @NotNull [] contents) {
super(contents);
}
@NotNull
public static ItemArray adapt(@NotNull Collection<ItemStack> drops) {
return new ItemArray(drops.toArray(ItemStack[]::new));
}
@NotNull
public static ItemArray adapt(@Nullable ItemStack @NotNull [] drops) {
return new ItemArray(drops);
}
@Override
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
throw new UnsupportedOperationException("A generic item array cannot be applied to a player");
}
}
}
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class PotionEffects extends FabricData implements Data.PotionEffects {
private final Collection<StatusEffectInstance> effects;
@NotNull
public static FabricData.PotionEffects from(@NotNull Collection<StatusEffectInstance> sei) {
return new FabricData.PotionEffects(Lists.newArrayList(sei.stream().filter(e -> !e.isAmbient()).toList()));
}
@NotNull
public static FabricData.PotionEffects adapt(@NotNull Collection<Effect> effects) {
return from(effects.stream()
.map(effect -> {
final StatusEffect type = matchEffectType(effect.type());
return type != null ? new StatusEffectInstance(
type,
effect.duration(),
effect.amplifier(),
effect.isAmbient(),
effect.showParticles(),
effect.hasIcon()
) : null;
})
.filter(Objects::nonNull)
.toList()
);
}
@NotNull
@SuppressWarnings("unused")
public static FabricData.PotionEffects empty() {
return new FabricData.PotionEffects(Lists.newArrayList());
}
@Override
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
final ServerPlayerEntity player = user.getPlayer();
final List<StatusEffect> effectsToRemove = player.getActiveStatusEffects().entrySet().stream()
.filter(e -> !e.getValue().isAmbient()).map(Map.Entry::getKey).toList();
effectsToRemove.forEach(player::removeStatusEffect);
getEffects().forEach(player::addStatusEffect);
}
@NotNull
@Override
@Unmodifiable
public List<Effect> getActiveEffects() {
return effects.stream()
.map(potionEffect -> {
final String key = getEffectId(potionEffect.getEffectType());
return key != null ? new Effect(
key,
potionEffect.getAmplifier(),
potionEffect.getDuration(),
potionEffect.isAmbient(),
potionEffect.shouldShowParticles(),
potionEffect.shouldShowIcon()
) : null;
})
.filter(Objects::nonNull)
.toList();
}
}
@Getter
@Setter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Advancements extends FabricData implements Data.Advancements {
private List<Advancement> completed;
@NotNull
public static FabricData.Advancements adapt(@NotNull ServerPlayerEntity player) {
final MinecraftServer server = Objects.requireNonNull(player.getServer(), "Server is null");
final List<Advancement> advancements = Lists.newArrayList();
forEachAdvancement(server, advancement -> {
final AdvancementProgress advancementProgress = player.getAdvancementTracker().getProgress(advancement);
final Map<String, Date> awardedCriteria = Maps.newHashMap();
advancementProgress.getObtainedCriteria().forEach((criteria) -> awardedCriteria.put(criteria,
advancementProgress.getEarliestProgressObtainDate()));
// Only save the advancement if criteria has been completed
if (!awardedCriteria.isEmpty()) {
advancements.add(Advancement.adapt(advancement.getId().toString(), awardedCriteria));
}
});
return new FabricData.Advancements(advancements);
}
@NotNull
public static FabricData.Advancements from(@NotNull List<Advancement> advancements) {
return new FabricData.Advancements(advancements);
}
@Override
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
final ServerPlayerEntity player = user.getPlayer();
final MinecraftServer server = Objects.requireNonNull(player.getServer(), "Server is null");
plugin.runAsync(() -> forEachAdvancement(server, advancement -> {
final AdvancementProgress progress = player.getAdvancementTracker().getProgress(advancement);
final Optional<Advancement> record = completed.stream()
.filter(r -> r.getKey().equals(advancement.getId().toString()))
.findFirst();
if (record.isEmpty()) {
return;
}
final Map<String, Date> criteria = record.get().getCompletedCriteria();
final List<String> awarded = Lists.newArrayList(progress.getObtainedCriteria());
this.setAdvancement(
plugin, advancement, player, user,
criteria.keySet().stream().filter(key -> !awarded.contains(key)).toList(),
awarded.stream().filter(key -> !criteria.containsKey(key)).toList()
);
}));
}
private void setAdvancement(@NotNull FabricHuskSync plugin,
@NotNull net.minecraft.advancement.Advancement advancement,
@NotNull ServerPlayerEntity player,
@NotNull FabricUser user,
@NotNull List<String> toAward,
@NotNull List<String> toRevoke) {
plugin.runSync(() -> {
// Track player exp level & progress
final int expLevel = player.experienceLevel;
final float expProgress = player.experienceProgress;
// Award and revoke advancement criteria
final PlayerAdvancementTracker progress = player.getAdvancementTracker();
toAward.forEach(a -> progress.grantCriterion(advancement, a));
toRevoke.forEach(r -> progress.revokeCriterion(advancement, r));
// Restore player exp level & progress
if (!toAward.isEmpty()
&& (player.experienceLevel != expLevel || player.experienceProgress != expProgress)) {
player.setExperienceLevel(expLevel);
player.setExperiencePoints((int) (player.getNextLevelExperience() * expProgress));
}
});
}
// Performs a consuming function for every advancement registered on the server
private static void forEachAdvancement(@NotNull MinecraftServer server,
@NotNull ThrowingConsumer<net.minecraft.advancement.Advancement> con) {
server.getAdvancementLoader().getAdvancements().forEach(con);
}
}
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class Location extends FabricData implements Data.Location, Adaptable {
@SerializedName("x")
private double x;
@SerializedName("y")
private double y;
@SerializedName("z")
private double z;
@SerializedName("yaw")
private float yaw;
@SerializedName("pitch")
private float pitch;
@SerializedName("world")
private World world;
@NotNull
public static FabricData.Location from(double x, double y, double z,
float yaw, float pitch, @NotNull World world) {
return new FabricData.Location(x, y, z, yaw, pitch, world);
}
@NotNull
public static FabricData.Location adapt(@NotNull ServerPlayerEntity player) {
return from(
player.getX(),
player.getY(),
player.getZ(),
player.getYaw(),
player.getPitch(),
new World(
Objects.requireNonNull(
player.getWorld(), "World is null"
).getRegistryKey().getValue().toString(),
UUID.nameUUIDFromBytes(
player.getWorld().getDimensionKey().getValue().toString().getBytes()
),
player.getWorld().getDimensionKey().getValue().toString()
)
);
}
@Override
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
final ServerPlayerEntity player = user.getPlayer();
final MinecraftServer server = plugin.getMinecraftServer();
try {
player.dismountVehicle();
FabricDimensions.teleport(
player,
server.getWorld(server.getWorldRegistryKeys().stream()
.filter(key -> key.getValue().equals(Identifier.tryParse(world.name())))
.findFirst().orElseThrow(
() -> new IllegalStateException("Invalid world")
)),
new TeleportTarget(
new Vec3d(x, y, z),
Vec3d.ZERO,
yaw,
pitch
)
);
} catch (Throwable e) {
throw new IllegalStateException("Failed to apply location", e);
}
}
}
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Statistics extends FabricData implements Data.Statistics, Adaptable {
private static final String BLOCK_STAT_TYPE = "block";
private static final String ITEM_STAT_TYPE = "item";
private static final String ENTITY_STAT_TYPE = "entity_type";
@SerializedName("generic")
private Map<String, Integer> genericStatistics;
@SerializedName("blocks")
private Map<String, Map<String, Integer>> blockStatistics;
@SerializedName("items")
private Map<String, Map<String, Integer>> itemStatistics;
@SerializedName("entities")
private Map<String, Map<String, Integer>> entityStatistics;
@NotNull
public static FabricData.Statistics adapt(@NotNull ServerPlayerEntity player) throws IllegalStateException {
// Adapt typed stats
final Map<String, Map<String, Integer>> blocks = Maps.newHashMap(),
items = Maps.newHashMap(), entities = Maps.newHashMap();
Registries.STAT_TYPE.getEntrySet().forEach(stat -> {
final Registry<?> registry = stat.getValue().getRegistry();
final String registryId = registry.getKey().getValue().getPath();
if (registryId.equals("custom_stat")) {
return;
}
final Map<String, Integer> map = (switch (registryId) {
case BLOCK_STAT_TYPE -> blocks;
case ITEM_STAT_TYPE -> items;
case ENTITY_STAT_TYPE -> entities;
default -> throw new IllegalStateException("Unexpected value: %s".formatted(registryId));
}).compute(stat.getKey().getValue().toString(), (k, v) -> v == null ? Maps.newHashMap() : v);
registry.getEntrySet().forEach(entry -> {
@SuppressWarnings({"unchecked", "rawtypes"}) final int value = player.getStatHandler()
.getStat((StatType) stat.getValue(), entry.getValue());
if (value != 0) {
map.put(entry.getKey().getValue().toString(), value);
}
});
});
// Add generic stats
final Map<String, Integer> generic = Maps.newHashMap();
Registries.CUSTOM_STAT.getEntrySet().forEach(stat -> {
final int value = player.getStatHandler().getStat(Stats.CUSTOM.getOrCreateStat(stat.getValue()));
if (value != 0) {
generic.put(stat.getKey().getValue().toString(), value);
}
});
return new FabricData.Statistics(generic, blocks, items, entities);
}
@NotNull
public static FabricData.Statistics from(@NotNull Map<String, Integer> generic,
@NotNull Map<String, Map<String, Integer>> blocks,
@NotNull Map<String, Map<String, Integer>> items,
@NotNull Map<String, Map<String, Integer>> entities) {
return new FabricData.Statistics(generic, blocks, items, entities);
}
@Override
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) {
final ServerPlayerEntity player = user.getPlayer();
genericStatistics.forEach((id, v) -> applyStat(player, id, null, v));
blockStatistics.forEach((id, m) -> m.forEach((b, v) -> applyStat(player, id, BLOCK_STAT_TYPE, v, b)));
itemStatistics.forEach((id, m) -> m.forEach((i, v) -> applyStat(player, id, ITEM_STAT_TYPE, v, i)));
entityStatistics.forEach((id, m) -> m.forEach((e, v) -> applyStat(player, id, ENTITY_STAT_TYPE, v, e)));
player.getStatHandler().updateStatSet();
player.getStatHandler().sendStats(player);
}
@SuppressWarnings("unchecked")
private <T> void applyStat(@NotNull ServerPlayerEntity player, @NotNull String id,
@Nullable String type, int value, @NotNull String... key) {
final Identifier statId = Identifier.tryParse(id);
if (statId == null) {
return;
}
if (type == null) {
player.getStatHandler().setStat(
player,
Stats.CUSTOM.getOrCreateStat(Registries.CUSTOM_STAT.get(statId)),
value
);
return;
}
final Identifier typeId = Identifier.tryParse(type);
final StatType<T> statType = (StatType<T>) Registries.STAT_TYPE.get(typeId);
if (statType == null) {
return;
}
final Registry<T> typeReg = statType.getRegistry();
final T typeInstance = typeReg.get(Identifier.tryParse(key[0]));
if (typeInstance == null) {
return;
}
player.getStatHandler().setStat(player, statType.getOrCreateStat(typeInstance), value);
}
}
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Attributes extends FabricData implements Data.Attributes, Adaptable {
private List<Attribute> attributes;
@NotNull
public static FabricData.Attributes adapt(@NotNull ServerPlayerEntity player, @NotNull HuskSync plugin) {
final List<Attribute> attributes = Lists.newArrayList();
Registries.ATTRIBUTE.forEach(id -> {
final EntityAttributeInstance instance = player.getAttributeInstance(id);
final Identifier key = Registries.ATTRIBUTE.getId(id);
if (instance == null || key == null) {
return;
}
final Set<Modifier> modifiers = Sets.newHashSet();
instance.getModifiers().forEach(modifier -> modifiers.add(new Modifier(
modifier.getId(),
modifier.getName(),
modifier.getValue(),
modifier.getOperation().getId(),
-1
)));
attributes.add(new Attribute(
key.toString(),
instance.getBaseValue(),
modifiers
));
});
return new FabricData.Attributes(attributes);
}
public Optional<Attribute> getAttribute(@NotNull EntityAttribute id) {
return Optional.ofNullable(Registries.ATTRIBUTE.getId(id)).map(Identifier::toString)
.flatMap(key -> attributes.stream().filter(attribute -> attribute.name().equals(key)).findFirst());
}
@SuppressWarnings("unused")
public Optional<Attribute> getAttribute(@NotNull String key) {
final EntityAttribute attribute = matchAttribute(key);
if (attribute == null) {
return Optional.empty();
}
return getAttribute(attribute);
}
@Override
protected void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) {
Registries.ATTRIBUTE.forEach(id -> applyAttribute(
user.getPlayer().getAttributeInstance(id),
getAttribute(id).orElse(null)
));
}
private static void applyAttribute(@Nullable EntityAttributeInstance instance,
@Nullable Attribute attribute) {
if (instance == null) {
return;
}
instance.setBaseValue(attribute == null ? instance.getAttribute().getDefaultValue() : attribute.baseValue());
instance.getModifiers().forEach(instance::removeModifier);
if (attribute != null) {
attribute.modifiers().forEach(modifier -> instance.addPersistentModifier(new EntityAttributeModifier(
modifier.uuid(),
modifier.name(),
modifier.amount(),
EntityAttributeModifier.Operation.fromId(modifier.operationType())
)));
}
}
}
@Getter
@Setter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Health extends FabricData implements Data.Health, Adaptable {
@SerializedName("health")
private double health;
@SerializedName("health_scale")
private double healthScale;
@SerializedName("is_health_scaled")
private boolean isHealthScaled;
@NotNull
public static FabricData.Health from(double health, double scale, boolean isScaled) {
return new FabricData.Health(health, scale, isScaled);
}
@NotNull
public static FabricData.Health adapt(@NotNull ServerPlayerEntity player) {
return from(
player.getHealth(),
20.0f, false // Health scale is a Bukkit API feature, not used in Fabric
);
}
@Override
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
final ServerPlayerEntity player = user.getPlayer();
player.setHealth((float) health);
}
}
@Getter
@Setter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Hunger extends FabricData implements Data.Hunger, Adaptable {
@SerializedName("food_level")
private int foodLevel;
@SerializedName("saturation")
private float saturation;
@SerializedName("exhaustion")
private float exhaustion;
@NotNull
public static FabricData.Hunger adapt(@NotNull ServerPlayerEntity player) {
final HungerManager hunger = player.getHungerManager();
return from(hunger.getFoodLevel(), hunger.getSaturationLevel(), hunger.getExhaustion());
}
@NotNull
public static FabricData.Hunger from(int foodLevel, float saturation, float exhaustion) {
return new FabricData.Hunger(foodLevel, saturation, exhaustion);
}
@Override
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
final ServerPlayerEntity player = user.getPlayer();
final HungerManager hunger = player.getHungerManager();
hunger.setFoodLevel(foodLevel);
hunger.setSaturationLevel(saturation);
hunger.setExhaustion(exhaustion);
}
}
@Getter
@Setter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Experience extends FabricData implements Data.Experience, Adaptable {
@SerializedName("total_experience")
private int totalExperience;
@SerializedName("exp_level")
private int expLevel;
@SerializedName("exp_progress")
private float expProgress;
@NotNull
public static FabricData.Experience from(int totalExperience, int expLevel, float expProgress) {
return new FabricData.Experience(totalExperience, expLevel, expProgress);
}
@NotNull
public static FabricData.Experience adapt(@NotNull ServerPlayerEntity player) {
return from(player.totalExperience, player.experienceLevel, player.experienceProgress);
}
@Override
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
final ServerPlayerEntity player = user.getPlayer();
player.totalExperience = totalExperience;
player.setExperienceLevel(expLevel);
player.setExperiencePoints((int) (player.getNextLevelExperience() * expProgress));
}
}
@Getter
@Setter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class GameMode extends FabricData implements Data.GameMode, Adaptable {
@SerializedName("game_mode")
private String gameMode;
@NotNull
public static FabricData.GameMode from(@NotNull String gameMode) {
return new FabricData.GameMode(gameMode);
}
@NotNull
public static FabricData.GameMode adapt(@NotNull ServerPlayerEntity player) {
return from(player.interactionManager.getGameMode().asString());
}
@Override
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
user.getPlayer().changeGameMode(net.minecraft.world.GameMode.byName(gameMode));
}
}
@Getter
@Setter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class FlightStatus extends FabricData implements Data.FlightStatus, Adaptable {
@SerializedName("allow_flight")
private boolean allowFlight;
@SerializedName("is_flying")
private boolean flying;
@NotNull
public static FabricData.FlightStatus from(boolean allowFlight, boolean flying) {
return new FabricData.FlightStatus(allowFlight, allowFlight && flying);
}
@NotNull
public static FabricData.FlightStatus adapt(@NotNull ServerPlayerEntity player) {
return from(player.getAbilities().allowFlying, player.getAbilities().flying);
}
@Override
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
final ServerPlayerEntity player = user.getPlayer();
player.getAbilities().allowFlying = allowFlight;
player.getAbilities().flying = allowFlight && flying;
player.sendAbilitiesUpdate();
}
}
}

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