mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-21 15:49:20 +00:00
Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
390a77b407 | ||
|
|
04ab9d14f8 | ||
|
|
3a32d481c4 | ||
|
|
8edbc029f8 | ||
|
|
258356e45d | ||
|
|
e1628b6448 | ||
|
|
fa32e97564 | ||
|
|
8080d57645 | ||
|
|
0a2f7b6cd4 | ||
|
|
26a2366876 | ||
|
|
2690ab3144 | ||
|
|
18b96944e9 | ||
|
|
3282f5739c | ||
|
|
50e66be0c0 | ||
|
|
593c88c8ba | ||
|
|
2f700b2d93 | ||
|
|
d1c95030f0 | ||
|
|
1ed2414241 | ||
|
|
8847483ff8 | ||
|
|
31552f85e4 | ||
|
|
125f142cf5 | ||
|
|
dc3882e47e | ||
|
|
dafbcad10e | ||
|
|
d1085ca7bd | ||
|
|
4663842946 | ||
|
|
e4262abfd7 | ||
|
|
fc6a760848 | ||
|
|
e03a580870 | ||
|
|
112e5fe0bd | ||
|
|
ae4f005a9c | ||
|
|
d1432ebb31 | ||
|
|
460cb54a7d | ||
|
|
ebf5b77f00 | ||
|
|
33904d82d0 | ||
|
|
10b3eb5a43 | ||
|
|
7ae0709895 | ||
|
|
9d6da91a5e | ||
|
|
268c351a95 | ||
|
|
8760fcea1f | ||
|
|
60a3bba165 | ||
|
|
082b3e6c42 | ||
|
|
221baa7b04 | ||
|
|
8b7b32906e | ||
|
|
261b9cc00c | ||
|
|
654e1f0855 | ||
|
|
fe14b4db35 | ||
|
|
c94ed4926f | ||
|
|
b0a37ddb04 | ||
|
|
e78e61b084 | ||
|
|
f849336435 | ||
|
|
f51e05061b | ||
|
|
b0b39e684c | ||
|
|
66af3065e3 | ||
|
|
12bac4011c | ||
|
|
02c64a54c2 | ||
|
|
9534a8ed0c | ||
|
|
8552598c6e | ||
|
|
41399b39b1 | ||
|
|
17086e51a9 | ||
|
|
4e479029a3 | ||
|
|
51faa6acb2 | ||
|
|
60a435aa82 | ||
|
|
e34fa07eb9 | ||
|
|
0520cc6ad0 | ||
|
|
c931910fc0 | ||
|
|
2bee9561d7 | ||
|
|
feb6280fd2 | ||
|
|
8f396273c7 | ||
|
|
5d584581f0 | ||
|
|
5fe9085483 | ||
|
|
c2e0c605f8 | ||
|
|
038150cff7 | ||
|
|
e19a82ef82 | ||
|
|
2aba652793 | ||
|
|
12d69e96de | ||
|
|
143bbfc4f8 | ||
|
|
5ce68458aa | ||
|
|
4494f4ee27 | ||
|
|
e550ad9156 | ||
|
|
68ed7248a1 | ||
|
|
2254c36bb4 | ||
|
|
b5789c04ec | ||
|
|
b35408c429 | ||
|
|
ac3f179321 | ||
|
|
3323418b74 | ||
|
|
22b7648b77 | ||
|
|
b5f447b20a | ||
|
|
086c235323 | ||
|
|
fbf9f7f2b1 | ||
|
|
28c4cfb55f | ||
|
|
b0363d10ed | ||
|
|
723c79b3a9 | ||
|
|
ff1c8cddb5 | ||
|
|
54553069bf | ||
|
|
bc9d31abc8 | ||
|
|
e5e848126a | ||
|
|
b0e0b9c435 | ||
|
|
6bfbeec74d | ||
|
|
745c420fed | ||
|
|
e5422d66f0 | ||
|
|
2e7ed6d9f5 | ||
|
|
d1e9f858fe | ||
|
|
0fce3c44ab | ||
|
|
3d29d45d8a | ||
|
|
725cc2b24b | ||
|
|
94e7fe61cc | ||
|
|
96c6a878c4 | ||
|
|
f650db4438 | ||
|
|
b7709f2d6c | ||
|
|
1c9d74f925 | ||
|
|
fd08a3e7d0 | ||
|
|
1829526aa7 | ||
|
|
f2d4bec138 | ||
|
|
948887c90f | ||
|
|
d78dd42b72 | ||
|
|
38c261871a | ||
|
|
9471e0cbff |
32
.github/workflows/java_ci.yml
vendored
Normal file
32
.github/workflows/java_ci.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
|
||||
|
||||
name: Java CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 16
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '16'
|
||||
distribution: 'temurin'
|
||||
- name: Build with Gradle
|
||||
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
|
||||
with:
|
||||
arguments: test
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "mvn-installing mpdbdataconverter..."
|
||||
curl "-L" "-O" "https://github.com/WiIIiam278/MPDBDataConverter/releases/download/1.0/mpdbdataconverter-1.0.jar"
|
||||
mvn "install:install-file" "-Dfile=mpdbdataconverter-1.0.jar" "-DgroupId=net.william278" "-DartifactId=mpdbdataconverter" "-Dversion=1.0" "-Dpackaging=jar" "-DgeneratePom=true" "-e"
|
||||
258
README.md
258
README.md
@@ -1,229 +1,61 @@
|
||||
[](https://github.com/WiIIiam278/HuskSync)
|
||||
# HuskSync
|
||||
# [](https://github.com/WiIIiam278/HuskSync)
|
||||

|
||||
[](https://discord.gg/tVYhJfyDWG)
|
||||
|
||||
**HuskSync** is a modern, cross-server player data synchronisation system that allows player data (inventories, health, hunger & status effects) to be synchronised across servers through the use of **Redis**.
|
||||
[Documentation, Guides & API](https://william278.net/docs/husksync/Home) · [Resource Page](https://www.spigotmc.org/resources/husksync.97144/) · [Bug Reports](https://github.com/WiIIiam278/HuskSync/issues)
|
||||
|
||||
## Disclaimer
|
||||
This source code is provided as reference to licensed individuals that have purchased the HuskSync plugin once from any of the official sources it is provided. The availability of this code does not grant you the rights to re-distribute, compile or share this source code outside this intended purpose.
|
||||
**HuskSync** is a modern, cross-server player data synchronisation system that enables the comprehensive synchronisation of your user's data across multiple proxied servers. It does this by making use of Redis and MySQL to optimally cache data while players change servers.
|
||||
|
||||
Are you a developer? [Read below for information about code bounty licensing](#Contributing).
|
||||
## Features
|
||||
- Synchronise inventories, ender chests, advancements, statistics, experience points, health, max health, hunger, saturation, potion effects, persistent data container tags, game mode, location and more across multiple proxied servers.
|
||||
- Create and manage "snapshot" backups of user data and roll back users to previous states on-the-fly. (`/userdata`)
|
||||
- Preview, list, delete, restore & pin user data snapshots in-game with an intuitive menu.
|
||||
- Examine the contents of player's inventories and ender chests on-the-fly. (`/inventory`, `/enderchest`)
|
||||
- Hooks with your [Player Analytics](https://github.com/plan-player-analytics/Plan) web panel to provide an overview of user data.
|
||||
- Supports segregating synchronisation across multiple distinct clusters on one network.
|
||||
|
||||
## Requirements
|
||||
* A MySQL Database (v8.0+).
|
||||
* A Redis Database (v5.0+)
|
||||
* Any number of proxied Spigot servers (Minecraft v1.16.5+)
|
||||
|
||||
## Setup
|
||||
### Requirements
|
||||
* A BungeeCord or Velocity-based proxy server
|
||||
* A Spigot-based game server
|
||||
* A Redis server
|
||||
1. Place the plugin jar file in the `/plugins/` directory of each Spigot server. You do not need to install HuskSync as a proxy plugin.
|
||||
2. Start, then stop every server to let HuskSync generate the config file.
|
||||
3. Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`) and fill in both the MySQL and Redis database credentials.
|
||||
4. Start every server again and synchronistaion will begin.
|
||||
|
||||
### Installation
|
||||
1. Install HuskSync in the `/plugins/` folder of both your Spigot and Proxy servers.
|
||||
2. Start your servers, then stop them again to allow the configuration files to generate.
|
||||
3. Navigate to the generated `config.yml` files on your Spigot server and Proxy (located in `/plugins/HuskSync/`) and fill in the credentials of your redis server.
|
||||
1. On the Proxy server, you can additionally configure a MySQL database to save player data in, as by default the plugin will create a SQLite database.
|
||||
3. By default, everything except player locations are synchronised. If you would like to change what gets synchronised, you can do this by editing the `config.yml` files of each Spigot server.
|
||||
4. Once you have finished setting everything up, make sure to restart all of your servers and proxy server. Then, log in and data should be synchronised!
|
||||
|
||||
### Migration from MySQLPlayerDataBridge
|
||||
HuskSync supports the migration of player data from [MySQLPlayerDataBridge](https://www.spigotmc.org/resources/mysql-player-data-bridge.8117/). Please note that HuskSync is not compatible with MySQLPlayerInventoryBridge, as that has a different system for data handling.
|
||||
|
||||
To migrate from MySQLPLayerDataBridge, you need a Proxy server with HuskSync installed and one Spigot server with both HuskSync and MySQLPlayerDataBridge installed. To migrate:
|
||||
1. Make sure HuskSync is set up correctly on the Proxy and Spigot server, making sure that the two are able to communicate with Redis (it will display a handshake confirmation message in both consoles when communications have been established)
|
||||
2. Make sure your database is configured correctly on your Proxy server. For example, if you would like to change from SQLite to MySQL, you should do this now because the data from MySQLPlayerDataBridge will be moved into it.
|
||||
3. Make sure no players are online, then in the Proxy server's console run `husksync migrate`
|
||||
4. Follow the steps in the Migration wizard to ensure the connection credentials and details of the database containing your MySQLPlayerDataBridge are correct, changing settings with `husksync migrate setting <setting> <new value>` as necessary.
|
||||
5. Run `husksync migrate start` in the Proxy server's console to start the migration. This could take some time, depending on the amount of data that needs migrating and the speed of your database/server. When the migration is complete, it will display a "Migration complete" message.
|
||||
|
||||
### Troubleshooting
|
||||
#### Commands do not function
|
||||
Please check that the plugin is installed and enabled on both the proxy and bukkit server you are trying to execute the command from and that both plugins connected to Redis. (A connection handshake confirmation message is logged to console when communications are successfully established.)
|
||||
|
||||
#### Data not being synced on player join and SQL errors in proxy console
|
||||
This issue frequently occurs in users running Cracked (illegal) servers. I do not support piracy and so will be limited in my ability to help you.
|
||||
If you are running an offline server for a legitimate reason, however, make sure that in the `paper.yml` of your Bukkit servers `bungee-online-mode` is set to the correct value - and that both your Proxy (BungeeCord, Waterfall, etc.) server and Bukkit (Spigot, paper, etc.) servers are set up correctly to work with offline mode.
|
||||
|
||||
#### Data sometimes not syncing between servers
|
||||
There are two primary reasons this may happen:
|
||||
* On your proxy server, you are running _FlameCord_ or a similar fork of Waterfall. Due to the nature of these forks changing security parameters, they can block or interfere with Redis packets being sent to and from your server. FlameCord, XCord and other forks are not compatible with HuskSync. For security-conscious users, I recommend Velocity.
|
||||
* Your backend servers/proxy and Redis server have noticeably different amounts of latency between each other. This is particularly relevant for users running across multiple machines, where some backend servers / the proxy are installed with Redis and other backend servers are on a different machine. The solution to this is to have your BungeeCord and Redis alone on one machine, and your backend servers across the others - or have a separate machine with equal latency to the others that has Redis on. In the future, I may have a look at automatically correcting and accounting for differences in latency.
|
||||
|
||||
## How it works
|
||||

|
||||
HuskSync saves a player's data when they log out to a cache on your proxy server, and redistributes that data to players when they join another HuskSync-enabled server. Player data in the cache is then saved to a database (be it SQLite or MySQL) and this is loaded from when a player joins your network.
|
||||
|
||||
To facilitate the transfer of data between servers, HuskSync serializes player data and then makes use of Redis to communicate between the Proxy and Spigot servers.
|
||||
|
||||
### What is synchronised
|
||||
Everything except player locations are synchronised by default. You can enable or disable what data is loaded on a server by modifying these values in the `/plugins/HuskSync/config.yml` file on each Spigot server.
|
||||
* Player inventory
|
||||
* Player armour and off-hand
|
||||
* Player currently selected hotbar slot
|
||||
* Player ender chest
|
||||
* Player experience points & levels
|
||||
* Player health
|
||||
* Player max health
|
||||
* Player health scale
|
||||
* Player hunger
|
||||
* Player saturation
|
||||
* Player exhaustion
|
||||
* Player game mode
|
||||
* Player advancements
|
||||
* Player statistics (ESC → Statistics menu)
|
||||
* Player location
|
||||
* Player flight status
|
||||
|
||||
### Commands
|
||||
Commands are handled by the proxy server, rather than each spigot server. Some will only work on Spigot servers with HuskSync installed. Please remember that you will need a Proxy permission plugin (e.g. LuckPermsBungee) to set permissions for proxy commands.
|
||||
|
||||
| Command | Description | Permission |
|
||||
|---------------------------------------|--------------------------------------|--------------------------------|
|
||||
| `/husksync about` | View plugin information | _None_ |
|
||||
| `/husksync update` | Check if an update is available | `husksync.command.admin` |
|
||||
| `/husksync status` | View system status information | `husksync.command.admin` |
|
||||
| `/husksync reload` | Reload config & message files | `husksync.command.admin` |
|
||||
| `/husksync invsee <player> [cluster]` | View an offline player's inventory | `husksync.command.inventory` |
|
||||
| `/husksync echest <player> [cluster]` | View an offline player's ender chest | `husksync.command.ender_chest` |
|
||||
| `/husksync migrate [args] ` | Migrate data from MPDB | _Console-only_ |
|
||||
|
||||
### Frequently Asked Questions (FAQs)
|
||||
#### Is Redis required?
|
||||
Yes. Redis is both free, easy to install and multiplatform, though. Pterodactyl users can also run it in an egg with relatively low overheads.
|
||||
|
||||
#### What is Redis?
|
||||
Redis is server software that acts as an in-memory data store. Minecraft server software typically makes use of its function to send messages efficiently.
|
||||
|
||||
#### Is Economy / Vault synchronization supported?
|
||||
No.
|
||||
|
||||
Synchronising economy data like MySQLPlayerDataBridge does causes a number of issues and incompatibilities that mean that MySQLPlayerDataBridge has had to add integrations with a number of plugins just to make them work. This leads to poor compatibility and more bugs as plugins change their APIs and systems. In the case of HuskSync, this would require both plugin authors and myself to manually support each other, which would inevitably increase update times, lead to a bottomless pit of "add support for this plugin" requests and these integrations would then inevitably break when authors decide to update their plugins, requiring me to update manually.
|
||||
|
||||
I strongly recommend making use of economy plugins that provide built-in support for cross-server synchronisation instead, which do not have the same issues. I have personally used [XConomy](https://www.spigotmc.org/resources/xconomy.75669/) in the past and reccommend it.
|
||||
|
||||
#### Will this work on servers running multiple proxies?
|
||||
Short answer: Not right now, but improved support for this is planned in the future.
|
||||
|
||||
Long answer: This is a difficult question to unpack because of the wide variety of setups that involve multiple proxies, however currently the architecture of how messages are sent between servers assumes that one proxy will serve multiple Bukkit servers, so having multiple proxies will lead to data going out of sync, among other issues.
|
||||
|
||||
#### Does it work with Velocity?
|
||||
Yes! Servers running the Velocity proxy software are supported as of HuskSync 1.2+.
|
||||
|
||||
#### Is this faster than MySqlPlayerDataBridge (MPDB)?
|
||||
It's difficult to say, and will depend on your server.
|
||||
|
||||
MPDB stores data in a MySQL database (hence the name) and operates by querying a database for said data when a player joins a Bukkit server.
|
||||
HuskSync stores player data in a central cache on the Proxy server and servers request data from said cache; data is only queried from the database when a player joins the network, not when switching servers within it.
|
||||
|
||||
HuskSync should operate faster in theory, then, as it does not need to query large amounts of data from a database file as often. However, any performance enhancements you might see will heavily depend on the speed of your existing database and your server hardware.
|
||||
|
||||
#### Are modded items supported?
|
||||
Most likely not - and I cannot support it - but feel free to test it, as depending on the implementation of your modding API it may work just fine.
|
||||
|
||||
## Developers
|
||||
### API
|
||||
HuskSync has an API for Bukkit providing events that fire when synchronisation takes place as well as a method to access and deserialize player data on demand. There is no API for the proxy side currently.
|
||||
|
||||
HuskSync's API is available on [JitPack](https://jitpack.io/#net.william278/HuskSync/Tag). You can view the [HuskSync JavaDocs here](https://javadoc.jitpack.io/net/william278/HuskSync/latest/javadoc/index.html). You should only use stuff in the `husksync.bukkit.api` and `husksync.bukkit.data` packages (as well as the PlayerData class located in the `husksync` root package.
|
||||
|
||||
#### Including the API in your project
|
||||
With Maven, add the repository to your pom.xml:
|
||||
```xml
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>jitpack.io</id>
|
||||
<url>https://jitpack.io</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
```
|
||||
Then, add the dependency. Replace `version` with the latest version of HuskSync: [](https://jitpack.io/#net.william278/HuskSync)
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>net.william278</groupId>
|
||||
<artifactId>HuskSync</artifactId>
|
||||
<version>version</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Or, with Gradle, add the dependency like so to your build.gradle:
|
||||
```
|
||||
allprojects {
|
||||
repositories {
|
||||
...
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
```
|
||||
Then add the dependency as follows. Replace `version` with the latest version of HuskSync: [](https://jitpack.io/#net.william278/HuskSync)
|
||||
```
|
||||
dependencies {
|
||||
compileOnly 'net.william278:HuskSync:version'
|
||||
}
|
||||
```
|
||||
|
||||
#### API Events
|
||||
* **SyncCompleteEvent** - Fires when a player's data has finished synchronising. Use #getData to get the PlayerData being set.
|
||||
* **SyncEvent** - Fires just before a player's data is synchronised. Can be cancelled. Use #getData to get the PlayerData being set, and #setData to set it.
|
||||
|
||||
#### Fetching player data on demand
|
||||
To fetch PlayerData from a UUID as you need it, create an instance of the HuskSyncAPI class and use the `#getPlayerData` method. Note that data returned in this method is only the data from the central cache. That is to say, if the player is online, the data returned in this way will not necessarily be the same as the player's actual current data.
|
||||
```java
|
||||
HuskSyncAPI huskSyncApi = HuskSyncAPI.getInstance();
|
||||
try {
|
||||
CompletableFuture<PlayerData> playerDataCompletableFuture = huskSyncApi.getPlayerData(playerUUID);
|
||||
// thenAccept blocks the thread until HuskSync has grabbed the data, so you may wish to run this asynchronously (e.g. Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {});.
|
||||
playerDataCompletableFuture.thenAccept(playerData -> {
|
||||
// You now have a PlayerData object which you can get serialized data from and deserialize with the DataSerializer static methods
|
||||
});
|
||||
} catch (IOException e) {
|
||||
Bukkit.getLogger().severe("An error occurred fetching player data!");
|
||||
}
|
||||
```
|
||||
|
||||
#### Getting ItemStacks and usable data from PlayerData
|
||||
Use the static methods provided in the [DataSerializer class](https://javadoc.jitpack.io/net.william278/HuskSync/latest/javadoc/net/william278/husksync/bukkit/data/DataSerializer.html). For instance, to get a player's inventory as an `ItemStack[]` from a `PlayerData` object.
|
||||
```java
|
||||
ItemStack[] inventoryItems = DataSerializer.serializeInventory(playerData.getSerializedInventory());
|
||||
ItemStack[] enderChestItems = DataSerializer.serializeInventory(playerData.getSerializedEnderChest());
|
||||
```
|
||||
|
||||
#### Updating PlayerData
|
||||
You can then update PlayerData back to the central cache using the `HuskSyncAPI#updatePlayerData(playerData)` method. For example:
|
||||
```java
|
||||
// Update a value in the player data object
|
||||
playerData.setHealth(20);
|
||||
try {
|
||||
// Update the player data to the cache
|
||||
huskSyncApi.updatePlayerData(playerData);
|
||||
} catch (IOException e) {
|
||||
Bukkit.getLogger().severe("An error occurred updating player data!");
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Contributing
|
||||
A code bounty program is in place for HuskSync, where developers making significant code contributions to HuskSync may be entitled to a discretionary license to use HuskSync in commercial contexts without having to purchase the resource, so please feel free to submit pull requests with improvements, fixes and features!
|
||||
|
||||
### Translation
|
||||
While the code bounty program is not available for translation contributors, they are still strongly appreciated in making the plugin more accessible. If you'd like to contribute translated message strings for your language, you can submit a Pull Request that creates a .yml file in `bungeecord/src/main/resources/languages` with the correct translations.
|
||||
|
||||
### Building
|
||||
You can build HuskSync yourself, though please read the license and buy yourself a copy as HuskSync is indeed a premium resource.
|
||||
|
||||
To build HuskSync, you'll need to get the [MPDBConverter](https://github.com/WiIIiam278/MPDBDataConverter) library, either by authenticating through GitHub packages or by downloading and running `mvn install-file` to publish it to your local maven repository.
|
||||
|
||||
Then, to build the plugin, run the following in the root of the repository:
|
||||
## Building
|
||||
To build HuskSync, simply run the following in the root of the repository:
|
||||
```
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
## License
|
||||
HuskSync is a premium resource. This source code is provided as reference only for those who have purchased the resource from an official source.
|
||||
|
||||
- [License](https://github.com/WiIIiam278/HuskSync/blob/master/LICENSE)
|
||||
|
||||
## Contributing
|
||||
A code bounty program is in place for HuskSync, where developers making significant code contributions to HuskSync may be entitled to a license at my discretion to use HuskSync in commercial contexts without having to purchase the resource. Please read the information for contributors in the LICENSE file before submitting a pull request.
|
||||
|
||||
## Translation
|
||||
Translations of the plugin locales are welcome to help make the plugin more accessible. Please submit a pull request with your translations as a `.yml` file.
|
||||
|
||||
- [Locales Directory](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/locales)
|
||||
- [English Locales](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/locales/en-gb.yml)
|
||||
|
||||
## bStats
|
||||
This plugin uses bStats to provide me with metrics about its usage:
|
||||
* [View Bukkit metrics](https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140)
|
||||
* [View BungeeCord metrics](https://bstats.org/plugin/bungeecord/HuskSync%20-%20BungeeCord/13141)
|
||||
* [View Velocity metrics](https://bstats.org/plugin/velocity/HuskSync%20-%20Velocity/13489)
|
||||
- [bStats Metrics](https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140)
|
||||
|
||||
You can turn metric collection off by navigating to `~/plugins/bStats/config.yml` and editing the config to disable plugin metrics.
|
||||
|
||||
## Support
|
||||
* Report bugs: [Click here](https://github.com/WiIIiam278/HuskSync/issues)
|
||||
* Discord support: Join the [HuskHelp Discord](https://discord.gg/tVYhJfyDWG)!
|
||||
* Proof of purchase is required for support.
|
||||
## Links
|
||||
- [Documentation, Guides & API](https://william278.net/docs/husksync/Home)
|
||||
- [Resource Page](https://www.spigotmc.org/resources/husksync.97144/)
|
||||
- [Bug Reports](https://github.com/WiIIiam278/HuskSync/issues)
|
||||
- [Discord Support](https://discord.gg/tVYhJfyDWG) (Proof of purchase required)
|
||||
|
||||
---
|
||||
© [William278](https://william278.net/), 2022. All rights reserved.
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
dependencies {
|
||||
compileOnly project(path: ':common')
|
||||
implementation project(path: ':bukkit')
|
||||
compileOnly project(path: ':common')
|
||||
|
||||
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
|
||||
compileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
dependencies {
|
||||
exclude(dependency('com.mojang:brigadier'))
|
||||
}
|
||||
|
||||
relocate 'org.apache', 'net.william278.husksync.libraries'
|
||||
relocate 'dev.dejvokep', 'net.william278.husksync.libraries'
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'com.google', 'net.william278.husksync.libraries'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.json', 'net.william278.husksync.libraries.json'
|
||||
relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore'
|
||||
|
||||
relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
|
||||
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
|
||||
}
|
||||
|
||||
java {
|
||||
|
||||
203
api/src/main/java/net/william278/husksync/api/HuskSyncAPI.java
Normal file
203
api/src/main/java/net/william278/husksync/api/HuskSyncAPI.java
Normal file
@@ -0,0 +1,203 @@
|
||||
package net.william278.husksync.api;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.data.*;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* The HuskSync API implementation for the Bukkit platform, providing methods to access and modify player {@link UserData} held by {@link User}s.
|
||||
* </p>
|
||||
* Retrieve an instance of the API class via {@link #getInstance()}.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class HuskSyncAPI extends BaseHuskSyncAPI {
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Instance of the API class
|
||||
*/
|
||||
private static final HuskSyncAPI INSTANCE = new HuskSyncAPI();
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Constructor, instantiating the API
|
||||
*/
|
||||
private HuskSyncAPI() {
|
||||
super(BukkitHuskSync.getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint to the HuskSync API - returns an instance of the API
|
||||
*
|
||||
* @return instance of the HuskSync API
|
||||
*/
|
||||
public static @NotNull HuskSyncAPI getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link User} instance for the given bukkit {@link Player}.
|
||||
*
|
||||
* @param player the bukkit player to get the {@link User} instance for
|
||||
* @return the {@link User} instance for the given bukkit {@link Player}
|
||||
* @since 2.0
|
||||
*/
|
||||
@NotNull
|
||||
public OnlineUser getUser(@NotNull Player player) {
|
||||
return BukkitPlayer.adapt(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the inventory in the database of the given {@link User} to the given {@link ItemStack} contents
|
||||
*
|
||||
* @param user the {@link User} to set the inventory of
|
||||
* @param inventoryContents the {@link ItemStack} contents to set the inventory to
|
||||
* @return future returning void when complete
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<Void> setInventoryData(@NotNull User user, @NotNull ItemStack[] inventoryContents) {
|
||||
return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData ->
|
||||
userData.ifPresent(data -> serializeItemStackArray(inventoryContents)
|
||||
.thenAccept(serializedInventory -> {
|
||||
data.getInventoryData().serializedItems = serializedInventory;
|
||||
setUserData(user, data).join();
|
||||
}))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the inventory in the database of the given {@link User} to the given {@link BukkitInventoryMap} contents
|
||||
*
|
||||
* @param user the {@link User} to set the inventory of
|
||||
* @param inventoryMap the {@link BukkitInventoryMap} contents to set the inventory to
|
||||
* @return future returning void when complete
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<Void> setInventoryData(@NotNull User user, @NotNull BukkitInventoryMap inventoryMap) {
|
||||
return setInventoryData(user, inventoryMap.getContents());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Ender Chest in the database of the given {@link User} to the given {@link ItemStack} contents
|
||||
*
|
||||
* @param user the {@link User} to set the Ender Chest of
|
||||
* @param enderChestContents the {@link ItemStack} contents to set the Ender Chest to
|
||||
* @return future returning void when complete
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<Void> setEnderChestData(@NotNull User user, @NotNull ItemStack[] enderChestContents) {
|
||||
return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData ->
|
||||
userData.ifPresent(data -> serializeItemStackArray(enderChestContents)
|
||||
.thenAccept(serializedInventory -> {
|
||||
data.getEnderChestData().serializedItems = serializedInventory;
|
||||
setUserData(user, data).join();
|
||||
}))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link BukkitInventoryMap} for the given {@link User}, containing their current inventory item data
|
||||
*
|
||||
* @param user the {@link User} to get the {@link BukkitInventoryMap} for
|
||||
* @return future returning the {@link BukkitInventoryMap} for the given {@link User} if they exist,
|
||||
* otherwise an empty {@link Optional}
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<Optional<BukkitInventoryMap>> getPlayerInventory(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> getUserData(user).join()
|
||||
.map(userData -> deserializeInventory(userData
|
||||
.getInventoryData().serializedItems).join()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ItemStack}s array contents of the given {@link User}'s Ender Chest data
|
||||
*
|
||||
* @param user the {@link User} to get the Ender Chest contents of
|
||||
* @return future returning the {@link ItemStack} array of Ender Chest items for the user if they exist,
|
||||
* otherwise an empty {@link Optional}
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<Optional<ItemStack[]>> getPlayerEnderChest(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> getUserData(user).join()
|
||||
.map(userData -> deserializeItemStackArray(userData
|
||||
.getEnderChestData().serializedItems).join()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a Base-64 encoded inventory array string into a {@link ItemStack} array.
|
||||
*
|
||||
* @param serializedItemStackArray The Base-64 encoded inventory array string.
|
||||
* @return The deserialized {@link ItemStack} array.
|
||||
* @throws DataSerializationException If an error occurs during deserialization.
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<ItemStack[]> deserializeItemStackArray(@NotNull String serializedItemStackArray)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> BukkitSerializer
|
||||
.deserializeItemStackArray(serializedItemStackArray).join());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a serialized {@link ItemStack} array of player inventory contents into a {@link BukkitInventoryMap}
|
||||
*
|
||||
* @param serializedInventory The serialized {@link ItemStack} array of player inventory contents.
|
||||
* @return A {@link BukkitInventoryMap} of the deserialized {@link ItemStack} contents array
|
||||
* @throws DataSerializationException If an error occurs during deserialization.
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<BukkitInventoryMap> deserializeInventory(@NotNull String serializedInventory)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> BukkitSerializer
|
||||
.deserializeInventory(serializedInventory).join());
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize an {@link ItemStack} array into a Base-64 encoded string.
|
||||
*
|
||||
* @param itemStacks The {@link ItemStack} array to serialize.
|
||||
* @return The serialized Base-64 encoded string.
|
||||
* @throws DataSerializationException If an error occurs during serialization.
|
||||
* @see #deserializeItemStackArray(String)
|
||||
* @see ItemData
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<String> serializeItemStackArray(@NotNull ItemStack[] itemStacks)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> BukkitSerializer.serializeItemStackArray(itemStacks).join());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a Base-64 encoded potion effect array string into a {@link PotionEffect} array.
|
||||
*
|
||||
* @param serializedPotionEffectArray The Base-64 encoded potion effect array string.
|
||||
* @return The deserialized {@link PotionEffect} array.
|
||||
* @throws DataSerializationException If an error occurs during deserialization.
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<PotionEffect[]> deserializePotionEffectArray(@NotNull String serializedPotionEffectArray)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> BukkitSerializer
|
||||
.deserializePotionEffectArray(serializedPotionEffectArray).join());
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a {@link PotionEffect} array into a Base-64 encoded string.
|
||||
*
|
||||
* @param potionEffects The {@link PotionEffect} array to serialize.
|
||||
* @return The serialized Base-64 encoded string.
|
||||
* @throws DataSerializationException If an error occurs during serialization.
|
||||
* @see #deserializePotionEffectArray(String)
|
||||
* @see PotionEffectData
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<String> serializePotionEffectArray(@NotNull PotionEffect[] potionEffects)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> BukkitSerializer.serializePotionEffectArray(potionEffects).join());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package net.william278.husksync.bukkit.api;
|
||||
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.listener.BukkitRedisListener;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* HuskSync's API. To access methods, use the {@link #getInstance()} entrypoint.
|
||||
*
|
||||
* @author William
|
||||
*/
|
||||
public class HuskSyncAPI {
|
||||
|
||||
private HuskSyncAPI() {
|
||||
}
|
||||
|
||||
private static HuskSyncAPI instance;
|
||||
|
||||
/**
|
||||
* The API entry point. Returns an instance of the {@link HuskSyncAPI}
|
||||
*
|
||||
* @return instance of the {@link HuskSyncAPI}
|
||||
*/
|
||||
public static HuskSyncAPI getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new HuskSyncAPI();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link CompletableFuture} that will fetch the {@link PlayerData} for a user given their {@link UUID},
|
||||
* which contains serialized synchronised data.
|
||||
* <p>
|
||||
* This can then be deserialized into ItemStacks and other usable values using the {@code DataSerializer} class.
|
||||
* <p>
|
||||
* If no data could be returned, such as if an invalid UUID is specified, the CompletableFuture will be cancelled.
|
||||
*
|
||||
* @param playerUUID The {@link UUID} of the player to get data for
|
||||
* @return a {@link CompletableFuture} with the user's {@link PlayerData} accessible on completion
|
||||
* @throws IOException If an exception occurs with serializing during processing of the request
|
||||
* @apiNote This only returns the latest saved and cached data of the user. This is <b>not</b> necessarily the current state of their inventory if they are online.
|
||||
*/
|
||||
public CompletableFuture<PlayerData> getPlayerData(UUID playerUUID) throws IOException {
|
||||
// Create the request to be completed
|
||||
final UUID requestUUID = UUID.randomUUID();
|
||||
BukkitRedisListener.apiRequests.put(requestUUID, new CompletableFuture<>());
|
||||
|
||||
// Remove the request from the map on completion
|
||||
BukkitRedisListener.apiRequests.get(requestUUID).whenComplete((playerData, throwable) -> BukkitRedisListener.apiRequests.remove(requestUUID));
|
||||
|
||||
// Request the data via the proxy
|
||||
new RedisMessage(RedisMessage.MessageType.API_DATA_REQUEST,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
playerUUID.toString(), requestUUID.toString()).send();
|
||||
|
||||
return BukkitRedisListener.apiRequests.get(requestUUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a player's {@link PlayerData} to the proxy cache and database.
|
||||
* <p>
|
||||
* If the player is online on the Proxy network, they will be updated and overwritten with this data.
|
||||
*
|
||||
* @param playerData The {@link PlayerData} (which contains the {@link UUID}) of the player data to update to the central cache and database
|
||||
* @throws IOException If an exception occurs with serializing during processing of the update
|
||||
*/
|
||||
public void updatePlayerData(PlayerData playerData) throws IOException {
|
||||
// Serialize and send the updated player data
|
||||
final String serializedPlayerData = RedisMessage.serialize(playerData);
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
serializedPlayerData, Boolean.toString(true)).send();
|
||||
}
|
||||
|
||||
}
|
||||
52
build.gradle
52
build.gradle
@@ -1,7 +1,8 @@
|
||||
plugins {
|
||||
id 'com.github.johnrengelman.shadow' version '7.1.0'
|
||||
id 'org.ajoberstar.grgit' version '4.1.1'
|
||||
id 'com.github.johnrengelman.shadow' version '7.1.2'
|
||||
id 'org.ajoberstar.grgit' version '5.0.0'
|
||||
id 'java'
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
group 'net.william278'
|
||||
@@ -9,6 +10,9 @@ version "$ext.plugin_version+${versionMetadata()}"
|
||||
|
||||
ext {
|
||||
set 'version', version.toString()
|
||||
set 'jedis_version', jedis_version.toString()
|
||||
set 'mysql_driver_version', mysql_driver_version.toString()
|
||||
set 'snappy_version', snappy_version.toString()
|
||||
}
|
||||
|
||||
import org.apache.tools.ant.filters.ReplaceTokens
|
||||
@@ -27,18 +31,19 @@ allprojects {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
|
||||
maven { url 'https://repo.velocitypowered.com/snapshots/' }
|
||||
maven { url 'https://repo.minebench.de/' }
|
||||
maven { url 'https://repo.codemc.org/repository/maven-public' }
|
||||
maven { url 'https://repo.alessiodp.com/releases/' }
|
||||
maven { url 'https://jitpack.io' }
|
||||
maven { url 'https://libraries.minecraft.net/' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation('redis.clients:jedis:4.2.3') {
|
||||
//noinspection GroovyAssignabilityCheck
|
||||
exclude module: 'slf4j-api'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
processResources {
|
||||
@@ -51,12 +56,41 @@ subprojects {
|
||||
version rootProject.version
|
||||
archivesBaseName = "${rootProject.name}-${project.name.capitalize()}"
|
||||
|
||||
if (['bukkit', 'api', 'bungeecord', 'velocity', 'plugin'].contains(project.name)) {
|
||||
if (['bukkit', 'api', 'plugin'].contains(project.name)) {
|
||||
shadowJar {
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
archiveClassifier.set('')
|
||||
}
|
||||
jar.dependsOn shadowJar
|
||||
|
||||
// API publishing
|
||||
if ('api'.contains(project.name)) {
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
sourcesJar {
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
}
|
||||
javadocJar {
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
}
|
||||
shadowJar.dependsOn(sourcesJar, javadocJar)
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
groupId = 'net.william278'
|
||||
artifactId = 'husksync'
|
||||
version = "$rootProject.version"
|
||||
artifact shadowJar
|
||||
artifact javadocJar
|
||||
artifact sourcesJar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jar.dependsOn(shadowJar)
|
||||
clean.delete "$rootDir/target"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,35 @@
|
||||
dependencies {
|
||||
implementation project(path: ':common')
|
||||
|
||||
implementation 'org.bstats:bstats-bukkit:3.0.0'
|
||||
implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
|
||||
implementation 'net.william278:mpdbdataconverter:1.0'
|
||||
implementation 'net.william278:mpdbdataconverter:1.0.1'
|
||||
implementation 'net.william278:hsldataconverter:1.0'
|
||||
implementation 'me.lucko:commodore:2.2'
|
||||
|
||||
compileOnly 'commons-io:commons-io:2.11.0'
|
||||
compileOnly 'de.themoep:minedown:1.7.1-SNAPSHOT'
|
||||
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
|
||||
compileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
compileOnly 'dev.dejvokep:boosted-yaml:1.3'
|
||||
compileOnly 'com.zaxxer:HikariCP:5.0.1'
|
||||
|
||||
testImplementation 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.apache', 'net.william278.husksync.libraries'
|
||||
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
|
||||
dependencies {
|
||||
exclude(dependency('com.mojang:brigadier'))
|
||||
}
|
||||
|
||||
relocate 'org.apache.commons.io', 'net.william278.husksync.libraries.commons.io'
|
||||
relocate 'com.google.gson', 'net.william278.husksync.libraries.gson'
|
||||
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 'dev.dejvokep', 'net.william278.husksync.libraries'
|
||||
|
||||
relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore'
|
||||
relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
|
||||
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package me.william278.husksync.bukkit.data;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.EntityType;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Holds legacy data store methods for data storage
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("DeprecatedIsStillUsed")
|
||||
public class DataSerializer {
|
||||
|
||||
/**
|
||||
* A record used to store data for advancement synchronisation
|
||||
*
|
||||
* @deprecated Old format - Use {@link AdvancementRecordDate} instead
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("DeprecatedIsStillUsed")
|
||||
// Suppress deprecation warnings here (still used for backwards compatibility)
|
||||
public record AdvancementRecord(String advancementKey,
|
||||
ArrayList<String> awardedAdvancementCriteria) implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* A record used to store data for a player's statistics
|
||||
*/
|
||||
public record StatisticData(HashMap<Statistic, Integer> untypedStatisticValues,
|
||||
HashMap<Statistic, HashMap<Material, Integer>> blockStatisticValues,
|
||||
HashMap<Statistic, HashMap<Material, Integer>> itemStatisticValues,
|
||||
HashMap<Statistic, HashMap<EntityType, Integer>> entityStatisticValues) implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* A record used to store data for native advancement synchronisation, tracking advancement date progress
|
||||
*/
|
||||
public record AdvancementRecordDate(String key, Map<String, Date> criteriaMap) implements Serializable {
|
||||
public AdvancementRecordDate(String key, List<String> criteriaList) {
|
||||
this(key, new HashMap<>() {{
|
||||
criteriaList.forEach(s -> put(s, Date.from(Instant.EPOCH)));
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A record used to store data for a player's location
|
||||
*/
|
||||
public record PlayerLocation(double x, double y, double z, float yaw, float pitch,
|
||||
String worldName, World.Environment environment) implements Serializable {
|
||||
}
|
||||
}
|
||||
313
bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java
Normal file
313
bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java
Normal file
@@ -0,0 +1,313 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import dev.dejvokep.boostedyaml.dvs.versioning.BasicVersioning;
|
||||
import dev.dejvokep.boostedyaml.settings.dumper.DumperSettings;
|
||||
import dev.dejvokep.boostedyaml.settings.general.GeneralSettings;
|
||||
import dev.dejvokep.boostedyaml.settings.loader.LoaderSettings;
|
||||
import dev.dejvokep.boostedyaml.settings.updater.UpdaterSettings;
|
||||
import net.william278.husksync.command.BukkitCommand;
|
||||
import net.william278.husksync.command.BukkitCommandType;
|
||||
import net.william278.husksync.command.Permission;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.CompressedDataAdapter;
|
||||
import net.william278.husksync.data.DataAdapter;
|
||||
import net.william278.husksync.data.JsonDataAdapter;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.database.MySqlDatabase;
|
||||
import net.william278.husksync.editor.DataEditor;
|
||||
import net.william278.husksync.event.BukkitEventCannon;
|
||||
import net.william278.husksync.event.EventCannon;
|
||||
import net.william278.husksync.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;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.util.*;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.permissions.PermissionDefault;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BukkitHuskSync extends JavaPlugin implements HuskSync {
|
||||
|
||||
/**
|
||||
* Metrics ID for <a href="https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140">HuskSync on Bukkit</a>.
|
||||
*/
|
||||
private static final int METRICS_ID = 13140;
|
||||
private Database database;
|
||||
private RedisManager redisManager;
|
||||
private Logger logger;
|
||||
private ResourceReader resourceReader;
|
||||
private EventListener eventListener;
|
||||
private DataAdapter dataAdapter;
|
||||
private DataEditor dataEditor;
|
||||
private EventCannon eventCannon;
|
||||
private Settings settings;
|
||||
private Locales locales;
|
||||
private List<Migrator> availableMigrators;
|
||||
private static BukkitHuskSync instance;
|
||||
|
||||
/**
|
||||
* (<b>Internal use only)</b> Returns the instance of the implementing Bukkit plugin
|
||||
*
|
||||
* @return the instance of the Bukkit plugin
|
||||
*/
|
||||
public static BukkitHuskSync getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// Initialize HuskSync
|
||||
final AtomicBoolean initialized = new AtomicBoolean(true);
|
||||
try {
|
||||
// Set the logging adapter and resource reader
|
||||
this.logger = new BukkitLogger(this.getLogger());
|
||||
this.resourceReader = new BukkitResourceReader(this);
|
||||
|
||||
// Load settings and locales
|
||||
getLoggingAdapter().log(Level.INFO, "Loading plugin configuration settings & locales...");
|
||||
initialized.set(reload().join());
|
||||
if (initialized.get()) {
|
||||
logger.showDebugLogs(settings.getBooleanValue(Settings.ConfigOption.DEBUG_LOGGING));
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully loaded plugin configuration settings & locales");
|
||||
} else {
|
||||
throw new HuskSyncInitializationException("Failed to load plugin configuration settings and/or locales");
|
||||
}
|
||||
|
||||
// Prepare data adapter
|
||||
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_COMPRESS_DATA)) {
|
||||
dataAdapter = new CompressedDataAdapter();
|
||||
} else {
|
||||
dataAdapter = new JsonDataAdapter();
|
||||
}
|
||||
|
||||
// Prepare event cannon
|
||||
eventCannon = new BukkitEventCannon();
|
||||
|
||||
// Prepare data editor
|
||||
dataEditor = new DataEditor(locales);
|
||||
|
||||
// Prepare migrators
|
||||
availableMigrators = new ArrayList<>();
|
||||
availableMigrators.add(new LegacyMigrator(this));
|
||||
final Plugin mySqlPlayerDataBridge = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
|
||||
if (mySqlPlayerDataBridge != null) {
|
||||
availableMigrators.add(new MpdbMigrator(this, mySqlPlayerDataBridge));
|
||||
}
|
||||
|
||||
// Prepare database connection
|
||||
this.database = new MySqlDatabase(settings, resourceReader, logger, dataAdapter, eventCannon);
|
||||
getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the database...");
|
||||
initialized.set(this.database.initialize());
|
||||
if (initialized.get()) {
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the database");
|
||||
} else {
|
||||
throw new HuskSyncInitializationException("Failed to establish a connection to the database. " +
|
||||
"Please check the supplied database credentials in the config file");
|
||||
}
|
||||
|
||||
// Prepare redis connection
|
||||
this.redisManager = new RedisManager(this);
|
||||
getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the Redis server...");
|
||||
initialized.set(this.redisManager.initialize().join());
|
||||
if (initialized.get()) {
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the Redis server");
|
||||
} else {
|
||||
throw new HuskSyncInitializationException("Failed to establish a connection to the Redis server. " +
|
||||
"Please check the supplied Redis credentials in the config file");
|
||||
}
|
||||
|
||||
// Register events
|
||||
getLoggingAdapter().log(Level.INFO, "Registering events...");
|
||||
this.eventListener = new BukkitEventListener(this);
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully registered events listener");
|
||||
|
||||
// Register permissions
|
||||
getLoggingAdapter().log(Level.INFO, "Registering permissions & commands...");
|
||||
Arrays.stream(Permission.values()).forEach(permission -> getServer().getPluginManager()
|
||||
.addPermission(new org.bukkit.permissions.Permission(permission.node, switch (permission.defaultAccess) {
|
||||
case EVERYONE -> PermissionDefault.TRUE;
|
||||
case NOBODY -> PermissionDefault.FALSE;
|
||||
case OPERATORS -> PermissionDefault.OP;
|
||||
})));
|
||||
|
||||
// Register commands
|
||||
for (final BukkitCommandType bukkitCommandType : BukkitCommandType.values()) {
|
||||
final PluginCommand pluginCommand = getCommand(bukkitCommandType.commandBase.command);
|
||||
if (pluginCommand != null) {
|
||||
new BukkitCommand(bukkitCommandType.commandBase, this).register(pluginCommand);
|
||||
}
|
||||
}
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully registered permissions & commands");
|
||||
|
||||
// Hook into plan
|
||||
if (Bukkit.getPluginManager().getPlugin("Plan") != null) {
|
||||
getLoggingAdapter().log(Level.INFO, "Enabling Plan integration...");
|
||||
new PlanHook(database, logger).hookIntoPlan();
|
||||
getLoggingAdapter().log(Level.INFO, "Plan integration enabled!");
|
||||
}
|
||||
|
||||
// Hook into bStats metrics
|
||||
try {
|
||||
new Metrics(this, METRICS_ID);
|
||||
} catch (final Exception e) {
|
||||
getLoggingAdapter().log(Level.WARNING, "Skipped bStats metrics initialization due to an exception.");
|
||||
}
|
||||
|
||||
// Check for updates
|
||||
if (settings.getBooleanValue(Settings.ConfigOption.CHECK_FOR_UPDATES)) {
|
||||
getLoggingAdapter().log(Level.INFO, "Checking for updates...");
|
||||
CompletableFuture.runAsync(() -> new UpdateChecker(getPluginVersion(), getLoggingAdapter()).logToConsole());
|
||||
}
|
||||
} catch (HuskSyncInitializationException exception) {
|
||||
getLoggingAdapter().log(Level.SEVERE, """
|
||||
***************************************************
|
||||
|
||||
Failed to initialize HuskSync!
|
||||
|
||||
***************************************************
|
||||
The plugin was disabled due to an error. Please check
|
||||
the logs below for details.
|
||||
No user data will be synchronised.
|
||||
***************************************************
|
||||
Caused by: %error_message%
|
||||
"""
|
||||
.replaceAll("%error_message%", exception.getMessage()));
|
||||
initialized.set(false);
|
||||
} catch (Exception exception) {
|
||||
getLoggingAdapter().log(Level.SEVERE, "An unhandled exception occurred initializing HuskSync!", exception);
|
||||
initialized.set(false);
|
||||
} finally {
|
||||
// Validate initialization
|
||||
if (initialized.get()) {
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully enabled HuskSync v" + getPluginVersion());
|
||||
} else {
|
||||
getLoggingAdapter().log(Level.SEVERE, "Failed to initialize HuskSync. The plugin will now be disabled");
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (this.eventListener != null) {
|
||||
this.eventListener.handlePluginDisable();
|
||||
}
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Set<OnlineUser> getOnlineUsers() {
|
||||
return Bukkit.getOnlinePlayers().stream().map(BukkitPlayer::adapt).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
|
||||
final Player player = Bukkit.getPlayer(uuid);
|
||||
if (player == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(BukkitPlayer.adapt(player));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Database getDatabase() {
|
||||
return database;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull RedisManager getRedisManager() {
|
||||
return redisManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull DataAdapter getDataAdapter() {
|
||||
return dataAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull DataEditor getDataEditor() {
|
||||
return dataEditor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull EventCannon getEventCannon() {
|
||||
return eventCannon;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<Migrator> getAvailableMigrators() {
|
||||
return availableMigrators;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Settings getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Locales getLocales() {
|
||||
return locales;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Logger getLoggingAdapter() {
|
||||
return logger;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ResourceReader getResourceReader() {
|
||||
return resourceReader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Version getPluginVersion() {
|
||||
return Version.pluginVersion(getDescription().getVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Version getMinecraftVersion() {
|
||||
return Version.minecraftVersion(Bukkit.getBukkitVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> reload() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
this.settings = Settings.load(YamlDocument.create(new File(getDataFolder(), "config.yml"), Objects.requireNonNull(resourceReader.getResource("config.yml")), GeneralSettings.builder().setUseDefaults(false).build(), LoaderSettings.builder().setAutoUpdate(true).build(), DumperSettings.builder().setEncoding(DumperSettings.Encoding.UNICODE).build(), UpdaterSettings.builder().setVersioning(new BasicVersioning("config_version")).build()));
|
||||
|
||||
this.locales = Locales.load(YamlDocument.create(new File(getDataFolder(), "messages-" + settings.getStringValue(Settings.ConfigOption.LANGUAGE) + ".yml"), Objects.requireNonNull(resourceReader.getResource("locales/" + settings.getStringValue(Settings.ConfigOption.LANGUAGE) + ".yml"))));
|
||||
return true;
|
||||
} catch (IOException | NullPointerException e) {
|
||||
getLoggingAdapter().log(Level.SEVERE, "Failed to load data from the config", e);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.util.BukkitUpdateChecker;
|
||||
import net.william278.husksync.bukkit.util.PlayerSetter;
|
||||
import net.william278.husksync.bukkit.config.ConfigLoader;
|
||||
import net.william278.husksync.bukkit.data.BukkitDataCache;
|
||||
import net.william278.husksync.bukkit.listener.BukkitRedisListener;
|
||||
import net.william278.husksync.bukkit.listener.BukkitEventListener;
|
||||
import net.william278.husksync.bukkit.migrator.MPDBDeserializer;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class HuskSyncBukkit extends JavaPlugin {
|
||||
|
||||
// Bukkit bStats ID (Different to BungeeCord)
|
||||
private static final int METRICS_ID = 13140;
|
||||
|
||||
private static HuskSyncBukkit instance;
|
||||
public static HuskSyncBukkit getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static BukkitDataCache bukkitCache;
|
||||
|
||||
public static BukkitRedisListener redisListener;
|
||||
|
||||
// Used for establishing a handshake with redis
|
||||
public static UUID serverUUID;
|
||||
|
||||
// Has a handshake been established with the Bungee?
|
||||
public static boolean handshakeCompleted = false;
|
||||
|
||||
// The handshake task to execute
|
||||
private static BukkitTask handshakeTask;
|
||||
|
||||
// Whether MySqlPlayerDataBridge is installed
|
||||
public static boolean isMySqlPlayerDataBridgeInstalled;
|
||||
|
||||
// Establish the handshake with the proxy
|
||||
public static void establishRedisHandshake() {
|
||||
serverUUID = UUID.randomUUID();
|
||||
getInstance().getLogger().log(Level.INFO, "Executing handshake with Proxy server...");
|
||||
final int[] attempts = {0}; // How many attempts to establish communication have been made
|
||||
handshakeTask = Bukkit.getScheduler().runTaskTimerAsynchronously(getInstance(), () -> {
|
||||
if (handshakeCompleted) {
|
||||
handshakeTask.cancel();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.CONNECTION_HANDSHAKE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
serverUUID.toString(),
|
||||
Boolean.toString(isMySqlPlayerDataBridgeInstalled),
|
||||
Bukkit.getName(),
|
||||
getInstance().getDescription().getVersion())
|
||||
.send();
|
||||
attempts[0]++;
|
||||
if (attempts[0] == 10) {
|
||||
getInstance().getLogger().log(Level.WARNING, "Failed to complete handshake with the Proxy server; Please make sure your Proxy server is online and has HuskSync installed in its' /plugins/ folder. HuskSync will continue to try and establish a connection.");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
getInstance().getLogger().log(Level.SEVERE, "Failed to serialize Redis message for handshake establishment", e);
|
||||
}
|
||||
}, 0, 60);
|
||||
}
|
||||
|
||||
private void closeRedisHandshake() {
|
||||
if (!handshakeCompleted) return;
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.TERMINATE_HANDSHAKE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
serverUUID.toString(),
|
||||
Bukkit.getName()).send();
|
||||
} catch (IOException e) {
|
||||
getInstance().getLogger().log(Level.SEVERE, "Failed to serialize Redis message for handshake termination", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// Plugin startup logic
|
||||
|
||||
// Load the config file
|
||||
getConfig().options().copyDefaults(true);
|
||||
saveDefaultConfig();
|
||||
saveConfig();
|
||||
reloadConfig();
|
||||
ConfigLoader.loadSettings(getConfig());
|
||||
|
||||
// Do update checker
|
||||
if (Settings.automaticUpdateChecks) {
|
||||
new BukkitUpdateChecker().logToConsole();
|
||||
}
|
||||
|
||||
// Check if MySqlPlayerDataBridge is installed
|
||||
Plugin mySqlPlayerDataBridge = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
|
||||
if (mySqlPlayerDataBridge != null) {
|
||||
isMySqlPlayerDataBridgeInstalled = mySqlPlayerDataBridge.isEnabled();
|
||||
MPDBDeserializer.setMySqlPlayerDataBridge();
|
||||
getLogger().info("MySQLPlayerDataBridge detected! Disabled data synchronisation to prevent data loss. To perform a migration, run \"husksync migrate\" in your Proxy (Bungeecord, Waterfall, etc) server console.");
|
||||
}
|
||||
|
||||
// Initialize last data update UUID cache
|
||||
bukkitCache = new BukkitDataCache();
|
||||
|
||||
// Initialize event listener
|
||||
getServer().getPluginManager().registerEvents(new BukkitEventListener(), this);
|
||||
|
||||
// Initialize the redis listener
|
||||
redisListener = new BukkitRedisListener();
|
||||
|
||||
// Ensure redis is connected; establish a handshake
|
||||
establishRedisHandshake();
|
||||
|
||||
// Initialize bStats metrics
|
||||
try {
|
||||
new Metrics(this, METRICS_ID);
|
||||
} catch (Exception e) {
|
||||
getLogger().info("Skipped metrics initialization");
|
||||
}
|
||||
|
||||
// Log to console
|
||||
getLogger().info("Enabled HuskSync (" + getServer().getName() + ") v" + getDescription().getVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
// Update player data for disconnecting players
|
||||
if (HuskSyncBukkit.handshakeCompleted && !HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled && Bukkit.getOnlinePlayers().size() > 0) {
|
||||
getLogger().info("Saving data for remaining online players...");
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
PlayerSetter.updatePlayerData(player, false);
|
||||
|
||||
// Clear player inventory and ender chest
|
||||
player.getInventory().clear();
|
||||
player.getEnderChest().clear();
|
||||
}
|
||||
getLogger().info("Data save complete!");
|
||||
}
|
||||
|
||||
|
||||
// Send termination handshake to proxy
|
||||
closeRedisHandshake();
|
||||
|
||||
// Plugin shutdown logic
|
||||
getLogger().info("Disabled HuskSync (" + getServer().getName() + ") v" + getDescription().getVersion());
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package net.william278.husksync.bukkit.config;
|
||||
|
||||
import net.william278.husksync.Settings;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
|
||||
public class ConfigLoader {
|
||||
|
||||
public static void loadSettings(FileConfiguration config) throws IllegalArgumentException {
|
||||
Settings.serverType = Settings.ServerType.BUKKIT;
|
||||
Settings.automaticUpdateChecks = config.getBoolean("check_for_updates", true);
|
||||
Settings.cluster = config.getString("cluster_id", "main");
|
||||
Settings.redisHost = config.getString("redis_settings.host", "localhost");
|
||||
Settings.redisPort = config.getInt("redis_settings.port", 6379);
|
||||
Settings.redisPassword = config.getString("redis_settings.password", "");
|
||||
Settings.redisSSL = config.getBoolean("redis_settings.use_ssl", false);
|
||||
|
||||
Settings.syncInventories = config.getBoolean("synchronisation_settings.inventories", true);
|
||||
Settings.syncEnderChests = config.getBoolean("synchronisation_settings.ender_chests", true);
|
||||
Settings.syncHealth = config.getBoolean("synchronisation_settings.health", true);
|
||||
Settings.syncHunger = config.getBoolean("synchronisation_settings.hunger", true);
|
||||
Settings.syncExperience = config.getBoolean("synchronisation_settings.experience", true);
|
||||
Settings.syncPotionEffects = config.getBoolean("synchronisation_settings.potion_effects", true);
|
||||
Settings.syncStatistics = config.getBoolean("synchronisation_settings.statistics", true);
|
||||
Settings.syncGameMode = config.getBoolean("synchronisation_settings.game_mode", true);
|
||||
Settings.syncAdvancements = config.getBoolean("synchronisation_settings.advancements", true);
|
||||
Settings.syncLocation = config.getBoolean("synchronisation_settings.location", false);
|
||||
Settings.syncFlight = config.getBoolean("synchronisation_settings.flight", false);
|
||||
|
||||
Settings.useNativeImplementation = config.getBoolean("native_advancement_synchronization", false);
|
||||
Settings.saveOnWorldSave = config.getBoolean("save_on_world_save", true);
|
||||
Settings.synchronizationTimeoutRetryDelay = config.getLong("synchronization_timeout_retry_delay", 15L);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package net.william278.husksync.bukkit.data;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.UUID;
|
||||
|
||||
public class BukkitDataCache {
|
||||
|
||||
/**
|
||||
* Map of Player UUIDs to request on join
|
||||
*/
|
||||
private static HashSet<UUID> requestOnJoin;
|
||||
|
||||
public boolean isPlayerRequestingOnJoin(UUID uuid) {
|
||||
return requestOnJoin.contains(uuid);
|
||||
}
|
||||
|
||||
public void setRequestOnJoin(UUID uuid) {
|
||||
requestOnJoin.add(uuid);
|
||||
}
|
||||
|
||||
public void removeRequestOnJoin(UUID uuid) {
|
||||
requestOnJoin.remove(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of Player UUIDs whose data has not been set yet
|
||||
*/
|
||||
private static HashSet<UUID> awaitingDataFetch;
|
||||
|
||||
public boolean isAwaitingDataFetch(UUID uuid) {
|
||||
return awaitingDataFetch.contains(uuid);
|
||||
}
|
||||
|
||||
public void setAwaitingDataFetch(UUID uuid) {
|
||||
awaitingDataFetch.add(uuid);
|
||||
}
|
||||
|
||||
public void removeAwaitingDataFetch(UUID uuid) {
|
||||
awaitingDataFetch.remove(uuid);
|
||||
}
|
||||
|
||||
public HashSet<UUID> getAwaitingDataFetch() {
|
||||
return awaitingDataFetch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of data being viewed by players
|
||||
*/
|
||||
private static HashMap<UUID, DataViewer.DataView> viewingPlayerData;
|
||||
|
||||
public void setViewing(UUID uuid, DataViewer.DataView dataView) {
|
||||
viewingPlayerData.put(uuid, dataView);
|
||||
}
|
||||
|
||||
public void removeViewing(UUID uuid) {
|
||||
viewingPlayerData.remove(uuid);
|
||||
}
|
||||
|
||||
public boolean isViewing(UUID uuid) {
|
||||
return viewingPlayerData.containsKey(uuid);
|
||||
}
|
||||
|
||||
public DataViewer.DataView getViewing(UUID uuid) {
|
||||
return viewingPlayerData.get(uuid);
|
||||
}
|
||||
|
||||
// Cache object
|
||||
public BukkitDataCache() {
|
||||
requestOnJoin = new HashSet<>();
|
||||
viewingPlayerData = new HashMap<>();
|
||||
awaitingDataFetch = new HashSet<>();
|
||||
}
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
package net.william278.husksync.bukkit.data;
|
||||
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.advancement.Advancement;
|
||||
import org.bukkit.advancement.AdvancementProgress;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Class that contains static methods for serializing and deserializing data from {@link net.william278.husksync.PlayerData}
|
||||
*/
|
||||
public class DataSerializer {
|
||||
|
||||
/**
|
||||
* Returns a serialized array of {@link ItemStack}s
|
||||
*
|
||||
* @param inventoryContents The contents of the inventory
|
||||
* @return The serialized inventory contents
|
||||
*/
|
||||
public static String serializeInventory(ItemStack[] inventoryContents) {
|
||||
// Return an empty string if there is no inventory item data to serialize
|
||||
if (inventoryContents.length == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Create an output stream that will be encoded into base 64
|
||||
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
|
||||
|
||||
try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
|
||||
// Define the length of the inventory array to serialize
|
||||
bukkitOutputStream.writeInt(inventoryContents.length);
|
||||
|
||||
// Write each serialize each ItemStack to the output stream
|
||||
for (ItemStack inventoryItem : inventoryContents) {
|
||||
bukkitOutputStream.writeObject(serializeItemStack(inventoryItem));
|
||||
}
|
||||
|
||||
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
|
||||
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Failed to serialize item stack data");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of ItemStacks from serialized inventory data. Note: empty slots will be represented by {@code null}
|
||||
*
|
||||
* @param inventoryData The serialized {@link ItemStack[]} array
|
||||
* @return The inventory contents as an array of {@link ItemStack}s
|
||||
* @throws IOException If the deserialization fails reading data from the InputStream
|
||||
* @throws ClassNotFoundException If the deserialization class cannot be found
|
||||
*/
|
||||
public static ItemStack[] deserializeInventory(String inventoryData) throws IOException, ClassNotFoundException {
|
||||
// Return empty array if there is no inventory data (set the player as having an empty inventory)
|
||||
if (inventoryData.isEmpty()) {
|
||||
return new ItemStack[0];
|
||||
}
|
||||
|
||||
// Create a byte input stream to read the serialized data
|
||||
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(inventoryData))) {
|
||||
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
|
||||
// Read the length of the Bukkit input stream and set the length of the array to this value
|
||||
ItemStack[] inventoryContents = new ItemStack[bukkitInputStream.readInt()];
|
||||
|
||||
// Set the ItemStacks in the array from deserialized ItemStack data
|
||||
int slotIndex = 0;
|
||||
for (ItemStack ignored : inventoryContents) {
|
||||
inventoryContents[slotIndex] = deserializeItemStack(bukkitInputStream.readObject());
|
||||
slotIndex++;
|
||||
}
|
||||
|
||||
// Return the finished, serialized inventory contents
|
||||
return inventoryContents;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized version of an {@link ItemStack} as a string to object Map
|
||||
*
|
||||
* @param item The {@link ItemStack} to serialize
|
||||
* @return The serialized {@link ItemStack}
|
||||
*/
|
||||
private static Map<String, Object> serializeItemStack(ItemStack item) {
|
||||
return item != null ? item.serialize() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deserialized {@link ItemStack} from the Object read from the {@link BukkitObjectInputStream}
|
||||
*
|
||||
* @param serializedItemStack The serialized item stack; a String-Object map
|
||||
* @return The deserialized {@link ItemStack}
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
|
||||
private static ItemStack deserializeItemStack(Object serializedItemStack) {
|
||||
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized array of {@link PotionEffect}s
|
||||
*
|
||||
* @param potionEffects The potion effect array
|
||||
* @return The serialized potion effects
|
||||
*/
|
||||
public static String serializePotionEffects(PotionEffect[] potionEffects) {
|
||||
// Return an empty string if there are no effects to serialize
|
||||
if (potionEffects.length == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Create an output stream that will be encoded into base 64
|
||||
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
|
||||
|
||||
try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
|
||||
// Define the length of the potion effect array to serialize
|
||||
bukkitOutputStream.writeInt(potionEffects.length);
|
||||
|
||||
// Write each serialize each PotionEffect to the output stream
|
||||
for (PotionEffect potionEffect : potionEffects) {
|
||||
bukkitOutputStream.writeObject(serializePotionEffect(potionEffect));
|
||||
}
|
||||
|
||||
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
|
||||
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Failed to serialize potion effect data");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of ItemStacks from serialized potion effect data
|
||||
*
|
||||
* @param potionEffectData The serialized {@link PotionEffect[]} array
|
||||
* @return The {@link PotionEffect}s
|
||||
* @throws IOException If the deserialization fails reading data from the InputStream
|
||||
* @throws ClassNotFoundException If the deserialization class cannot be found
|
||||
*/
|
||||
public static PotionEffect[] deserializePotionEffects(String potionEffectData) throws IOException, ClassNotFoundException {
|
||||
// Return empty array if there is no potion effect data (don't apply any effects to the player)
|
||||
if (potionEffectData.isEmpty()) {
|
||||
return new PotionEffect[0];
|
||||
}
|
||||
|
||||
// Create a byte input stream to read the serialized data
|
||||
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(potionEffectData))) {
|
||||
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
|
||||
// Read the length of the Bukkit input stream and set the length of the array to this value
|
||||
PotionEffect[] potionEffects = new PotionEffect[bukkitInputStream.readInt()];
|
||||
|
||||
// Set the potion effects in the array from deserialized PotionEffect data
|
||||
int potionIndex = 0;
|
||||
for (PotionEffect ignored : potionEffects) {
|
||||
potionEffects[potionIndex] = deserializePotionEffect(bukkitInputStream.readObject());
|
||||
potionIndex++;
|
||||
}
|
||||
|
||||
// Return the finished, serialized potion effect array
|
||||
return potionEffects;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized version of an {@link ItemStack} as a string to object Map
|
||||
*
|
||||
* @param potionEffect The {@link ItemStack} to serialize
|
||||
* @return The serialized {@link ItemStack}
|
||||
*/
|
||||
private static Map<String, Object> serializePotionEffect(PotionEffect potionEffect) {
|
||||
return potionEffect != null ? potionEffect.serialize() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deserialized {@link PotionEffect} from the Object read from the {@link BukkitObjectInputStream}
|
||||
*
|
||||
* @param serializedPotionEffect The serialized potion effect; a String-Object map
|
||||
* @return The deserialized {@link PotionEffect}
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
|
||||
private static PotionEffect deserializePotionEffect(Object serializedPotionEffect) {
|
||||
return serializedPotionEffect != null ? new PotionEffect((Map<String, Object>) serializedPotionEffect) : null;
|
||||
}
|
||||
|
||||
public static me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation deserializePlayerLocationData(String serializedLocationData) throws IOException {
|
||||
if (serializedLocationData.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return (me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation) RedisMessage.deserialize(serializedLocationData);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IOException("Unable to decode class type.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getSerializedLocation(Player player) throws IOException {
|
||||
final Location playerLocation = player.getLocation();
|
||||
return RedisMessage.serialize(new me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation(playerLocation.getX(), playerLocation.getY(), playerLocation.getZ(),
|
||||
playerLocation.getYaw(), playerLocation.getPitch(), player.getWorld().getName(), player.getWorld().getEnvironment()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a player's advancement data as serialized with {@link #getSerializedAdvancements(Player)} into {@link me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate} data.
|
||||
*
|
||||
* @param serializedAdvancementData The serialized advancement data {@link String}
|
||||
* @return The deserialized {@link me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate} for the player
|
||||
* @throws IOException If the deserialization fails
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // Ignore the unchecked cast here
|
||||
public static List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> deserializeAdvancementData(String serializedAdvancementData) throws IOException {
|
||||
if (serializedAdvancementData.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
List<?> deserialize = (List<?>) RedisMessage.deserialize(serializedAdvancementData);
|
||||
|
||||
// Migrate old AdvancementRecord into date format
|
||||
if (!deserialize.isEmpty() && deserialize.get(0) instanceof me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecord) {
|
||||
deserialize = ((List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecord>) deserialize).stream()
|
||||
.map(o -> new me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate(
|
||||
o.advancementKey(),
|
||||
o.awardedAdvancementCriteria()
|
||||
)).toList();
|
||||
}
|
||||
|
||||
return (List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate>) deserialize;
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IOException("Unable to decode class type.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of a player's advancements that can be deserialized with {@link #deserializeStatisticData(String)}
|
||||
*
|
||||
* @param player {@link Player} to serialize advancement data of
|
||||
* @return The serialized advancement data as a {@link String}
|
||||
* @throws IOException If the serialization fails
|
||||
*/
|
||||
public static String getSerializedAdvancements(Player player) throws IOException {
|
||||
Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
|
||||
ArrayList<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> advancementData = new ArrayList<>();
|
||||
|
||||
while (serverAdvancements.hasNext()) {
|
||||
final AdvancementProgress progress = player.getAdvancementProgress(serverAdvancements.next());
|
||||
final NamespacedKey advancementKey = progress.getAdvancement().getKey();
|
||||
|
||||
final Map<String, Date> awardedCriteria = new HashMap<>();
|
||||
progress.getAwardedCriteria().forEach(s -> awardedCriteria.put(s, progress.getDateAwarded(s)));
|
||||
|
||||
advancementData.add(new me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate(advancementKey.getNamespace() + ":" + advancementKey.getKey(), awardedCriteria));
|
||||
}
|
||||
|
||||
return RedisMessage.serialize(advancementData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a player's statistic data as serialized with {@link #getSerializedStatisticData(Player)} into {@link me.william278.husksync.bukkit.data.DataSerializer.StatisticData}.
|
||||
*
|
||||
* @param serializedStatisticData The serialized statistic data {@link String}
|
||||
* @return The deserialized {@link me.william278.husksync.bukkit.data.DataSerializer.StatisticData} for the player
|
||||
* @throws IOException If the deserialization fails
|
||||
*/
|
||||
public static me.william278.husksync.bukkit.data.DataSerializer.StatisticData deserializeStatisticData(String serializedStatisticData) throws IOException {
|
||||
if (serializedStatisticData.isEmpty()) {
|
||||
return new me.william278.husksync.bukkit.data.DataSerializer.StatisticData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
|
||||
}
|
||||
try {
|
||||
return (me.william278.husksync.bukkit.data.DataSerializer.StatisticData) RedisMessage.deserialize(serializedStatisticData);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IOException("Unable to decode class type.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of a player's statistic data that can be deserialized with {@link #deserializeStatisticData(String)}
|
||||
*
|
||||
* @param player {@link Player} to serialize statistic data of
|
||||
* @return The serialized statistic data as a {@link String}
|
||||
* @throws IOException If the serialization fails
|
||||
*/
|
||||
public static String getSerializedStatisticData(Player player) throws IOException {
|
||||
HashMap<Statistic, Integer> untypedStatisticValues = new HashMap<>();
|
||||
HashMap<Statistic, HashMap<Material, Integer>> blockStatisticValues = new HashMap<>();
|
||||
HashMap<Statistic, HashMap<Material, Integer>> itemStatisticValues = new HashMap<>();
|
||||
HashMap<Statistic, HashMap<EntityType, Integer>> entityStatisticValues = new HashMap<>();
|
||||
for (Statistic statistic : Statistic.values()) {
|
||||
switch (statistic.getType()) {
|
||||
case ITEM -> {
|
||||
HashMap<Material, Integer> itemValues = new HashMap<>();
|
||||
for (Material itemMaterial : Arrays.stream(Material.values()).filter(Material::isItem).toList()) {
|
||||
itemValues.put(itemMaterial, player.getStatistic(statistic, itemMaterial));
|
||||
}
|
||||
itemStatisticValues.put(statistic, itemValues);
|
||||
}
|
||||
case BLOCK -> {
|
||||
HashMap<Material, Integer> blockValues = new HashMap<>();
|
||||
for (Material blockMaterial : Arrays.stream(Material.values()).filter(Material::isBlock).toList()) {
|
||||
blockValues.put(blockMaterial, player.getStatistic(statistic, blockMaterial));
|
||||
}
|
||||
blockStatisticValues.put(statistic, blockValues);
|
||||
}
|
||||
case ENTITY -> {
|
||||
HashMap<EntityType, Integer> entityValues = new HashMap<>();
|
||||
for (EntityType type : Arrays.stream(EntityType.values()).filter(EntityType::isAlive).toList()) {
|
||||
entityValues.put(type, player.getStatistic(statistic, type));
|
||||
}
|
||||
entityStatisticValues.put(statistic, entityValues);
|
||||
}
|
||||
case UNTYPED -> untypedStatisticValues.put(statistic, player.getStatistic(statistic));
|
||||
}
|
||||
}
|
||||
|
||||
me.william278.husksync.bukkit.data.DataSerializer.StatisticData statisticData = new me.william278.husksync.bukkit.data.DataSerializer.StatisticData(untypedStatisticValues, blockStatisticValues, itemStatisticValues, entityStatisticValues);
|
||||
return RedisMessage.serialize(statisticData);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package net.william278.husksync.bukkit.data;
|
||||
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.util.PlayerSetter;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Class used for managing viewing inventories using inventory-see command
|
||||
*/
|
||||
public class DataViewer {
|
||||
|
||||
/**
|
||||
* Show a viewer's data to a viewer
|
||||
*
|
||||
* @param viewer The viewing {@link Player} who will see the data
|
||||
* @param data The {@link DataView} to show the viewer
|
||||
* @throws IOException If an exception occurred deserializing item data
|
||||
*/
|
||||
public static void showData(Player viewer, DataView data) throws IOException, ClassNotFoundException {
|
||||
// Show an inventory with the viewer's inventory and equipment
|
||||
viewer.closeInventory();
|
||||
viewer.openInventory(createInventory(viewer, data));
|
||||
|
||||
// Set the viewer as viewing
|
||||
HuskSyncBukkit.bukkitCache.setViewing(viewer.getUniqueId(), data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles what happens after a data viewer finishes viewing data
|
||||
*
|
||||
* @param viewer The viewing {@link Player} who was looking at data
|
||||
* @param inventory The {@link Inventory} that was being viewed
|
||||
* @throws IOException If an exception occurred serializing item data
|
||||
*/
|
||||
public static void stopShowing(Player viewer, Inventory inventory) throws IOException {
|
||||
// Get the DataView the player was looking at
|
||||
DataView dataView = HuskSyncBukkit.bukkitCache.getViewing(viewer.getUniqueId());
|
||||
|
||||
// Set the player as no longer viewing an inventory
|
||||
HuskSyncBukkit.bukkitCache.removeViewing(viewer.getUniqueId());
|
||||
|
||||
// Get and update the PlayerData with the new item data
|
||||
PlayerData playerData = dataView.playerData();
|
||||
String serializedItemData = DataSerializer.serializeInventory(inventory.getContents());
|
||||
switch (dataView.inventoryType()) {
|
||||
case INVENTORY -> playerData.setSerializedInventory(serializedItemData);
|
||||
case ENDER_CHEST -> playerData.setSerializedEnderChest(serializedItemData);
|
||||
}
|
||||
|
||||
// Send a redis message with the updated data after the viewing
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
RedisMessage.serialize(playerData), Boolean.toString(true))
|
||||
.send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the inventory object that the viewer will see
|
||||
*
|
||||
* @param viewer The {@link Player} who will view the data
|
||||
* @param data The {@link DataView} data to view
|
||||
* @return The {@link Inventory} that the viewer will see
|
||||
* @throws IOException If an exception occurred deserializing item data
|
||||
*/
|
||||
private static Inventory createInventory(Player viewer, DataView data) throws IOException, ClassNotFoundException {
|
||||
Inventory inventory = switch (data.inventoryType) {
|
||||
case INVENTORY -> Bukkit.createInventory(viewer, 45, data.ownerName + "'s Inventory");
|
||||
case ENDER_CHEST -> Bukkit.createInventory(viewer, 27, data.ownerName + "'s Ender Chest");
|
||||
};
|
||||
PlayerSetter.setInventory(inventory, data.getDeserializedData());
|
||||
return inventory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents Player Data being viewed by a {@link Player}
|
||||
*/
|
||||
public record DataView(PlayerData playerData, String ownerName, InventoryType inventoryType) {
|
||||
/**
|
||||
* What kind of item data is being viewed
|
||||
*/
|
||||
public enum InventoryType {
|
||||
/**
|
||||
* A player's inventory
|
||||
*/
|
||||
INVENTORY,
|
||||
|
||||
/**
|
||||
* A player's ender chest
|
||||
*/
|
||||
ENDER_CHEST
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the deserialized data currently being viewed
|
||||
*
|
||||
* @return The deserialized item data, as an {@link ItemStack[]} array
|
||||
* @throws IOException If an exception occurred deserializing item data
|
||||
*/
|
||||
public ItemStack[] getDeserializedData() throws IOException, ClassNotFoundException {
|
||||
return switch (inventoryType) {
|
||||
case INVENTORY -> DataSerializer.deserializeInventory(playerData.getSerializedInventory());
|
||||
case ENDER_CHEST -> DataSerializer.deserializeInventory(playerData.getSerializedEnderChest());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package net.william278.husksync.bukkit.events;
|
||||
|
||||
import net.william278.husksync.PlayerData;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.player.PlayerEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Represents an event that will be fired when a {@link Player} has finished being synchronised with the correct {@link PlayerData}.
|
||||
*/
|
||||
public class SyncCompleteEvent extends PlayerEvent {
|
||||
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
private final PlayerData data;
|
||||
|
||||
public SyncCompleteEvent(Player player, PlayerData data) {
|
||||
super(player);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link PlayerData} which has just been set on the {@link Player}
|
||||
* @return The {@link PlayerData} that has been set
|
||||
*/
|
||||
public PlayerData getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package net.william278.husksync.bukkit.events;
|
||||
|
||||
import net.william278.husksync.PlayerData;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.player.PlayerEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Represents an event that will be fired before a {@link Player} is about to be synchronised with their {@link PlayerData}.
|
||||
*/
|
||||
public class SyncEvent extends PlayerEvent implements Cancellable {
|
||||
|
||||
private boolean cancelled;
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
private PlayerData data;
|
||||
|
||||
public SyncEvent(Player player, PlayerData data) {
|
||||
super(player);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link PlayerData} which has just been set on the {@link Player}
|
||||
*
|
||||
* @return The {@link PlayerData} that has been set
|
||||
*/
|
||||
public PlayerData getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link PlayerData} to be synchronised to this player
|
||||
*
|
||||
* @param data The {@link PlayerData} to set to the player
|
||||
*/
|
||||
public void setData(PlayerData data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cancellation state of this event. A cancelled event will not be executed in the server, but will still pass to other plugins
|
||||
*
|
||||
* @return true if this event is cancelled
|
||||
*/
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cancellation state of this event. A cancelled event will not be executed in the server, but will still pass to other plugins.
|
||||
*
|
||||
* @param cancel true if you wish to cancel this event
|
||||
*/
|
||||
@Override
|
||||
public void setCancelled(boolean cancel) {
|
||||
this.cancelled = cancel;
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
package net.william278.husksync.bukkit.listener;
|
||||
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.data.DataViewer;
|
||||
import net.william278.husksync.bukkit.util.PlayerSetter;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.entity.EntityPickupItemEvent;
|
||||
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||
import org.bukkit.event.player.*;
|
||||
import org.bukkit.event.world.WorldSaveEvent;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitEventListener implements Listener {
|
||||
|
||||
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||
// When a player leaves a Bukkit server
|
||||
final Player player = event.getPlayer();
|
||||
|
||||
// If the player was awaiting data fetch, remove them and prevent data from being overwritten
|
||||
if (HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(player.getUniqueId())) {
|
||||
HuskSyncBukkit.bukkitCache.removeAwaitingDataFetch(player.getUniqueId());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled)
|
||||
return; // If the plugin has not been initialized correctly
|
||||
|
||||
// Update the player's data
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
// Update data to proxy
|
||||
PlayerSetter.updatePlayerData(player, true);
|
||||
|
||||
// Clear player inventory and ender chest
|
||||
player.getInventory().clear();
|
||||
player.getEnderChest().clear();
|
||||
});
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
if (!plugin.isEnabled()) return; // If the plugin has not been initialized correctly
|
||||
|
||||
// When a player joins a Bukkit server
|
||||
final Player player = event.getPlayer();
|
||||
|
||||
// Mark the player as awaiting data fetch
|
||||
HuskSyncBukkit.bukkitCache.setAwaitingDataFetch(player.getUniqueId());
|
||||
|
||||
if (!HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled) {
|
||||
return; // If the data handshake has not been completed yet (or MySqlPlayerDataBridge is installed)
|
||||
}
|
||||
|
||||
// Send a redis message requesting the player data (if they need to)
|
||||
if (HuskSyncBukkit.bukkitCache.isPlayerRequestingOnJoin(player.getUniqueId())) {
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
PlayerSetter.requestPlayerData(player.getUniqueId());
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData fetch request", e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If the player's data wasn't set after the synchronization timeout retry delay ticks, ensure it will be
|
||||
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, () -> {
|
||||
if (player.isOnline()) {
|
||||
try {
|
||||
if (HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(player.getUniqueId())) {
|
||||
PlayerSetter.requestPlayerData(player.getUniqueId());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData fetch request", e);
|
||||
}
|
||||
}
|
||||
}, Settings.synchronizationTimeoutRetryDelay);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryClose(InventoryCloseEvent event) {
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId()))
|
||||
return; // If the plugin has not been initialized correctly
|
||||
|
||||
// When a player closes an Inventory
|
||||
final Player player = (Player) event.getPlayer();
|
||||
|
||||
// Handle a player who has finished viewing a player's item data
|
||||
if (HuskSyncBukkit.bukkitCache.isViewing(player.getUniqueId())) {
|
||||
try {
|
||||
DataViewer.stopShowing(player, event.getInventory());
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to serialize updated item data", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Events to cancel if the player has not been set yet
|
||||
*/
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onDropItem(PlayerDropItemEvent event) {
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
|
||||
event.setCancelled(true); // If the plugin / player has not been set
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onPickupItem(EntityPickupItemEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(player.getUniqueId())) {
|
||||
event.setCancelled(true); // If the plugin / player has not been set
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
|
||||
event.setCancelled(true); // If the plugin / player has not been set
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onBlockPlace(BlockPlaceEvent event) {
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
|
||||
event.setCancelled(true); // If the plugin / player has not been set
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onBlockBreak(BlockBreakEvent event) {
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
|
||||
event.setCancelled(true); // If the plugin / player has not been set
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onInventoryOpen(InventoryOpenEvent event) {
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
|
||||
event.setCancelled(true); // If the plugin / player has not been set
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.NORMAL)
|
||||
public void onWorldSave(WorldSaveEvent event) {
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted) {
|
||||
return;
|
||||
}
|
||||
for (Player playerInWorld : event.getWorld().getPlayers()) {
|
||||
PlayerSetter.updatePlayerData(playerInWorld, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
package net.william278.husksync.bukkit.listener;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.config.ConfigLoader;
|
||||
import net.william278.husksync.bukkit.data.DataViewer;
|
||||
import net.william278.husksync.bukkit.migrator.MPDBDeserializer;
|
||||
import net.william278.husksync.bukkit.util.PlayerSetter;
|
||||
import net.william278.husksync.migrator.MPDBPlayerData;
|
||||
import net.william278.husksync.redis.RedisListener;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.william278.husksync.util.MessageManager;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitRedisListener extends RedisListener {
|
||||
|
||||
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||
|
||||
public static HashMap<UUID, CompletableFuture<PlayerData>> apiRequests = new HashMap<>();
|
||||
|
||||
// Initialize the listener on the bukkit server
|
||||
public BukkitRedisListener() {
|
||||
super();
|
||||
listen();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming {@link RedisMessage}
|
||||
*
|
||||
* @param message The {@link RedisMessage} to handle
|
||||
*/
|
||||
@Override
|
||||
public void handleMessage(RedisMessage message) {
|
||||
// Ignore messages for proxy servers
|
||||
if (!message.getMessageTarget().targetServerType().equals(Settings.ServerType.BUKKIT)) {
|
||||
return;
|
||||
}
|
||||
// Ignore messages if the plugin is disabled
|
||||
if (!plugin.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
// Ignore messages for other clusters if applicable
|
||||
final String targetClusterId = message.getMessageTarget().targetClusterId();
|
||||
if (targetClusterId != null) {
|
||||
if (!targetClusterId.equalsIgnoreCase(Settings.cluster)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the incoming redis message; either for a specific player or the system
|
||||
if (message.getMessageTarget().targetPlayerUUID() == null) {
|
||||
switch (message.getMessageType()) {
|
||||
case REQUEST_DATA_ON_JOIN -> {
|
||||
UUID playerUUID = UUID.fromString(message.getMessageDataElements()[1]);
|
||||
switch (RedisMessage.RequestOnJoinUpdateType.valueOf(message.getMessageDataElements()[0])) {
|
||||
case ADD_REQUESTER -> HuskSyncBukkit.bukkitCache.setRequestOnJoin(playerUUID);
|
||||
case REMOVE_REQUESTER -> HuskSyncBukkit.bukkitCache.removeRequestOnJoin(playerUUID);
|
||||
}
|
||||
}
|
||||
case CONNECTION_HANDSHAKE -> {
|
||||
UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
String proxyBrand = message.getMessageDataElements()[1];
|
||||
if (serverUUID.equals(HuskSyncBukkit.serverUUID)) {
|
||||
HuskSyncBukkit.handshakeCompleted = true;
|
||||
log(Level.INFO, "Completed handshake with " + proxyBrand + " proxy (" + serverUUID + ")");
|
||||
|
||||
// If there are any players awaiting a data update, request it
|
||||
for (UUID uuid : HuskSyncBukkit.bukkitCache.getAwaitingDataFetch()) {
|
||||
try {
|
||||
PlayerSetter.requestPlayerData(uuid);
|
||||
} catch (IOException e) {
|
||||
log(Level.SEVERE, "Failed to serialize handshake message data");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case TERMINATE_HANDSHAKE -> {
|
||||
UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
String proxyBrand = message.getMessageDataElements()[1];
|
||||
if (serverUUID.equals(HuskSyncBukkit.serverUUID)) {
|
||||
HuskSyncBukkit.handshakeCompleted = false;
|
||||
log(Level.WARNING, proxyBrand + " proxy has terminated communications; attempting to re-establish (" + serverUUID + ")");
|
||||
|
||||
// Attempt to re-establish communications via another handshake
|
||||
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, HuskSyncBukkit::establishRedisHandshake, 20);
|
||||
}
|
||||
}
|
||||
case DECODE_MPDB_DATA -> {
|
||||
UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
String encodedData = message.getMessageDataElements()[1];
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
if (serverUUID.equals(HuskSyncBukkit.serverUUID)) {
|
||||
try {
|
||||
MPDBPlayerData data = (MPDBPlayerData) RedisMessage.deserialize(encodedData);
|
||||
new RedisMessage(RedisMessage.MessageType.DECODED_MPDB_DATA_SET,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
RedisMessage.serialize(MPDBDeserializer.convertMPDBData(data)),
|
||||
data.playerName)
|
||||
.send();
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to serialize encoded MPDB data");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
case API_DATA_RETURN -> {
|
||||
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
if (apiRequests.containsKey(requestUUID)) {
|
||||
try {
|
||||
final PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageDataElements()[1]);
|
||||
apiRequests.get(requestUUID).complete(data);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to serialize returned API-requested player data");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
case API_DATA_CANCEL -> {
|
||||
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
// Cancel requests if no data could be found on the proxy
|
||||
if (apiRequests.containsKey(requestUUID)) {
|
||||
apiRequests.get(requestUUID).cancel(true);
|
||||
}
|
||||
}
|
||||
case RELOAD_CONFIG -> {
|
||||
plugin.reloadConfig();
|
||||
ConfigLoader.loadSettings(plugin.getConfig());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (player.getUniqueId().equals(message.getMessageTarget().targetPlayerUUID())) {
|
||||
switch (message.getMessageType()) {
|
||||
case PLAYER_DATA_SET -> {
|
||||
if (HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled) return;
|
||||
try {
|
||||
// Deserialize the received PlayerData
|
||||
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageData());
|
||||
|
||||
// Set the player's data
|
||||
PlayerSetter.setPlayerFrom(player, data);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to deserialize PlayerData when handling data from the proxy");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
case SEND_PLUGIN_INFORMATION -> {
|
||||
String proxyBrand = message.getMessageDataElements()[0];
|
||||
String proxyVersion = message.getMessageDataElements()[1];
|
||||
assert plugin.getDescription().getDescription() != null;
|
||||
player.spigot().sendMessage(new MineDown(MessageManager.PLUGIN_INFORMATION.toString()
|
||||
.replaceAll("%plugin_description%", plugin.getDescription().getDescription())
|
||||
.replaceAll("%proxy_brand%", proxyBrand)
|
||||
.replaceAll("%proxy_version%", proxyVersion)
|
||||
.replaceAll("%bukkit_brand%", Bukkit.getName())
|
||||
.replaceAll("%bukkit_version%", plugin.getDescription().getVersion()))
|
||||
.toComponent());
|
||||
}
|
||||
case OPEN_INVENTORY -> {
|
||||
// Get the name of the inventory owner
|
||||
String inventoryOwnerName = message.getMessageDataElements()[0];
|
||||
|
||||
// Synchronously do inventory setting, etc
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
try {
|
||||
// Get that player's data
|
||||
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageDataElements()[1]);
|
||||
|
||||
// Show the data to the player
|
||||
DataViewer.showData(player, new DataViewer.DataView(data, inventoryOwnerName, DataViewer.DataView.InventoryType.INVENTORY));
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to deserialize PlayerData when handling inventory-see data from the proxy");
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
case OPEN_ENDER_CHEST -> {
|
||||
// Get the name of the inventory owner
|
||||
String enderChestOwnerName = message.getMessageDataElements()[0];
|
||||
|
||||
// Synchronously do inventory setting, etc
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
try {
|
||||
// Get that player's data
|
||||
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageDataElements()[1]);
|
||||
|
||||
// Show the data to the player
|
||||
DataViewer.showData(player, new DataViewer.DataView(data, enderChestOwnerName, DataViewer.DataView.InventoryType.ENDER_CHEST));
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to deserialize PlayerData when handling ender chest-see data from the proxy");
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log to console
|
||||
*
|
||||
* @param level The {@link Level} to log
|
||||
* @param message Message to log
|
||||
*/
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
plugin.getLogger().log(level, message);
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package net.william278.husksync.bukkit.migrator;
|
||||
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.bukkit.data.DataSerializer;
|
||||
import net.william278.husksync.bukkit.util.PlayerSetter;
|
||||
import net.william278.husksync.migrator.MPDBPlayerData;
|
||||
import net.william278.mpdbconverter.MPDBConverter;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class MPDBDeserializer {
|
||||
|
||||
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||
|
||||
// Instance of MySqlPlayerDataBridge
|
||||
private static MPDBConverter mpdbConverter;
|
||||
|
||||
public static void setMySqlPlayerDataBridge() {
|
||||
Plugin mpdbPlugin = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
|
||||
assert mpdbPlugin != null;
|
||||
mpdbConverter = MPDBConverter.getInstance(mpdbPlugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert MySqlPlayerDataBridge ({@link MPDBPlayerData}) data to HuskSync's {@link PlayerData}
|
||||
*
|
||||
* @param mpdbPlayerData The {@link MPDBPlayerData} to convert
|
||||
* @return The converted {@link PlayerData}
|
||||
*/
|
||||
public static PlayerData convertMPDBData(MPDBPlayerData mpdbPlayerData) {
|
||||
PlayerData playerData = PlayerData.DEFAULT_PLAYER_DATA(mpdbPlayerData.playerUUID);
|
||||
playerData.useDefaultData = false;
|
||||
if (!HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled) {
|
||||
plugin.getLogger().log(Level.SEVERE, "MySqlPlayerDataBridge is not installed, failed to serialize data!");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert the data
|
||||
try {
|
||||
// Set inventory contents
|
||||
Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
|
||||
if (!mpdbPlayerData.inventoryData.isEmpty() && !mpdbPlayerData.inventoryData.equalsIgnoreCase("none")) {
|
||||
PlayerSetter.setInventory(inventory, mpdbConverter.getItemStackFromSerializedData(mpdbPlayerData.inventoryData));
|
||||
}
|
||||
|
||||
// Set armor (if there is data; MPDB stores empty data with literally the word "none". Obviously.)
|
||||
int armorSlot = 36;
|
||||
if (!mpdbPlayerData.armorData.isEmpty() && !mpdbPlayerData.armorData.equalsIgnoreCase("none")) {
|
||||
ItemStack[] armorItems = mpdbConverter.getItemStackFromSerializedData(mpdbPlayerData.armorData);
|
||||
for (ItemStack armorPiece : armorItems) {
|
||||
if (armorPiece != null) {
|
||||
inventory.setItem(armorSlot, armorPiece);
|
||||
}
|
||||
armorSlot++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Now apply the contents and clear the temporary inventory variable
|
||||
playerData.setSerializedInventory(DataSerializer.serializeInventory(inventory.getContents()));
|
||||
|
||||
// Set ender chest (again, if there is data)
|
||||
ItemStack[] enderChestData;
|
||||
if (!mpdbPlayerData.enderChestData.isEmpty() && !mpdbPlayerData.enderChestData.equalsIgnoreCase("none")) {
|
||||
enderChestData = mpdbConverter.getItemStackFromSerializedData(mpdbPlayerData.enderChestData);
|
||||
} else {
|
||||
enderChestData = new ItemStack[0];
|
||||
}
|
||||
playerData.setSerializedEnderChest(DataSerializer.serializeInventory(enderChestData));
|
||||
|
||||
// Set experience
|
||||
playerData.setExpLevel(mpdbPlayerData.expLevel);
|
||||
playerData.setExpProgress(mpdbPlayerData.expProgress);
|
||||
playerData.setTotalExperience(mpdbPlayerData.totalExperience);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to convert MPDB data to HuskSync's format!");
|
||||
e.printStackTrace();
|
||||
}
|
||||
return playerData;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package net.william278.husksync.bukkit.util;
|
||||
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.util.UpdateChecker;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitUpdateChecker extends UpdateChecker {
|
||||
|
||||
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||
|
||||
public BukkitUpdateChecker() {
|
||||
super(plugin.getDescription().getVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
plugin.getLogger().log(level, message);
|
||||
}
|
||||
}
|
||||
@@ -1,488 +0,0 @@
|
||||
package net.william278.husksync.bukkit.util;
|
||||
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.events.SyncCompleteEvent;
|
||||
import net.william278.husksync.bukkit.events.SyncEvent;
|
||||
import net.william278.husksync.bukkit.data.DataSerializer;
|
||||
import net.william278.husksync.bukkit.util.nms.AdvancementUtils;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.advancement.Advancement;
|
||||
import org.bukkit.advancement.AdvancementProgress;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class PlayerSetter {
|
||||
|
||||
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||
|
||||
/**
|
||||
* Returns the new serialized PlayerData for a player.
|
||||
*
|
||||
* @param player The {@link Player} to get the new serialized PlayerData for
|
||||
* @return The {@link PlayerData}, serialized as a {@link String}
|
||||
* @throws IOException If the serialization fails
|
||||
*/
|
||||
private static String getNewSerializedPlayerData(Player player) throws IOException {
|
||||
final double maxHealth = getMaxHealth(player); // Get the player's max health (used to determine health as well)
|
||||
return RedisMessage.serialize(new PlayerData(player.getUniqueId(),
|
||||
DataSerializer.serializeInventory(player.getInventory().getContents()),
|
||||
DataSerializer.serializeInventory(player.getEnderChest().getContents()),
|
||||
Math.min(player.getHealth(), maxHealth),
|
||||
maxHealth,
|
||||
player.isHealthScaled() ? player.getHealthScale() : 0D,
|
||||
player.getFoodLevel(),
|
||||
player.getSaturation(),
|
||||
player.getExhaustion(),
|
||||
player.getInventory().getHeldItemSlot(),
|
||||
DataSerializer.serializePotionEffects(getPlayerPotionEffects(player)),
|
||||
player.getTotalExperience(),
|
||||
player.getLevel(),
|
||||
player.getExp(),
|
||||
player.getGameMode().toString(),
|
||||
DataSerializer.getSerializedStatisticData(player),
|
||||
player.isFlying(),
|
||||
DataSerializer.getSerializedAdvancements(player),
|
||||
DataSerializer.getSerializedLocation(player)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Player}'s maximum health, minus any health boost effects
|
||||
*
|
||||
* @param player The {@link Player} to get the maximum health of
|
||||
* @return The {@link Player}'s max health
|
||||
*/
|
||||
private static double getMaxHealth(Player player) {
|
||||
double maxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).getBaseValue();
|
||||
|
||||
// If the player has additional health bonuses from synchronised potion effects, subtract these from this number as they are synchronised separately
|
||||
if (player.hasPotionEffect(PotionEffectType.HEALTH_BOOST) && maxHealth > 20D) {
|
||||
PotionEffect healthBoostEffect = player.getPotionEffect(PotionEffectType.HEALTH_BOOST);
|
||||
assert healthBoostEffect != null;
|
||||
double healthBoostBonus = 4 * (healthBoostEffect.getAmplifier() + 1);
|
||||
maxHealth -= healthBoostBonus;
|
||||
}
|
||||
return maxHealth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Player}'s active potion effects in a {@link PotionEffect} array
|
||||
*
|
||||
* @param player The {@link Player} to get the effects of
|
||||
* @return The {@link PotionEffect} array
|
||||
*/
|
||||
private static PotionEffect[] getPlayerPotionEffects(Player player) {
|
||||
PotionEffect[] potionEffects = new PotionEffect[player.getActivePotionEffects().size()];
|
||||
int arrayIndex = 0;
|
||||
for (PotionEffect effect : player.getActivePotionEffects()) {
|
||||
potionEffects[arrayIndex] = effect;
|
||||
arrayIndex++;
|
||||
}
|
||||
return potionEffects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a {@link Player}'s data, sending it to the proxy
|
||||
*
|
||||
* @param player {@link Player} to send data to proxy
|
||||
* @param bounceBack whether the plugin should bounce-back the updated data to the player (used for server switching)
|
||||
*/
|
||||
public static void updatePlayerData(Player player, boolean bounceBack) {
|
||||
// Send a redis message with the player's last updated PlayerData version UUID and their new PlayerData
|
||||
try {
|
||||
final String serializedPlayerData = getNewSerializedPlayerData(player);
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
serializedPlayerData, Boolean.toString(bounceBack)).send();
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData update to the proxy", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a {@link Player}'s data from the proxy
|
||||
*
|
||||
* @param playerUUID The {@link UUID} of the {@link Player} to fetch PlayerData from
|
||||
* @throws IOException If the request Redis message data fails to serialize
|
||||
*/
|
||||
public static void requestPlayerData(UUID playerUUID) throws IOException {
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_REQUEST,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
playerUUID.toString()).send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a player from their PlayerData, based on settings
|
||||
*
|
||||
* @param player The {@link Player} to set
|
||||
* @param dataToSet The {@link PlayerData} to assign to the player
|
||||
*/
|
||||
public static void setPlayerFrom(Player player, PlayerData dataToSet) {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
// Handle the SyncEvent
|
||||
SyncEvent syncEvent = new SyncEvent(player, dataToSet);
|
||||
Bukkit.getPluginManager().callEvent(syncEvent);
|
||||
final PlayerData data = syncEvent.getData();
|
||||
if (syncEvent.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the data is flagged as being default data, skip setting
|
||||
if (data.useDefaultData) {
|
||||
HuskSyncBukkit.bukkitCache.removeAwaitingDataFetch(player.getUniqueId());
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear player
|
||||
player.getInventory().clear();
|
||||
player.getEnderChest().clear();
|
||||
player.setExp(0);
|
||||
player.setLevel(0);
|
||||
|
||||
HuskSyncBukkit.bukkitCache.removeAwaitingDataFetch(player.getUniqueId());
|
||||
|
||||
// Set the player's data from the PlayerData
|
||||
try {
|
||||
// Don't sync the player if they are dead
|
||||
if (player.isDead() || player.getHealth() <= 0) {
|
||||
return;
|
||||
}
|
||||
if (Settings.syncAdvancements) {
|
||||
List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> advancementRecords
|
||||
= DataSerializer.deserializeAdvancementData(data.getSerializedAdvancements());
|
||||
|
||||
if (Settings.useNativeImplementation) {
|
||||
try {
|
||||
nativeSyncPlayerAdvancements(player, advancementRecords);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING,
|
||||
"Your server does not support a native implementation of achievements synchronization");
|
||||
plugin.getLogger().log(Level.WARNING,
|
||||
"Your server version is {0}. Please disable using native implementation!", Bukkit.getVersion());
|
||||
|
||||
Settings.useNativeImplementation = false;
|
||||
setPlayerAdvancements(player, advancementRecords, data);
|
||||
plugin.getLogger().log(Level.SEVERE, e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
setPlayerAdvancements(player, advancementRecords, data);
|
||||
}
|
||||
}
|
||||
// Don't sync the player if they are dead
|
||||
if (player.isDead() || player.getHealth() <= 0) {
|
||||
Bukkit.getPluginManager().callEvent(new SyncCompleteEvent(player, data));
|
||||
return;
|
||||
}
|
||||
if (Settings.syncInventories) {
|
||||
setPlayerInventory(player, DataSerializer.deserializeInventory(data.getSerializedInventory()));
|
||||
player.getInventory().setHeldItemSlot(data.getSelectedSlot());
|
||||
}
|
||||
if (Settings.syncEnderChests) {
|
||||
setPlayerEnderChest(player, DataSerializer.deserializeInventory(data.getSerializedEnderChest()));
|
||||
}
|
||||
if (Settings.syncHealth) {
|
||||
setPlayerHealth(player, data.getHealth(), data.getMaxHealth(), data.getHealthScale());
|
||||
}
|
||||
if (Settings.syncHunger) {
|
||||
player.setFoodLevel(data.getHunger());
|
||||
player.setSaturation(data.getSaturation());
|
||||
player.setExhaustion(data.getSaturationExhaustion());
|
||||
}
|
||||
if (Settings.syncExperience) {
|
||||
// This is also handled when syncing advancements to ensure its correct
|
||||
setPlayerExperience(player, data);
|
||||
}
|
||||
if (Settings.syncPotionEffects) {
|
||||
setPlayerPotionEffects(player, DataSerializer.deserializePotionEffects(data.getSerializedEffectData()));
|
||||
}
|
||||
if (Settings.syncStatistics) {
|
||||
setPlayerStatistics(player, DataSerializer.deserializeStatisticData(data.getSerializedStatistics()));
|
||||
}
|
||||
if (Settings.syncGameMode) {
|
||||
player.setGameMode(GameMode.valueOf(data.getGameMode()));
|
||||
}
|
||||
if (Settings.syncLocation) {
|
||||
setPlayerLocation(player, DataSerializer.deserializePlayerLocationData(data.getSerializedLocation()));
|
||||
}
|
||||
if (Settings.syncFlight) {
|
||||
if (data.isFlying()) {
|
||||
player.setAllowFlight(true);
|
||||
}
|
||||
player.setFlying(player.getAllowFlight() && data.isFlying());
|
||||
}
|
||||
|
||||
// Handle the SyncCompleteEvent
|
||||
Bukkit.getPluginManager().callEvent(new SyncCompleteEvent(player, data));
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to deserialize PlayerData", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a player's ender chest from a set of {@link ItemStack}s
|
||||
*
|
||||
* @param player The player to set the inventory of
|
||||
* @param items The array of {@link ItemStack}s to set
|
||||
*/
|
||||
private static void setPlayerEnderChest(Player player, ItemStack[] items) {
|
||||
setInventory(player.getEnderChest(), items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a player's inventory from a set of {@link ItemStack}s
|
||||
*
|
||||
* @param player The player to set the inventory of
|
||||
* @param items The array of {@link ItemStack}s to set
|
||||
*/
|
||||
private static void setPlayerInventory(Player player, ItemStack[] items) {
|
||||
setInventory(player.getInventory(), items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an inventory's contents from an array of {@link ItemStack}s
|
||||
*
|
||||
* @param inventory The inventory to set
|
||||
* @param items The {@link ItemStack}s to fill it with
|
||||
*/
|
||||
public static void setInventory(Inventory inventory, ItemStack[] items) {
|
||||
inventory.clear();
|
||||
int index = 0;
|
||||
for (ItemStack item : items) {
|
||||
if (item != null) {
|
||||
inventory.setItem(index, item);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a player's current potion effects from a set of {@link PotionEffect[]}
|
||||
*
|
||||
* @param player The player to set the potion effects of
|
||||
* @param effects The array of {@link PotionEffect}s to set
|
||||
*/
|
||||
private static void setPlayerPotionEffects(Player player, PotionEffect[] effects) {
|
||||
for (PotionEffect effect : player.getActivePotionEffects()) {
|
||||
player.removePotionEffect(effect.getType());
|
||||
}
|
||||
for (PotionEffect effect : effects) {
|
||||
player.addPotionEffect(effect);
|
||||
}
|
||||
}
|
||||
|
||||
private static void nativeSyncPlayerAdvancements(final Player player, final List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> advancementRecords) {
|
||||
final Object playerAdvancements = AdvancementUtils.getPlayerAdvancements(player);
|
||||
|
||||
// Clear
|
||||
AdvancementUtils.clearPlayerAdvancements(playerAdvancements);
|
||||
AdvancementUtils.clearVisibleAdvancements(playerAdvancements);
|
||||
|
||||
advancementRecords.forEach(advancementRecord -> {
|
||||
NamespacedKey namespacedKey = Objects.requireNonNull(
|
||||
NamespacedKey.fromString(advancementRecord.key()),
|
||||
"Invalid Namespaced key of " + advancementRecord.key()
|
||||
);
|
||||
|
||||
Advancement bukkitAdvancement = Bukkit.getAdvancement(namespacedKey);
|
||||
if (bukkitAdvancement == null) {
|
||||
plugin.getLogger().log(Level.WARNING, "Ignored advancement '{0}' - it doesn't exist anymore?", namespacedKey);
|
||||
return;
|
||||
}
|
||||
|
||||
Object advancement = AdvancementUtils.getHandle(bukkitAdvancement);
|
||||
Map<String, Date> criteriaList = advancementRecord.criteriaMap();
|
||||
{
|
||||
Map<String, Object> nativeCriteriaMap = new HashMap<>();
|
||||
criteriaList.forEach((criteria, date) ->
|
||||
nativeCriteriaMap.put(criteria, AdvancementUtils.newCriterionProgress(date))
|
||||
);
|
||||
Object nativeAdvancementProgress = AdvancementUtils.newAdvancementProgress(nativeCriteriaMap);
|
||||
|
||||
AdvancementUtils.startProgress(playerAdvancements, advancement, nativeAdvancementProgress);
|
||||
}
|
||||
});
|
||||
AdvancementUtils.ensureAllVisible(playerAdvancements); // Set all completed advancement is visible
|
||||
AdvancementUtils.markPlayerAdvancementsFirst(playerAdvancements); // Mark the sending of visible advancement as the first
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a player's advancements and progress to match the advancementData
|
||||
*
|
||||
* @param player The player to set the advancements of
|
||||
* @param advancementData The ArrayList of {@link me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate}s to set
|
||||
*/
|
||||
private static void setPlayerAdvancements(Player player, List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> advancementData, PlayerData data) {
|
||||
// Temporarily disable advancement announcing if needed
|
||||
boolean announceAdvancementUpdate = false;
|
||||
if (Boolean.TRUE.equals(player.getWorld().getGameRuleValue(GameRule.ANNOUNCE_ADVANCEMENTS))) {
|
||||
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false);
|
||||
announceAdvancementUpdate = true;
|
||||
}
|
||||
final boolean finalAnnounceAdvancementUpdate = announceAdvancementUpdate;
|
||||
|
||||
// Run async because advancement loading is very slow
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
|
||||
// Apply the advancements to the player
|
||||
final Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
|
||||
while (serverAdvancements.hasNext()) { // Iterate through all advancements
|
||||
boolean correctExperienceCheck = false; // Determines whether the experience might have changed warranting an update
|
||||
Advancement advancement = serverAdvancements.next();
|
||||
AdvancementProgress playerProgress = player.getAdvancementProgress(advancement);
|
||||
for (me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate record : advancementData) {
|
||||
// If the advancement is one on the data
|
||||
if (record.key().equals(advancement.getKey().getNamespace() + ":" + advancement.getKey().getKey())) {
|
||||
|
||||
// Award all criteria that the player does not have that they do on the cache
|
||||
ArrayList<String> currentlyAwardedCriteria = new ArrayList<>(playerProgress.getAwardedCriteria());
|
||||
for (String awardCriteria : record.criteriaMap().keySet()) {
|
||||
if (!playerProgress.getAwardedCriteria().contains(awardCriteria)) {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> player.getAdvancementProgress(advancement).awardCriteria(awardCriteria));
|
||||
correctExperienceCheck = true;
|
||||
}
|
||||
currentlyAwardedCriteria.remove(awardCriteria);
|
||||
}
|
||||
|
||||
// Revoke all criteria that the player does have but should not
|
||||
for (String awardCriteria : currentlyAwardedCriteria) {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> player.getAdvancementProgress(advancement).revokeCriteria(awardCriteria));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the player's experience in case the advancement changed that
|
||||
if (correctExperienceCheck) {
|
||||
if (Settings.syncExperience) {
|
||||
setPlayerExperience(player, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable announcing advancements (back on main thread again)
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (finalAnnounceAdvancementUpdate) {
|
||||
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a player's statistics (in the Statistic menu)
|
||||
*
|
||||
* @param player The player to set the statistics of
|
||||
* @param statisticData The {@link me.william278.husksync.bukkit.data.DataSerializer.StatisticData} to set
|
||||
*/
|
||||
private static void setPlayerStatistics(Player player, me.william278.husksync.bukkit.data.DataSerializer.StatisticData statisticData) {
|
||||
// Set untyped statistics
|
||||
for (Statistic statistic : statisticData.untypedStatisticValues().keySet()) {
|
||||
player.setStatistic(statistic, statisticData.untypedStatisticValues().get(statistic));
|
||||
}
|
||||
|
||||
// Set block statistics
|
||||
for (Statistic statistic : statisticData.blockStatisticValues().keySet()) {
|
||||
for (Material blockMaterial : statisticData.blockStatisticValues().get(statistic).keySet()) {
|
||||
player.setStatistic(statistic, blockMaterial, statisticData.blockStatisticValues().get(statistic).get(blockMaterial));
|
||||
}
|
||||
}
|
||||
|
||||
// Set item statistics
|
||||
for (Statistic statistic : statisticData.itemStatisticValues().keySet()) {
|
||||
for (Material itemMaterial : statisticData.itemStatisticValues().get(statistic).keySet()) {
|
||||
player.setStatistic(statistic, itemMaterial, statisticData.itemStatisticValues().get(statistic).get(itemMaterial));
|
||||
}
|
||||
}
|
||||
|
||||
// Set entity statistics
|
||||
for (Statistic statistic : statisticData.entityStatisticValues().keySet()) {
|
||||
for (EntityType entityType : statisticData.entityStatisticValues().get(statistic).keySet()) {
|
||||
player.setStatistic(statistic, entityType, statisticData.entityStatisticValues().get(statistic).get(entityType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a player's exp level, exp points & score
|
||||
*
|
||||
* @param player The {@link Player} to set
|
||||
* @param data The {@link PlayerData} to set them
|
||||
*/
|
||||
private static void setPlayerExperience(Player player, PlayerData data) {
|
||||
player.setTotalExperience(data.getTotalExperience());
|
||||
player.setLevel(data.getExpLevel());
|
||||
player.setExp(data.getExpProgress());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a player's location from {@link me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation} data
|
||||
*
|
||||
* @param player The {@link Player} to teleport
|
||||
* @param location The {@link me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation}
|
||||
*/
|
||||
private static void setPlayerLocation(Player player, me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation location) {
|
||||
// Don't teleport if the location is invalid
|
||||
if (location == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the world; if the names match, use that
|
||||
World world = Bukkit.getWorld(location.worldName());
|
||||
if (world == null) {
|
||||
|
||||
// If the names don't match, find the corresponding world with the same dimension environment
|
||||
for (World worldOnServer : Bukkit.getWorlds()) {
|
||||
if (worldOnServer.getEnvironment().equals(location.environment())) {
|
||||
world = worldOnServer;
|
||||
}
|
||||
}
|
||||
|
||||
// If that still fails, return
|
||||
if (world == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Teleport the player
|
||||
player.teleport(new Location(world, location.x(), location.y(), location.z(), location.yaw(), location.pitch()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Correctly set a {@link Player}'s health data
|
||||
*
|
||||
* @param player The {@link Player} to set
|
||||
* @param health Health to set to the player
|
||||
* @param maxHealth Max health to set to the player
|
||||
* @param healthScale Health scaling to apply to the player
|
||||
*/
|
||||
private static void setPlayerHealth(Player player, double health, double maxHealth, double healthScale) {
|
||||
// Set max health
|
||||
if (maxHealth != 0D) {
|
||||
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).setBaseValue(maxHealth);
|
||||
}
|
||||
|
||||
// Set health
|
||||
double currentHealth = player.getHealth();
|
||||
if (health != currentHealth) player.setHealth(currentHealth > maxHealth ? maxHealth : health);
|
||||
|
||||
// Set health scaling if needed
|
||||
if (healthScale != 0D) {
|
||||
player.setHealthScale(healthScale);
|
||||
} else {
|
||||
player.setHealthScale(maxHealth);
|
||||
}
|
||||
player.setHealthScaled(healthScale != 0D);
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package net.william278.husksync.bukkit.util.nms;
|
||||
|
||||
import net.william278.husksync.util.ThrowSupplier;
|
||||
import org.bukkit.advancement.Advancement;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class AdvancementUtils {
|
||||
|
||||
public final static Class<?> PLAYER_ADVANCEMENT;
|
||||
private final static Field PLAYER_ADVANCEMENTS_MAP;
|
||||
private final static Field PLAYER_VISIBLE_SET;
|
||||
private final static Field PLAYER_ADVANCEMENTS;
|
||||
private final static Field CRITERIA_MAP;
|
||||
private final static Field CRITERIA_DATE;
|
||||
private final static Field IS_FIRST_PACKET;
|
||||
private final static Method GET_HANDLE;
|
||||
private final static Method START_PROGRESS;
|
||||
private final static Method ENSURE_ALL_VISIBLE;
|
||||
private final static Class<?> ADVANCEMENT_PROGRESS;
|
||||
private final static Class<?> CRITERION_PROGRESS;
|
||||
|
||||
static {
|
||||
Class<?> SERVER_PLAYER = MinecraftVersionUtils.getMinecraftClass("level.EntityPlayer");
|
||||
PLAYER_ADVANCEMENTS = ThrowSupplier.get(() -> SERVER_PLAYER.getDeclaredField("cs"));
|
||||
PLAYER_ADVANCEMENTS.setAccessible(true);
|
||||
|
||||
Class<?> CRAFT_ADVANCEMENT = MinecraftVersionUtils.getBukkitClass("advancement.CraftAdvancement");
|
||||
GET_HANDLE = ThrowSupplier.get(() -> CRAFT_ADVANCEMENT.getDeclaredMethod("getHandle"));
|
||||
|
||||
ADVANCEMENT_PROGRESS = ThrowSupplier.get(() -> Class.forName("net.minecraft.advancements.AdvancementProgress"));
|
||||
CRITERIA_MAP = ThrowSupplier.get(() -> ADVANCEMENT_PROGRESS.getDeclaredField("a"));
|
||||
CRITERIA_MAP.setAccessible(true);
|
||||
|
||||
CRITERION_PROGRESS = ThrowSupplier.get(() -> Class.forName("net.minecraft.advancements.CriterionProgress"));
|
||||
CRITERIA_DATE = ThrowSupplier.get(() -> CRITERION_PROGRESS.getDeclaredField("b"));
|
||||
CRITERIA_DATE.setAccessible(true);
|
||||
|
||||
Class<?> ADVANCEMENT = ThrowSupplier.get(() -> Class.forName("net.minecraft.advancements.Advancement"));
|
||||
|
||||
PLAYER_ADVANCEMENT = MinecraftVersionUtils.getMinecraftClass("AdvancementDataPlayer");
|
||||
PLAYER_ADVANCEMENTS_MAP = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredField("h"));
|
||||
PLAYER_ADVANCEMENTS_MAP.setAccessible(true);
|
||||
|
||||
PLAYER_VISIBLE_SET = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredField("i"));
|
||||
PLAYER_VISIBLE_SET.setAccessible(true);
|
||||
|
||||
START_PROGRESS = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredMethod("a", ADVANCEMENT, ADVANCEMENT_PROGRESS));
|
||||
START_PROGRESS.setAccessible(true);
|
||||
|
||||
ENSURE_ALL_VISIBLE = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredMethod("c"));
|
||||
ENSURE_ALL_VISIBLE.setAccessible(true);
|
||||
|
||||
IS_FIRST_PACKET = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredField("n"));
|
||||
IS_FIRST_PACKET.setAccessible(true);
|
||||
}
|
||||
|
||||
public static void markPlayerAdvancementsFirst(final Object playerAdvancements) {
|
||||
try {
|
||||
IS_FIRST_PACKET.set(playerAdvancements, true);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Object getPlayerAdvancements(Player player) {
|
||||
Object nativePlayer = EntityUtils.getHandle(player);
|
||||
try {
|
||||
return PLAYER_ADVANCEMENTS.get(nativePlayer);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearPlayerAdvancements(final Object playerAdvancement) {
|
||||
try {
|
||||
((Map<?, ?>) PLAYER_ADVANCEMENTS_MAP.get(playerAdvancement))
|
||||
.clear();
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Object getHandle(Advancement advancement) {
|
||||
try {
|
||||
return GET_HANDLE.invoke(advancement);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Object newCriterionProgress(final Date date) {
|
||||
try {
|
||||
Object nativeCriterionProgress = CRITERION_PROGRESS.getDeclaredConstructor().newInstance();
|
||||
CRITERIA_DATE.set(nativeCriterionProgress, date);
|
||||
return nativeCriterionProgress;
|
||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked") // Suppress unchecked cast warnings here
|
||||
public static Object newAdvancementProgress(final Map<String, Object> criteria) {
|
||||
try {
|
||||
Object nativeAdvancementProgress = ADVANCEMENT_PROGRESS.getDeclaredConstructor().newInstance();
|
||||
|
||||
final Map<String, Object> criteriaMap = (Map<String, Object>) CRITERIA_MAP.get(nativeAdvancementProgress);
|
||||
criteriaMap.putAll(criteria);
|
||||
|
||||
return nativeAdvancementProgress;
|
||||
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void startProgress(final Object playerAdvancements, final Object advancement, final Object advancementProgress) {
|
||||
try {
|
||||
START_PROGRESS.invoke(playerAdvancements, advancement, advancementProgress);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ensureAllVisible(final Object playerAdvancements) {
|
||||
try {
|
||||
ENSURE_ALL_VISIBLE.invoke(playerAdvancements);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearVisibleAdvancements(final Object playerAdvancements) {
|
||||
try {
|
||||
((Set<?>) PLAYER_VISIBLE_SET.get(playerAdvancements))
|
||||
.clear();
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package net.william278.husksync.bukkit.util.nms;
|
||||
|
||||
import net.william278.husksync.util.ThrowSupplier;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class EntityUtils {
|
||||
|
||||
private final static Method GET_HANDLE;
|
||||
|
||||
static {
|
||||
final Class<?> CRAFT_ENTITY = MinecraftVersionUtils.getBukkitClass("entity.CraftEntity");
|
||||
GET_HANDLE = ThrowSupplier.get(() -> CRAFT_ENTITY.getDeclaredMethod("getHandle"));
|
||||
}
|
||||
|
||||
public static Object getHandle(LivingEntity livingEntity) throws RuntimeException {
|
||||
try {
|
||||
return GET_HANDLE.invoke(livingEntity);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package net.william278.husksync.bukkit.util.nms;
|
||||
|
||||
import net.william278.husksync.util.ThrowSupplier;
|
||||
import net.william278.husksync.util.VersionUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
public class MinecraftVersionUtils {
|
||||
|
||||
public final static String CRAFTBUKKIT_PACKAGE_PATH = Bukkit.getServer().getClass().getPackage().getName();
|
||||
|
||||
public final static String PACKAGE_VERSION = CRAFTBUKKIT_PACKAGE_PATH.split("\\.")[3];
|
||||
public final static VersionUtils.Version SERVER_VERSION
|
||||
= VersionUtils.Version.of(Bukkit.getBukkitVersion().split("-")[0]);
|
||||
public final static String MINECRAFT_PACKAGE = SERVER_VERSION.compareTo(VersionUtils.Version.of("1.17")) < 0 ?
|
||||
"net.minecraft.server.".concat(PACKAGE_VERSION) : "net.minecraft.server";
|
||||
|
||||
public static Class<?> getBukkitClass(String path) {
|
||||
return ThrowSupplier.get(() -> Class.forName(CRAFTBUKKIT_PACKAGE_PATH.concat(".").concat(path)));
|
||||
}
|
||||
|
||||
public static Class<?> getMinecraftClass(String path) {
|
||||
return ThrowSupplier.get(() -> Class.forName(MINECRAFT_PACKAGE.concat(".").concat(path)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import me.lucko.commodore.CommodoreProvider;
|
||||
import me.lucko.commodore.file.CommodoreFileReader;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Used for registering Brigadier hooks on platforms that support commodore for rich command syntax
|
||||
*/
|
||||
public class BrigadierUtil {
|
||||
|
||||
protected static void registerCommodore(@NotNull BukkitHuskSync plugin, @NotNull PluginCommand pluginCommand,
|
||||
@NotNull CommandBase command) {
|
||||
// Register command descriptions via commodore (brigadier wrapper)
|
||||
try (InputStream pluginFile = plugin.getResourceReader()
|
||||
.getResource("commodore/" + command.command + ".commodore")) {
|
||||
CommodoreProvider.getCommodore(plugin).register(pluginCommand,
|
||||
CommodoreFileReader.INSTANCE.parse(pluginFile),
|
||||
player -> player.hasPermission(command.permission));
|
||||
} catch (IOException e) {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE,
|
||||
"Failed to load " + command.command + ".commodore command definitions", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import me.lucko.commodore.CommodoreProvider;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import org.bukkit.command.*;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Bukkit executor that implements and executes {@link CommandBase}s
|
||||
*/
|
||||
public class BukkitCommand implements CommandExecutor, TabExecutor {
|
||||
|
||||
/**
|
||||
* The {@link CommandBase} that will be executed
|
||||
*/
|
||||
protected final CommandBase command;
|
||||
|
||||
/**
|
||||
* The implementing plugin
|
||||
*/
|
||||
private final BukkitHuskSync plugin;
|
||||
|
||||
public BukkitCommand(@NotNull CommandBase command, @NotNull BukkitHuskSync implementor) {
|
||||
this.command = command;
|
||||
this.plugin = implementor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link PluginCommand} to this implementation
|
||||
*
|
||||
* @param pluginCommand {@link PluginCommand} to register
|
||||
*/
|
||||
public void register(@NotNull PluginCommand pluginCommand) {
|
||||
pluginCommand.setExecutor(this);
|
||||
pluginCommand.setTabCompleter(this);
|
||||
pluginCommand.setPermission(command.permission);
|
||||
pluginCommand.setDescription(command.getDescription());
|
||||
if (CommodoreProvider.isSupported()) {
|
||||
BrigadierUtil.registerCommodore(plugin, pluginCommand, command);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
|
||||
@NotNull String label, @NotNull String[] args) {
|
||||
if (sender instanceof Player player) {
|
||||
this.command.onExecute(BukkitPlayer.adapt(player), args);
|
||||
} else {
|
||||
if (this.command instanceof ConsoleExecutable consoleExecutable) {
|
||||
consoleExecutable.onConsoleExecute(args);
|
||||
} else {
|
||||
plugin.getLocales().getLocale("error_in_game_command_only").
|
||||
ifPresent(locale -> sender.spigot().sendMessage(locale.toComponent()));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
|
||||
@NotNull String alias, @NotNull String[] args) {
|
||||
if (this.command instanceof TabCompletable tabCompletable) {
|
||||
return tabCompletable.onTabComplete(args);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Commands available on the Bukkit HuskSync implementation
|
||||
*/
|
||||
public enum BukkitCommandType {
|
||||
|
||||
HUSKSYNC_COMMAND(new HuskSyncCommand(BukkitHuskSync.getInstance())),
|
||||
USERDATA_COMMAND(new UserDataCommand(BukkitHuskSync.getInstance())),
|
||||
INVENTORY_COMMAND(new InventoryCommand(BukkitHuskSync.getInstance())),
|
||||
ENDER_CHEST_COMMAND(new EnderChestCommand(BukkitHuskSync.getInstance()));
|
||||
|
||||
public final CommandBase commandBase;
|
||||
|
||||
BukkitCommandType(@NotNull CommandBase commandBase) {
|
||||
this.commandBase = commandBase;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A mapped player inventory, providing methods to easily access a player's inventory.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitInventoryMap {
|
||||
|
||||
private ItemStack[] contents;
|
||||
|
||||
/**
|
||||
* Creates a new mapped inventory from the given contents.
|
||||
*
|
||||
* @param contents the contents of the inventory
|
||||
*/
|
||||
protected BukkitInventoryMap(ItemStack[] contents) {
|
||||
this.contents = contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the contents of the inventory.
|
||||
*
|
||||
* @return the contents of the inventory
|
||||
*/
|
||||
public ItemStack[] getContents() {
|
||||
return contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the contents of the inventory.
|
||||
*
|
||||
* @param contents the contents of the inventory
|
||||
*/
|
||||
public void setContents(ItemStack[] contents) {
|
||||
this.contents = contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size of the inventory.
|
||||
*
|
||||
* @return the size of the inventory
|
||||
*/
|
||||
public int getSize() {
|
||||
return contents.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the item at the given index.
|
||||
*
|
||||
* @param index the index of the item to get
|
||||
* @return the item at the given index
|
||||
*/
|
||||
public Optional<ItemStack> getItemAt(int index) {
|
||||
if (contents.length >= index) {
|
||||
if (contents[index] == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(contents[index]);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the item at the given index.
|
||||
*
|
||||
* @param itemStack the item to set at the given index
|
||||
* @param index the index of the item to set
|
||||
* @throws IllegalArgumentException if the index is out of bounds
|
||||
*/
|
||||
public void setItemAt(@NotNull ItemStack itemStack, int index) throws IllegalArgumentException {
|
||||
contents[index] = itemStack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the main inventory contents.
|
||||
*
|
||||
* @return the main inventory contents
|
||||
*/
|
||||
public ItemStack[] getInventory() {
|
||||
final ItemStack[] inventory = new ItemStack[36];
|
||||
System.arraycopy(contents, 0, inventory, 0, Math.min(contents.length, inventory.length));
|
||||
return inventory;
|
||||
}
|
||||
|
||||
public ItemStack[] getHotbar() {
|
||||
final ItemStack[] armor = new ItemStack[9];
|
||||
for (int i = 0; i <= 9; i++) {
|
||||
armor[i] = getItemAt(i).orElse(null);
|
||||
}
|
||||
return armor;
|
||||
}
|
||||
|
||||
public Optional<ItemStack> getOffHand() {
|
||||
return getItemAt(40);
|
||||
}
|
||||
|
||||
public Optional<ItemStack> getHelmet() {
|
||||
return getItemAt(39);
|
||||
}
|
||||
|
||||
public Optional<ItemStack> getChestplate() {
|
||||
return getItemAt(38);
|
||||
}
|
||||
|
||||
public Optional<ItemStack> getLeggings() {
|
||||
return getItemAt(37);
|
||||
}
|
||||
|
||||
public Optional<ItemStack> getBoots() {
|
||||
return getItemAt(36);
|
||||
}
|
||||
|
||||
public ItemStack[] getArmor() {
|
||||
final ItemStack[] armor = new ItemStack[4];
|
||||
for (int i = 36; i < 40; i++) {
|
||||
armor[i - 36] = getItemAt(i).orElse(null);
|
||||
}
|
||||
return armor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class BukkitSerializer {
|
||||
|
||||
/**
|
||||
* Returns a serialized array of {@link ItemStack}s
|
||||
*
|
||||
* @param inventoryContents The contents of the inventory
|
||||
* @return The serialized inventory contents
|
||||
*/
|
||||
public static CompletableFuture<String> serializeItemStackArray(@NotNull ItemStack[] inventoryContents)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Return an empty string if there is no inventory item data to serialize
|
||||
if (inventoryContents.length == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Create an output stream that will be encoded into base 64
|
||||
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
|
||||
|
||||
try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
|
||||
// Define the length of the inventory array to serialize
|
||||
bukkitOutputStream.writeInt(inventoryContents.length);
|
||||
|
||||
// Write each serialize each ItemStack to the output stream
|
||||
for (ItemStack inventoryItem : inventoryContents) {
|
||||
bukkitOutputStream.writeObject(serializeItemStack(inventoryItem));
|
||||
}
|
||||
|
||||
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
|
||||
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
|
||||
} catch (IOException e) {
|
||||
throw new DataSerializationException("Failed to serialize item stack data", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link BukkitInventoryMap} from a serialized array of ItemStacks representing the contents of a player's inventory.
|
||||
*
|
||||
* @param serializedPlayerInventory The serialized {@link ItemStack} inventory array
|
||||
* @return The deserialized ItemStacks, mapped for convenience as a {@link BukkitInventoryMap}
|
||||
* @throws DataSerializationException If the serialized item stack array could not be deserialized
|
||||
*/
|
||||
public static CompletableFuture<BukkitInventoryMap> deserializeInventory(@NotNull String serializedPlayerInventory)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> new BukkitInventoryMap(deserializeItemStackArray(serializedPlayerInventory).join()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of ItemStacks from serialized inventory data.
|
||||
*
|
||||
* @param serializeItemStackArray The serialized {@link ItemStack} array
|
||||
* @return The deserialized array of {@link ItemStack}s
|
||||
* @throws DataSerializationException If the serialized item stack array could not be deserialized
|
||||
* @implNote Empty slots will be represented by {@code null}
|
||||
*/
|
||||
public static CompletableFuture<ItemStack[]> deserializeItemStackArray(@NotNull String serializeItemStackArray)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Return empty array if there is no inventory data (set the player as having an empty inventory)
|
||||
if (serializeItemStackArray.isEmpty()) {
|
||||
return new ItemStack[0];
|
||||
}
|
||||
|
||||
// Create a byte input stream to read the serialized data
|
||||
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(serializeItemStackArray))) {
|
||||
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
|
||||
// Read the length of the Bukkit input stream and set the length of the array to this value
|
||||
ItemStack[] inventoryContents = new ItemStack[bukkitInputStream.readInt()];
|
||||
|
||||
// Set the ItemStacks in the array from deserialized ItemStack data
|
||||
int slotIndex = 0;
|
||||
for (ItemStack ignored : inventoryContents) {
|
||||
inventoryContents[slotIndex] = deserializeItemStack(bukkitInputStream.readObject());
|
||||
slotIndex++;
|
||||
}
|
||||
|
||||
// Return the finished, serialized inventory contents
|
||||
return inventoryContents;
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
throw new DataSerializationException("Failed to deserialize item stack data", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized version of an {@link ItemStack} as a string to object Map
|
||||
*
|
||||
* @param item The {@link ItemStack} to serialize
|
||||
* @return The serialized {@link ItemStack}
|
||||
*/
|
||||
@Nullable
|
||||
private static Map<String, Object> serializeItemStack(@Nullable ItemStack item) {
|
||||
return item != null ? item.serialize() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deserialized {@link ItemStack} from the Object read from the {@link BukkitObjectInputStream}
|
||||
*
|
||||
* @param serializedItemStack The serialized item stack; a String-Object map
|
||||
* @return The deserialized {@link ItemStack}
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
|
||||
@Nullable
|
||||
private static ItemStack deserializeItemStack(@Nullable Object serializedItemStack) {
|
||||
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized array of {@link PotionEffect}s
|
||||
*
|
||||
* @param potionEffects The potion effect array
|
||||
* @return The serialized potion effects
|
||||
*/
|
||||
public static CompletableFuture<String> serializePotionEffectArray(@NotNull PotionEffect[] potionEffects) throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Return an empty string if there are no effects to serialize
|
||||
if (potionEffects.length == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Create an output stream that will be encoded into base 64
|
||||
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
|
||||
|
||||
try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
|
||||
// Define the length of the potion effect array to serialize
|
||||
bukkitOutputStream.writeInt(potionEffects.length);
|
||||
|
||||
// Write each serialize each PotionEffect to the output stream
|
||||
for (PotionEffect potionEffect : potionEffects) {
|
||||
bukkitOutputStream.writeObject(serializePotionEffect(potionEffect));
|
||||
}
|
||||
|
||||
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
|
||||
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
|
||||
} catch (IOException e) {
|
||||
throw new DataSerializationException("Failed to serialize potion effect data", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of ItemStacks from serialized potion effect data
|
||||
*
|
||||
* @param potionEffectData The serialized {@link PotionEffect} array
|
||||
* @return The {@link PotionEffect}s
|
||||
*/
|
||||
public static CompletableFuture<PotionEffect[]> deserializePotionEffectArray(@NotNull String potionEffectData) throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Return empty array if there is no potion effect data (don't apply any effects to the player)
|
||||
if (potionEffectData.isEmpty()) {
|
||||
return new PotionEffect[0];
|
||||
}
|
||||
|
||||
// Create a byte input stream to read the serialized data
|
||||
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(potionEffectData))) {
|
||||
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
|
||||
// Read the length of the Bukkit input stream and set the length of the array to this value
|
||||
PotionEffect[] potionEffects = new PotionEffect[bukkitInputStream.readInt()];
|
||||
|
||||
// Set the potion effects in the array from deserialized PotionEffect data
|
||||
int potionIndex = 0;
|
||||
for (PotionEffect ignored : potionEffects) {
|
||||
potionEffects[potionIndex] = deserializePotionEffect(bukkitInputStream.readObject());
|
||||
potionIndex++;
|
||||
}
|
||||
|
||||
// Return the finished, serialized potion effect array
|
||||
return potionEffects;
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
throw new DataSerializationException("Failed to deserialize potion effects", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized version of an {@link ItemStack} as a string to object Map
|
||||
*
|
||||
* @param potionEffect The {@link ItemStack} to serialize
|
||||
* @return The serialized {@link ItemStack}
|
||||
*/
|
||||
@Nullable
|
||||
private static Map<String, Object> serializePotionEffect(@Nullable PotionEffect potionEffect) {
|
||||
return potionEffect != null ? potionEffect.serialize() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deserialized {@link PotionEffect} from the Object read from the {@link BukkitObjectInputStream}
|
||||
*
|
||||
* @param serializedPotionEffect The serialized potion effect; a String-Object map
|
||||
* @return The deserialized {@link PotionEffect}
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
|
||||
@Nullable
|
||||
private static PotionEffect deserializePotionEffect(@Nullable Object serializedPotionEffect) {
|
||||
return serializedPotionEffect != null ? new PotionEffect((Map<String, Object>) serializedPotionEffect) : null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, Cancellable {
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
private boolean cancelled = false;
|
||||
private UserData userData;
|
||||
private final User user;
|
||||
private final DataSaveCause saveCause;
|
||||
|
||||
protected BukkitDataSaveEvent(@NotNull User user, @NotNull UserData userData,
|
||||
@NotNull DataSaveCause saveCause) {
|
||||
this.user = user;
|
||||
this.userData = userData;
|
||||
this.saveCause = saveCause;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCancelled(boolean cancelled) {
|
||||
this.cancelled = cancelled;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull UserData getUserData() {
|
||||
return userData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserData(@NotNull UserData userData) {
|
||||
this.userData = userData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull DataSaveCause getSaveCause() {
|
||||
return saveCause;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class BukkitEvent extends Event implements net.william278.husksync.event.Event {
|
||||
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
|
||||
protected BukkitEvent() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<net.william278.husksync.event.Event> fire() {
|
||||
final CompletableFuture<net.william278.husksync.event.Event> eventFireFuture = new CompletableFuture<>();
|
||||
// Don't fire events while the server is shutting down
|
||||
if (!BukkitHuskSync.getInstance().isEnabled()) {
|
||||
eventFireFuture.complete(this);
|
||||
} else {
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
Bukkit.getServer().getPluginManager().callEvent(this);
|
||||
eventFireFuture.complete(this);
|
||||
});
|
||||
}
|
||||
return eventFireFuture;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class BukkitEventCannon extends EventCannon {
|
||||
|
||||
public BukkitEventCannon() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Event> firePreSyncEvent(@NotNull OnlineUser user, @NotNull UserData userData) {
|
||||
return new BukkitPreSyncEvent(((BukkitPlayer) user).getPlayer(), userData).fire();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Event> fireDataSaveEvent(@NotNull User user, @NotNull UserData userData,
|
||||
@NotNull DataSaveCause saveCause) {
|
||||
return new BukkitDataSaveEvent(user, userData, saveCause).fire();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fireSyncCompleteEvent(@NotNull OnlineUser user) {
|
||||
new BukkitSyncCompleteEvent(((BukkitPlayer) user).getPlayer()).fire();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class BukkitPlayerEvent extends BukkitEvent implements PlayerEvent {
|
||||
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
|
||||
protected final Player player;
|
||||
|
||||
protected BukkitPlayerEvent(@NotNull Player player) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OnlineUser getUser() {
|
||||
return BukkitPlayer.adapt(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Event> fire() {
|
||||
final CompletableFuture<Event> eventFireFuture = new CompletableFuture<>();
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
Bukkit.getServer().getPluginManager().callEvent(this);
|
||||
eventFireFuture.complete(this);
|
||||
});
|
||||
return eventFireFuture;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.data.UserData;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEvent, Cancellable {
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
private boolean cancelled = false;
|
||||
private UserData userData;
|
||||
|
||||
protected BukkitPreSyncEvent(@NotNull Player player, @NotNull UserData userData) {
|
||||
super(player);
|
||||
this.userData = userData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCancelled(boolean cancelled) {
|
||||
this.cancelled = cancelled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull UserData getUserData() {
|
||||
return userData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserData(@NotNull UserData userData) {
|
||||
this.userData = userData;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitSyncCompleteEvent extends BukkitPlayerEvent implements SyncCompleteEvent {
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
|
||||
protected BukkitSyncCompleteEvent(@NotNull Player player) {
|
||||
super(player);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.data.BukkitSerializer;
|
||||
import net.william278.husksync.data.DataSerializationException;
|
||||
import net.william278.husksync.data.ItemData;
|
||||
import net.william278.husksync.editor.ItemEditorMenuType;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.entity.EntityPickupItemEvent;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||
import org.bukkit.event.player.PlayerDropItemEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.world.WorldSaveEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BukkitEventListener extends EventListener implements Listener {
|
||||
|
||||
public BukkitEventListener(@NotNull BukkitHuskSync huskSync) {
|
||||
super(huskSync);
|
||||
Bukkit.getServer().getPluginManager().registerEvents(this, huskSync);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPlayerJoin(@NotNull PlayerJoinEvent event) {
|
||||
super.handlePlayerJoin(BukkitPlayer.adapt(event.getPlayer()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPlayerQuit(@NotNull PlayerQuitEvent event) {
|
||||
super.handlePlayerQuit(BukkitPlayer.adapt(event.getPlayer()));
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onWorldSave(@NotNull WorldSaveEvent event) {
|
||||
CompletableFuture.runAsync(() -> super.handleAsyncWorldSave(event.getWorld().getPlayers().stream()
|
||||
.map(BukkitPlayer::adapt).collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onInventoryClose(@NotNull InventoryCloseEvent event) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
if (event.getPlayer() instanceof Player player) {
|
||||
final OnlineUser user = BukkitPlayer.adapt(player);
|
||||
plugin.getDataEditor().getEditingInventoryData(user).ifPresent(menu -> {
|
||||
try {
|
||||
BukkitSerializer.serializeItemStackArray(Arrays.copyOf(event.getInventory().getContents(),
|
||||
menu.itemEditorMenuType == ItemEditorMenuType.INVENTORY_VIEWER
|
||||
? player.getInventory().getSize()
|
||||
: player.getEnderChest().getSize())).thenAccept(
|
||||
serializedInventory -> super.handleMenuClose(user, new ItemData(serializedInventory)));
|
||||
} catch (DataSerializationException e) {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE,
|
||||
"Failed to serialize inventory data during menu close", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Events to cancel if the player has not been set yet
|
||||
*/
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onDropItem(@NotNull PlayerDropItemEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer())));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPickupItem(@NotNull EntityPickupItemEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(player)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteract(@NotNull PlayerInteractEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer())));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockPlace(@NotNull BlockPlaceEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer())));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockBreak(@NotNull BlockBreakEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer())));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryClick(@NotNull InventoryClickEvent event) {
|
||||
if (event.getWhoClicked() instanceof Player player) {
|
||||
event.setCancelled(cancelInventoryClick(BukkitPlayer.adapt(player)));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
|
||||
if (event.getPlayer() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(player)));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(player)));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onPlayerDeath(PlayerDeathEvent event) {
|
||||
if (cancelPlayerEvent(BukkitPlayer.adapt(event.getEntity()))) {
|
||||
event.getDrops().clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
package net.william278.husksync.migrator;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import me.william278.husksync.bukkit.data.DataSerializer;
|
||||
import net.william278.hslmigrator.HSLConverter;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.*;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class LegacyMigrator extends Migrator {
|
||||
|
||||
private final HSLConverter hslConverter;
|
||||
private String sourceHost;
|
||||
private int sourcePort;
|
||||
private String sourceUsername;
|
||||
private String sourcePassword;
|
||||
private String sourceDatabase;
|
||||
private String sourcePlayersTable;
|
||||
private String sourceDataTable;
|
||||
|
||||
private final String minecraftVersion;
|
||||
|
||||
public LegacyMigrator(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
this.hslConverter = HSLConverter.getInstance();
|
||||
this.sourceHost = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_HOST);
|
||||
this.sourcePort = plugin.getSettings().getIntegerValue(Settings.ConfigOption.DATABASE_PORT);
|
||||
this.sourceUsername = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_USERNAME);
|
||||
this.sourcePassword = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_PASSWORD);
|
||||
this.sourceDatabase = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_NAME);
|
||||
this.sourcePlayersTable = "husksync_players";
|
||||
this.sourceDataTable = "husksync_data";
|
||||
this.minecraftVersion = plugin.getMinecraftVersion().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> start() {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Starting migration of legacy HuskSync v1.x data...");
|
||||
final long startTime = System.currentTimeMillis();
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Wipe the existing database, preparing it for data import
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Preparing existing database (wiping)...");
|
||||
plugin.getDatabase().wipeDatabase().join();
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
|
||||
|
||||
// Create jdbc driver connection url
|
||||
final String jdbcUrl = "jdbc:mysql://" + sourceHost + ":" + sourcePort + "/" + sourceDatabase;
|
||||
|
||||
// Create a new data source for the mpdb converter
|
||||
try (final HikariDataSource connectionPool = new HikariDataSource()) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Establishing connection to legacy database...");
|
||||
connectionPool.setJdbcUrl(jdbcUrl);
|
||||
connectionPool.setUsername(sourceUsername);
|
||||
connectionPool.setPassword(sourcePassword);
|
||||
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase());
|
||||
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Downloading raw data from the legacy database (this might take a while)...");
|
||||
final List<LegacyData> dataToMigrate = new ArrayList<>();
|
||||
try (final Connection connection = connectionPool.getConnection()) {
|
||||
try (final PreparedStatement statement = connection.prepareStatement("""
|
||||
SELECT `uuid`, `username`, `inventory`, `ender_chest`, `health`, `max_health`, `health_scale`, `hunger`, `saturation`, `saturation_exhaustion`, `selected_slot`, `status_effects`, `total_experience`, `exp_level`, `exp_progress`, `game_mode`, `statistics`, `is_flying`, `advancements`, `location`
|
||||
FROM `%source_players_table%`
|
||||
INNER JOIN `%source_data_table%`
|
||||
ON `%source_players_table%`.`id` = `%source_data_table%`.`player_id`
|
||||
WHERE `username` IS NOT NULL;
|
||||
""".replaceAll(Pattern.quote("%source_players_table%"), sourcePlayersTable)
|
||||
.replaceAll(Pattern.quote("%source_data_table%"), sourceDataTable))) {
|
||||
try (final ResultSet resultSet = statement.executeQuery()) {
|
||||
int playersMigrated = 0;
|
||||
while (resultSet.next()) {
|
||||
dataToMigrate.add(new LegacyData(
|
||||
new User(UUID.fromString(resultSet.getString("uuid")),
|
||||
resultSet.getString("username")),
|
||||
resultSet.getString("inventory"),
|
||||
resultSet.getString("ender_chest"),
|
||||
resultSet.getDouble("health"),
|
||||
resultSet.getDouble("max_health"),
|
||||
resultSet.getDouble("health_scale"),
|
||||
resultSet.getInt("hunger"),
|
||||
resultSet.getFloat("saturation"),
|
||||
resultSet.getFloat("saturation_exhaustion"),
|
||||
resultSet.getInt("selected_slot"),
|
||||
resultSet.getString("status_effects"),
|
||||
resultSet.getInt("total_experience"),
|
||||
resultSet.getInt("exp_level"),
|
||||
resultSet.getFloat("exp_progress"),
|
||||
resultSet.getString("game_mode"),
|
||||
resultSet.getString("statistics"),
|
||||
resultSet.getBoolean("is_flying"),
|
||||
resultSet.getString("advancements"),
|
||||
resultSet.getString("location")
|
||||
));
|
||||
playersMigrated++;
|
||||
if (playersMigrated % 50 == 0) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Downloaded legacy data for " + playersMigrated + " players...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the legacy database!");
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Converting HuskSync 1.x data to the new user data format (this might take a while)...");
|
||||
|
||||
final AtomicInteger playersConverted = new AtomicInteger();
|
||||
dataToMigrate.forEach(data -> data.toUserData(hslConverter, minecraftVersion).thenAccept(convertedData -> {
|
||||
plugin.getDatabase().ensureUser(data.user()).thenRun(() ->
|
||||
plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.LEGACY_MIGRATION)
|
||||
.exceptionally(exception -> {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().username + ": " + exception.getMessage());
|
||||
return null;
|
||||
})).join();
|
||||
|
||||
playersConverted.getAndIncrement();
|
||||
if (playersConverted.get() % 50 == 0) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Converted legacy data for " + playersConverted + " players...");
|
||||
}
|
||||
}).join());
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE, "Error while migrating legacy data: " + e.getMessage() + " - are your source database credentials correct?");
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleConfigurationCommand(@NotNull String[] args) {
|
||||
if (args.length == 2) {
|
||||
if (switch (args[0].toLowerCase()) {
|
||||
case "host" -> {
|
||||
this.sourceHost = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "port" -> {
|
||||
try {
|
||||
this.sourcePort = Integer.parseInt(args[1]);
|
||||
yield true;
|
||||
} catch (NumberFormatException e) {
|
||||
yield false;
|
||||
}
|
||||
}
|
||||
case "username" -> {
|
||||
this.sourceUsername = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "password" -> {
|
||||
this.sourcePassword = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "database" -> {
|
||||
this.sourceDatabase = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "players_table" -> {
|
||||
this.sourcePlayersTable = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "data_table" -> {
|
||||
this.sourceDataTable = args[1];
|
||||
yield true;
|
||||
}
|
||||
default -> false;
|
||||
}) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu());
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Successfully set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]));
|
||||
} else {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||
}
|
||||
} else {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
return "legacy";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "HuskSync v1.x --> v2.x Migrator";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getHelpMenu() {
|
||||
return """
|
||||
=== HuskSync v1.x --> v2.x Migration Wizard =========
|
||||
This will migrate all user data from HuskSync v1.x to
|
||||
HuskSync v2.x's new format. To perform the migration,
|
||||
please follow the steps below carefully.
|
||||
|
||||
[!] Existing data in the database will be wiped. [!]
|
||||
|
||||
STEP 1] Please ensure no players are on any servers.
|
||||
|
||||
STEP 2] HuskSync will need to connect to the database
|
||||
used to hold the existing, legacy HuskSync data.
|
||||
If this is the same database as the one you are
|
||||
currently using, you probably don't need to change
|
||||
anything.
|
||||
Please check that the credentials below are the
|
||||
correct credentials of the source legacy HuskSync
|
||||
database.
|
||||
- host: %source_host%
|
||||
- port: %source_port%
|
||||
- username: %source_username%
|
||||
- password: %source_password%
|
||||
- database: %source_database%
|
||||
- players_table: %source_players_table%
|
||||
- data_table: %source_data_table%
|
||||
If any of these are not correct, please correct them
|
||||
using the command:
|
||||
"husksync migrate legacy set <parameter> <value>"
|
||||
(e.g.: "husksync migrate legacy set host 1.2.3.4")
|
||||
|
||||
STEP 3] HuskSync will migrate data into the database
|
||||
tables configures in the config.yml file of this
|
||||
server. Please make sure you're happy with this
|
||||
before proceeding.
|
||||
|
||||
STEP 4] To start the migration, please run:
|
||||
"husksync migrate legacy start"
|
||||
""".replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost))
|
||||
.replaceAll(Pattern.quote("%source_port%"), Integer.toString(sourcePort))
|
||||
.replaceAll(Pattern.quote("%source_username%"), obfuscateDataString(sourceUsername))
|
||||
.replaceAll(Pattern.quote("%source_password%"), obfuscateDataString(sourcePassword))
|
||||
.replaceAll(Pattern.quote("%source_database%"), sourceDatabase)
|
||||
.replaceAll(Pattern.quote("%source_players_table%"), sourcePlayersTable)
|
||||
.replaceAll(Pattern.quote("%source_data_table%"), sourceDataTable);
|
||||
}
|
||||
|
||||
private record LegacyData(@NotNull User user,
|
||||
@NotNull String serializedInventory, @NotNull String serializedEnderChest,
|
||||
double health, double maxHealth, double healthScale, int hunger, float saturation,
|
||||
float saturationExhaustion, int selectedSlot, @NotNull String serializedPotionEffects,
|
||||
int totalExp, int expLevel, float expProgress,
|
||||
@NotNull String gameMode, @NotNull String serializedStatistics, boolean isFlying,
|
||||
@NotNull String serializedAdvancements, @NotNull String serializedLocation) {
|
||||
|
||||
@NotNull
|
||||
public CompletableFuture<UserData> toUserData(@NotNull HSLConverter converter,
|
||||
@NotNull String minecraftVersion) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
final DataSerializer.StatisticData legacyStatisticData = converter
|
||||
.deserializeStatisticData(serializedStatistics);
|
||||
final StatisticsData convertedStatisticData = new StatisticsData(
|
||||
convertStatisticMap(legacyStatisticData.untypedStatisticValues()),
|
||||
convertMaterialStatisticMap(legacyStatisticData.blockStatisticValues()),
|
||||
convertMaterialStatisticMap(legacyStatisticData.itemStatisticValues()),
|
||||
convertEntityStatisticMap(legacyStatisticData.entityStatisticValues()));
|
||||
|
||||
final List<AdvancementData> convertedAdvancements = converter
|
||||
.deserializeAdvancementData(serializedAdvancements)
|
||||
.stream().map(data -> new AdvancementData(data.key(), data.criteriaMap())).toList();
|
||||
|
||||
final DataSerializer.PlayerLocation legacyLocationData = converter
|
||||
.deserializePlayerLocationData(serializedLocation);
|
||||
final LocationData convertedLocationData = new LocationData(
|
||||
legacyLocationData == null ? "world" : legacyLocationData.worldName(),
|
||||
UUID.randomUUID(),
|
||||
"NORMAL",
|
||||
legacyLocationData == null ? 0d : legacyLocationData.x(),
|
||||
legacyLocationData == null ? 64d : legacyLocationData.y(),
|
||||
legacyLocationData == null ? 0d : legacyLocationData.z(),
|
||||
legacyLocationData == null ? 90f : legacyLocationData.yaw(),
|
||||
legacyLocationData == null ? 180f : legacyLocationData.pitch());
|
||||
|
||||
return new UserData(new StatusData(health, maxHealth, healthScale, hunger, saturation,
|
||||
saturationExhaustion, selectedSlot, totalExp, expLevel, expProgress, gameMode, isFlying),
|
||||
new ItemData(serializedInventory), new ItemData(serializedEnderChest),
|
||||
new PotionEffectData(serializedPotionEffects), convertedAdvancements,
|
||||
convertedStatisticData, convertedLocationData,
|
||||
new PersistentDataContainerData(new HashMap<>()),
|
||||
minecraftVersion);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Map<String, Integer> convertStatisticMap(@NotNull HashMap<Statistic, Integer> rawMap) {
|
||||
final HashMap<String, Integer> convertedMap = new HashMap<>();
|
||||
for (Map.Entry<Statistic, Integer> entry : rawMap.entrySet()) {
|
||||
convertedMap.put(entry.getKey().toString(), entry.getValue());
|
||||
}
|
||||
return convertedMap;
|
||||
}
|
||||
|
||||
private Map<String, Map<String, Integer>> convertMaterialStatisticMap(@NotNull HashMap<Statistic, HashMap<Material, Integer>> rawMap) {
|
||||
final Map<String, Map<String, Integer>> convertedMap = new HashMap<>();
|
||||
for (Map.Entry<Statistic, HashMap<Material, Integer>> entry : rawMap.entrySet()) {
|
||||
for (Map.Entry<Material, Integer> materialEntry : entry.getValue().entrySet()) {
|
||||
convertedMap.computeIfAbsent(entry.getKey().toString(), k -> new HashMap<>())
|
||||
.put(materialEntry.getKey().toString(), materialEntry.getValue());
|
||||
}
|
||||
}
|
||||
return convertedMap;
|
||||
}
|
||||
|
||||
private Map<String, Map<String, Integer>> convertEntityStatisticMap(@NotNull HashMap<Statistic, HashMap<EntityType, Integer>> rawMap) {
|
||||
final Map<String, Map<String, Integer>> convertedMap = new HashMap<>();
|
||||
for (Map.Entry<Statistic, HashMap<EntityType, Integer>> entry : rawMap.entrySet()) {
|
||||
for (Map.Entry<EntityType, Integer> materialEntry : entry.getValue().entrySet()) {
|
||||
convertedMap.computeIfAbsent(entry.getKey().toString(), k -> new HashMap<>())
|
||||
.put(materialEntry.getKey().toString(), materialEntry.getValue());
|
||||
}
|
||||
}
|
||||
return convertedMap;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package net.william278.husksync.migrator;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.*;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.mpdbconverter.MPDBConverter;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A migrator for migrating MySQLPlayerDataBridge data to HuskSync {@link UserData}
|
||||
*/
|
||||
public class MpdbMigrator extends Migrator {
|
||||
|
||||
private final MPDBConverter mpdbConverter;
|
||||
private String sourceHost;
|
||||
private int sourcePort;
|
||||
private String sourceUsername;
|
||||
private String sourcePassword;
|
||||
private String sourceDatabase;
|
||||
private String sourceInventoryTable;
|
||||
private String sourceEnderChestTable;
|
||||
private String sourceExperienceTable;
|
||||
private final String minecraftVersion;
|
||||
|
||||
public MpdbMigrator(@NotNull BukkitHuskSync plugin, @NotNull Plugin mySqlPlayerDataBridge) {
|
||||
super(plugin);
|
||||
this.mpdbConverter = MPDBConverter.getInstance(mySqlPlayerDataBridge);
|
||||
this.sourceHost = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_HOST);
|
||||
this.sourcePort = plugin.getSettings().getIntegerValue(Settings.ConfigOption.DATABASE_PORT);
|
||||
this.sourceUsername = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_USERNAME);
|
||||
this.sourcePassword = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_PASSWORD);
|
||||
this.sourceDatabase = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_NAME);
|
||||
this.sourceInventoryTable = "mpdb_inventory";
|
||||
this.sourceEnderChestTable = "mpdb_enderchest";
|
||||
this.sourceExperienceTable = "mpdb_experience";
|
||||
this.minecraftVersion = plugin.getMinecraftVersion().toString();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> start() {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Starting migration from MySQLPlayerDataBridge to HuskSync...");
|
||||
final long startTime = System.currentTimeMillis();
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Wipe the existing database, preparing it for data import
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Preparing existing database (wiping)...");
|
||||
plugin.getDatabase().wipeDatabase().join();
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
|
||||
|
||||
// Create jdbc driver connection url
|
||||
final String jdbcUrl = "jdbc:mysql://" + sourceHost + ":" + sourcePort + "/" + sourceDatabase;
|
||||
|
||||
// Create a new data source for the mpdb converter
|
||||
try (final HikariDataSource connectionPool = new HikariDataSource()) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Establishing connection to MySQLPlayerDataBridge database...");
|
||||
connectionPool.setJdbcUrl(jdbcUrl);
|
||||
connectionPool.setUsername(sourceUsername);
|
||||
connectionPool.setPassword(sourcePassword);
|
||||
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase());
|
||||
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Downloading raw data from the MySQLPlayerDataBridge database (this might take a while)...");
|
||||
final List<MpdbData> dataToMigrate = new ArrayList<>();
|
||||
try (final Connection connection = connectionPool.getConnection()) {
|
||||
try (final PreparedStatement statement = connection.prepareStatement("""
|
||||
SELECT `%source_inventory_table%`.`player_uuid`, `%source_inventory_table%`.`player_name`, `inventory`, `armor`, `enderchest`, `exp_lvl`, `exp`, `total_exp`
|
||||
FROM `%source_inventory_table%`
|
||||
INNER JOIN `%source_ender_chest_table%`
|
||||
ON `%source_inventory_table%`.`player_uuid` = `%source_ender_chest_table%`.`player_uuid`
|
||||
INNER JOIN `%source_xp_table%`
|
||||
ON `%source_inventory_table%`.`player_uuid` = `%source_xp_table%`.`player_uuid`;
|
||||
""".replaceAll(Pattern.quote("%source_inventory_table%"), sourceInventoryTable)
|
||||
.replaceAll(Pattern.quote("%source_ender_chest_table%"), sourceEnderChestTable)
|
||||
.replaceAll(Pattern.quote("%source_xp_table%"), sourceExperienceTable))) {
|
||||
try (final ResultSet resultSet = statement.executeQuery()) {
|
||||
int playersMigrated = 0;
|
||||
while (resultSet.next()) {
|
||||
dataToMigrate.add(new MpdbData(
|
||||
new User(UUID.fromString(resultSet.getString("player_uuid")),
|
||||
resultSet.getString("player_name")),
|
||||
resultSet.getString("inventory"),
|
||||
resultSet.getString("armor"),
|
||||
resultSet.getString("enderchest"),
|
||||
resultSet.getInt("exp_lvl"),
|
||||
resultSet.getInt("exp"),
|
||||
resultSet.getInt("total_exp")
|
||||
));
|
||||
playersMigrated++;
|
||||
if (playersMigrated % 25 == 0) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Downloaded MySQLPlayerDataBridge data for " + playersMigrated + " players...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the MySQLPlayerDataBridge database!");
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Converting raw MySQLPlayerDataBridge data to HuskSync user data (this might take a while)...");
|
||||
|
||||
final AtomicInteger playersConverted = new AtomicInteger();
|
||||
dataToMigrate.forEach(data -> data.toUserData(mpdbConverter, minecraftVersion).thenAccept(convertedData -> {
|
||||
plugin.getDatabase().ensureUser(data.user()).thenRun(() ->
|
||||
plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.MPDB_MIGRATION))
|
||||
.exceptionally(exception -> {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate MySQLPlayerDataBridge data for " + data.user().username + ": " + exception.getMessage());
|
||||
return null;
|
||||
}).join();
|
||||
playersConverted.getAndIncrement();
|
||||
if (playersConverted.get() % 50 == 0) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Converted MySQLPlayerDataBridge data for " + playersConverted + " players...");
|
||||
}
|
||||
}).join());
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE, "Error while migrating data: " + e.getMessage() + " - are your source database credentials correct?");
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleConfigurationCommand(@NotNull String[] args) {
|
||||
if (args.length == 2) {
|
||||
if (switch (args[0].toLowerCase()) {
|
||||
case "host" -> {
|
||||
this.sourceHost = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "port" -> {
|
||||
try {
|
||||
this.sourcePort = Integer.parseInt(args[1]);
|
||||
yield true;
|
||||
} catch (NumberFormatException e) {
|
||||
yield false;
|
||||
}
|
||||
}
|
||||
case "username" -> {
|
||||
this.sourceUsername = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "password" -> {
|
||||
this.sourcePassword = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "database" -> {
|
||||
this.sourceDatabase = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "inventory_table" -> {
|
||||
this.sourceInventoryTable = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "ender_chest_table" -> {
|
||||
this.sourceEnderChestTable = args[1];
|
||||
yield true;
|
||||
}
|
||||
case "experience_table" -> {
|
||||
this.sourceExperienceTable = args[1];
|
||||
yield true;
|
||||
}
|
||||
default -> false;
|
||||
}) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu());
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Successfully set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]));
|
||||
} else {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||
}
|
||||
} else {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
return "mpdb";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "MySQLPlayerDataBridge Migrator";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getHelpMenu() {
|
||||
return """
|
||||
=== MySQLPlayerDataBridge Migration Wizard ==========
|
||||
This will migrate inventories, ender chests and XP
|
||||
from the MySQLPlayerDataBridge plugin to HuskSync.
|
||||
|
||||
To prevent excessive migration times, other non-vital
|
||||
data will not be transferred.
|
||||
|
||||
[!] Existing data in the database will be wiped. [!]
|
||||
|
||||
STEP 1] Please ensure no players are on any servers.
|
||||
|
||||
STEP 2] HuskSync will need to connect to the database
|
||||
used to hold the source MySQLPlayerDataBridge data.
|
||||
Please check these database parameters are OK:
|
||||
- host: %source_host%
|
||||
- port: %source_port%
|
||||
- username: %source_username%
|
||||
- password: %source_password%
|
||||
- database: %source_database%
|
||||
- inventory_table: %source_inventory_table%
|
||||
- ender_chest_table: %source_ender_chest_table%
|
||||
- experience_table: %source_xp_table%
|
||||
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")
|
||||
|
||||
STEP 3] HuskSync will migrate data into the database
|
||||
tables configures in the config.yml file of this
|
||||
server. Please make sure you're happy with this
|
||||
before proceeding.
|
||||
|
||||
STEP 4] To start the migration, please run:
|
||||
"husksync migrate mpdb start"
|
||||
""".replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost))
|
||||
.replaceAll(Pattern.quote("%source_port%"), Integer.toString(sourcePort))
|
||||
.replaceAll(Pattern.quote("%source_username%"), obfuscateDataString(sourceUsername))
|
||||
.replaceAll(Pattern.quote("%source_password%"), obfuscateDataString(sourcePassword))
|
||||
.replaceAll(Pattern.quote("%source_database%"), sourceDatabase)
|
||||
.replaceAll(Pattern.quote("%source_inventory_table%"), sourceInventoryTable)
|
||||
.replaceAll(Pattern.quote("%source_ender_chest_table%"), sourceEnderChestTable)
|
||||
.replaceAll(Pattern.quote("%source_xp_table%"), sourceExperienceTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents data exported from the MySQLPlayerDataBridge source database
|
||||
*
|
||||
* @param user The user whose data is being migrated
|
||||
* @param serializedInventory The serialized inventory data
|
||||
* @param serializedArmor The serialized armor data
|
||||
* @param serializedEnderChest The serialized ender chest data
|
||||
* @param expLevel The player's current XP level
|
||||
* @param expProgress The player's current XP progress
|
||||
* @param totalExp The player's total XP score
|
||||
*/
|
||||
private record MpdbData(@NotNull User user, @NotNull String serializedInventory,
|
||||
@NotNull String serializedArmor, @NotNull String serializedEnderChest,
|
||||
int expLevel, float expProgress, int totalExp) {
|
||||
/**
|
||||
* Converts exported MySQLPlayerDataBridge data into HuskSync's {@link UserData} object format
|
||||
*
|
||||
* @param converter The {@link MPDBConverter} to use for converting to {@link ItemStack}s
|
||||
* @return A {@link CompletableFuture} that will resolve to the converted {@link UserData} object
|
||||
*/
|
||||
@NotNull
|
||||
public CompletableFuture<UserData> toUserData(@NotNull MPDBConverter converter,
|
||||
@NotNull String minecraftVersion) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Combine inventory and armour
|
||||
final Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
|
||||
inventory.setContents(converter.getItemStackFromSerializedData(serializedInventory));
|
||||
final ItemStack[] armor = converter.getItemStackFromSerializedData(serializedArmor).clone();
|
||||
for (int i = 36; i < 36 + armor.length; i++) {
|
||||
inventory.setItem(i, armor[i - 36]);
|
||||
}
|
||||
|
||||
// Create user data record
|
||||
return new UserData(new StatusData(20, 20, 0, 20, 10,
|
||||
1, 0, totalExp, expLevel, expProgress, "SURVIVAL",
|
||||
false),
|
||||
new ItemData(BukkitSerializer.serializeItemStackArray(inventory.getContents()).join()),
|
||||
new ItemData(BukkitSerializer.serializeItemStackArray(converter
|
||||
.getItemStackFromSerializedData(serializedEnderChest)).join()),
|
||||
new PotionEffectData(""), new ArrayList<>(),
|
||||
new StatisticsData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()),
|
||||
new LocationData("world", UUID.randomUUID(), "NORMAL", 0, 0, 0,
|
||||
0f, 0f),
|
||||
new PersistentDataContainerData(new HashMap<>()),
|
||||
minecraftVersion);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,609 @@
|
||||
package net.william278.husksync.player;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.md_5.bungee.api.ChatMessageType;
|
||||
import net.md_5.bungee.api.chat.BaseComponent;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.data.*;
|
||||
import net.william278.husksync.editor.ItemEditorMenu;
|
||||
import net.william278.husksync.util.Version;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.advancement.Advancement;
|
||||
import org.bukkit.advancement.AdvancementProgress;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Bukkit implementation of an {@link OnlineUser}
|
||||
*/
|
||||
public class BukkitPlayer extends OnlineUser {
|
||||
|
||||
private static final PersistentDataType<?, ?>[] PRIMITIVE_PERSISTENT_DATA_TYPES = new PersistentDataType<?, ?>[]{
|
||||
PersistentDataType.BYTE,
|
||||
PersistentDataType.SHORT,
|
||||
PersistentDataType.INTEGER,
|
||||
PersistentDataType.LONG,
|
||||
PersistentDataType.FLOAT,
|
||||
PersistentDataType.DOUBLE,
|
||||
PersistentDataType.STRING,
|
||||
PersistentDataType.BYTE_ARRAY,
|
||||
PersistentDataType.INTEGER_ARRAY,
|
||||
PersistentDataType.LONG_ARRAY,
|
||||
PersistentDataType.TAG_CONTAINER_ARRAY,
|
||||
PersistentDataType.TAG_CONTAINER};
|
||||
|
||||
private final Player player;
|
||||
|
||||
private BukkitPlayer(@NotNull Player player) {
|
||||
super(player.getUniqueId(), player.getName());
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
public static BukkitPlayer adapt(@NotNull Player player) {
|
||||
return new BukkitPlayer(player);
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<StatusData> getStatus() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final double maxHealth = getMaxHealth(player);
|
||||
return new StatusData(Math.min(player.getHealth(), maxHealth),
|
||||
maxHealth,
|
||||
player.isHealthScaled() ? player.getHealthScale() : 0d,
|
||||
player.getFoodLevel(),
|
||||
player.getSaturation(),
|
||||
player.getExhaustion(),
|
||||
player.getInventory().getHeldItemSlot(),
|
||||
player.getTotalExperience(),
|
||||
player.getLevel(),
|
||||
player.getExp(),
|
||||
player.getGameMode().name(),
|
||||
player.getAllowFlight() && player.isFlying());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setStatus(@NotNull StatusData statusData,
|
||||
@NotNull List<StatusDataFlag> statusDataFlags) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
double currentMaxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH))
|
||||
.getBaseValue();
|
||||
if (statusDataFlags.contains(StatusDataFlag.SET_MAX_HEALTH)) {
|
||||
if (statusData.maxHealth != 0d) {
|
||||
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH))
|
||||
.setBaseValue(statusData.maxHealth);
|
||||
currentMaxHealth = statusData.maxHealth;
|
||||
}
|
||||
}
|
||||
if (statusDataFlags.contains(StatusDataFlag.SET_HEALTH)) {
|
||||
final double currentHealth = player.getHealth();
|
||||
if (statusData.health != currentHealth) {
|
||||
final double healthToSet = currentHealth > currentMaxHealth ? currentMaxHealth : statusData.health;
|
||||
if (healthToSet < 1) {
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> player.setHealth(healthToSet));
|
||||
} else {
|
||||
player.setHealth(healthToSet);
|
||||
}
|
||||
}
|
||||
|
||||
if (statusData.healthScale != 0d) {
|
||||
player.setHealthScale(statusData.healthScale);
|
||||
} else {
|
||||
player.setHealthScale(statusData.maxHealth);
|
||||
}
|
||||
player.setHealthScaled(statusData.healthScale != 0D);
|
||||
}
|
||||
if (statusDataFlags.contains(StatusDataFlag.SET_HUNGER)) {
|
||||
player.setFoodLevel(statusData.hunger);
|
||||
player.setSaturation(statusData.saturation);
|
||||
player.setExhaustion(statusData.saturationExhaustion);
|
||||
}
|
||||
if (statusDataFlags.contains(StatusDataFlag.SET_SELECTED_ITEM_SLOT)) {
|
||||
player.getInventory().setHeldItemSlot(statusData.selectedItemSlot);
|
||||
}
|
||||
if (statusDataFlags.contains(StatusDataFlag.SET_EXPERIENCE)) {
|
||||
player.setTotalExperience(statusData.totalExperience);
|
||||
player.setLevel(statusData.expLevel);
|
||||
player.setExp(statusData.expProgress);
|
||||
}
|
||||
if (statusDataFlags.contains(StatusDataFlag.SET_GAME_MODE)) {
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () ->
|
||||
player.setGameMode(GameMode.valueOf(statusData.gameMode)));
|
||||
}
|
||||
if (statusDataFlags.contains(StatusDataFlag.SET_FLYING)) {
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
if (statusData.isFlying) {
|
||||
player.setAllowFlight(true);
|
||||
player.setFlying(true);
|
||||
}
|
||||
player.setFlying(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ItemData> getInventory() {
|
||||
return BukkitSerializer.serializeItemStackArray(player.getInventory().getContents())
|
||||
.thenApply(ItemData::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setInventory(@NotNull ItemData itemData) {
|
||||
return BukkitSerializer.deserializeInventory(itemData.serializedItems).thenApplyAsync(contents -> {
|
||||
final CompletableFuture<Void> inventorySetFuture = new CompletableFuture<>();
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
player.getInventory().setContents(contents.getContents());
|
||||
inventorySetFuture.complete(null);
|
||||
});
|
||||
return inventorySetFuture.join();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ItemData> getEnderChest() {
|
||||
return BukkitSerializer.serializeItemStackArray(player.getEnderChest().getContents())
|
||||
.thenApply(ItemData::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setEnderChest(@NotNull ItemData enderChestData) {
|
||||
return BukkitSerializer.deserializeItemStackArray(enderChestData.serializedItems).thenApplyAsync(contents -> {
|
||||
final CompletableFuture<Void> enderChestSetFuture = new CompletableFuture<>();
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
player.getEnderChest().setContents(contents);
|
||||
enderChestSetFuture.complete(null);
|
||||
});
|
||||
return enderChestSetFuture.join();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<PotionEffectData> getPotionEffects() {
|
||||
return BukkitSerializer.serializePotionEffectArray(player.getActivePotionEffects()
|
||||
.toArray(new PotionEffect[0])).thenApply(PotionEffectData::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setPotionEffects(@NotNull PotionEffectData potionEffectData) {
|
||||
return BukkitSerializer.deserializePotionEffectArray(potionEffectData.serializedPotionEffects)
|
||||
.thenApplyAsync(effects -> {
|
||||
final CompletableFuture<Void> potionEffectsSetFuture = new CompletableFuture<>();
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
for (PotionEffect effect : player.getActivePotionEffects()) {
|
||||
player.removePotionEffect(effect.getType());
|
||||
}
|
||||
for (PotionEffect effect : effects) {
|
||||
player.addPotionEffect(effect);
|
||||
}
|
||||
potionEffectsSetFuture.complete(null);
|
||||
});
|
||||
return potionEffectsSetFuture.join();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<AdvancementData>> getAdvancements() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
|
||||
final ArrayList<AdvancementData> advancementData = new ArrayList<>();
|
||||
|
||||
// Iterate through the server advancement set and add all advancements to the list
|
||||
serverAdvancements.forEachRemaining(advancement -> {
|
||||
final AdvancementProgress advancementProgress = player.getAdvancementProgress(advancement);
|
||||
final Map<String, Date> awardedCriteria = new HashMap<>();
|
||||
|
||||
advancementProgress.getAwardedCriteria().forEach(criteriaKey -> awardedCriteria.put(criteriaKey,
|
||||
advancementProgress.getDateAwarded(criteriaKey)));
|
||||
|
||||
// Only save the advancement if criteria has been completed
|
||||
if (!awardedCriteria.isEmpty()) {
|
||||
advancementData.add(new AdvancementData(advancement.getKey().toString(), awardedCriteria));
|
||||
}
|
||||
});
|
||||
return advancementData;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setAdvancements(@NotNull List<AdvancementData> advancementData) {
|
||||
return CompletableFuture.runAsync(() -> Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
|
||||
// Temporarily disable advancement announcing if needed
|
||||
boolean announceAdvancementUpdate = false;
|
||||
if (Boolean.TRUE.equals(player.getWorld().getGameRuleValue(GameRule.ANNOUNCE_ADVANCEMENTS))) {
|
||||
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false);
|
||||
announceAdvancementUpdate = true;
|
||||
}
|
||||
final boolean finalAnnounceAdvancementUpdate = announceAdvancementUpdate;
|
||||
|
||||
// Save current experience and level
|
||||
final int experienceLevel = player.getLevel();
|
||||
final float expProgress = player.getExp();
|
||||
|
||||
// Determines whether the experience might have changed warranting an update
|
||||
final AtomicBoolean correctExperience = new AtomicBoolean(false);
|
||||
|
||||
// Run asynchronously as advancement setting is expensive
|
||||
CompletableFuture.runAsync(() -> {
|
||||
// Apply the advancements to the player
|
||||
final Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
|
||||
while (serverAdvancements.hasNext()) {
|
||||
// Iterate through all advancements
|
||||
final Advancement advancement = serverAdvancements.next();
|
||||
final AdvancementProgress playerProgress = player.getAdvancementProgress(advancement);
|
||||
|
||||
advancementData.stream().filter(record -> record.key.equals(advancement.getKey().toString())).findFirst().ifPresentOrElse(
|
||||
// Award all criteria that the player does not have that they do on the cache
|
||||
record -> {
|
||||
record.completedCriteria.keySet().stream()
|
||||
.filter(criterion -> !playerProgress.getAwardedCriteria().contains(criterion))
|
||||
.forEach(criterion -> {
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(),
|
||||
() -> player.getAdvancementProgress(advancement).awardCriteria(criterion));
|
||||
correctExperience.set(true);
|
||||
});
|
||||
|
||||
// Revoke all criteria that the player does have but should not
|
||||
new ArrayList<>(playerProgress.getAwardedCriteria()).stream().filter(criterion -> !record.completedCriteria.containsKey(criterion))
|
||||
.forEach(criterion -> Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(),
|
||||
() -> player.getAdvancementProgress(advancement).revokeCriteria(criterion)));
|
||||
|
||||
},
|
||||
// Revoke the criteria as the player shouldn't have any
|
||||
() -> new ArrayList<>(playerProgress.getAwardedCriteria()).forEach(criterion ->
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(),
|
||||
() -> player.getAdvancementProgress(advancement).revokeCriteria(criterion))));
|
||||
|
||||
// Update the player's experience in case the advancement changed that
|
||||
if (correctExperience.get()) {
|
||||
player.setLevel(experienceLevel);
|
||||
player.setExp(expProgress);
|
||||
correctExperience.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable announcing advancements (back on main thread again)
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
if (finalAnnounceAdvancementUpdate) {
|
||||
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<StatisticsData> getStatistics() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final Map<String, Integer> untypedStatisticValues = new HashMap<>();
|
||||
final Map<String, Map<String, Integer>> blockStatisticValues = new HashMap<>();
|
||||
final Map<String, Map<String, Integer>> itemStatisticValues = new HashMap<>();
|
||||
final Map<String, Map<String, Integer>> entityStatisticValues = new HashMap<>();
|
||||
|
||||
for (Statistic statistic : Statistic.values()) {
|
||||
switch (statistic.getType()) {
|
||||
case ITEM -> {
|
||||
final Map<String, Integer> itemValues = new HashMap<>();
|
||||
Arrays.stream(Material.values()).filter(Material::isItem)
|
||||
.filter(itemMaterial -> (player.getStatistic(statistic, itemMaterial)) != 0)
|
||||
.forEach(itemMaterial -> itemValues.put(itemMaterial.name(),
|
||||
player.getStatistic(statistic, itemMaterial)));
|
||||
if (!itemValues.isEmpty()) {
|
||||
itemStatisticValues.put(statistic.name(), itemValues);
|
||||
}
|
||||
}
|
||||
case BLOCK -> {
|
||||
final Map<String, Integer> blockValues = new HashMap<>();
|
||||
Arrays.stream(Material.values()).filter(Material::isBlock)
|
||||
.filter(blockMaterial -> (player.getStatistic(statistic, blockMaterial)) != 0)
|
||||
.forEach(blockMaterial -> blockValues.put(blockMaterial.name(),
|
||||
player.getStatistic(statistic, blockMaterial)));
|
||||
if (!blockValues.isEmpty()) {
|
||||
blockStatisticValues.put(statistic.name(), blockValues);
|
||||
}
|
||||
}
|
||||
case ENTITY -> {
|
||||
final Map<String, Integer> entityValues = new HashMap<>();
|
||||
Arrays.stream(EntityType.values()).filter(EntityType::isAlive)
|
||||
.filter(entityType -> (player.getStatistic(statistic, entityType)) != 0)
|
||||
.forEach(entityType -> entityValues.put(entityType.name(),
|
||||
player.getStatistic(statistic, entityType)));
|
||||
if (!entityValues.isEmpty()) {
|
||||
entityStatisticValues.put(statistic.name(), entityValues);
|
||||
}
|
||||
}
|
||||
case UNTYPED -> {
|
||||
if (player.getStatistic(statistic) != 0) {
|
||||
untypedStatisticValues.put(statistic.name(), player.getStatistic(statistic));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new StatisticsData(untypedStatisticValues, blockStatisticValues,
|
||||
itemStatisticValues, entityStatisticValues);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setStatistics(@NotNull StatisticsData statisticsData) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
// Set untyped statistics
|
||||
for (String statistic : statisticsData.untypedStatistics.keySet()) {
|
||||
player.setStatistic(Statistic.valueOf(statistic), statisticsData.untypedStatistics.get(statistic));
|
||||
}
|
||||
|
||||
// Set block statistics
|
||||
for (String statistic : statisticsData.blockStatistics.keySet()) {
|
||||
for (String blockMaterial : statisticsData.blockStatistics.get(statistic).keySet()) {
|
||||
player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(blockMaterial),
|
||||
statisticsData.blockStatistics.get(statistic).get(blockMaterial));
|
||||
}
|
||||
}
|
||||
|
||||
// Set item statistics
|
||||
for (String statistic : statisticsData.itemStatistics.keySet()) {
|
||||
for (String itemMaterial : statisticsData.itemStatistics.get(statistic).keySet()) {
|
||||
player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(itemMaterial),
|
||||
statisticsData.itemStatistics.get(statistic).get(itemMaterial));
|
||||
}
|
||||
}
|
||||
|
||||
// Set entity statistics
|
||||
for (String statistic : statisticsData.entityStatistics.keySet()) {
|
||||
for (String entityType : statisticsData.entityStatistics.get(statistic).keySet()) {
|
||||
player.setStatistic(Statistic.valueOf(statistic), EntityType.valueOf(entityType),
|
||||
statisticsData.entityStatistics.get(statistic).get(entityType));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<LocationData> getLocation() {
|
||||
return CompletableFuture.supplyAsync(() ->
|
||||
new LocationData(player.getWorld().getName(), player.getWorld().getUID(), player.getWorld().getEnvironment().name(),
|
||||
player.getLocation().getX(), player.getLocation().getY(), player.getLocation().getZ(),
|
||||
player.getLocation().getYaw(), player.getLocation().getPitch()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setLocation(@NotNull LocationData locationData) {
|
||||
final CompletableFuture<Void> teleportFuture = new CompletableFuture<>();
|
||||
AtomicReference<World> bukkitWorld = new AtomicReference<>(Bukkit.getWorld(locationData.worldName));
|
||||
if (bukkitWorld.get() == null) {
|
||||
bukkitWorld.set(Bukkit.getWorld(locationData.worldUuid));
|
||||
}
|
||||
if (bukkitWorld.get() == null) {
|
||||
Bukkit.getWorlds().stream().filter(world -> world.getEnvironment() == World.Environment
|
||||
.valueOf(locationData.worldEnvironment)).findFirst().ifPresent(bukkitWorld::set);
|
||||
}
|
||||
if (bukkitWorld.get() != null) {
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
player.teleport(new Location(bukkitWorld.get(),
|
||||
locationData.x, locationData.y, locationData.z,
|
||||
locationData.yaw, locationData.pitch), PlayerTeleportEvent.TeleportCause.PLUGIN);
|
||||
teleportFuture.complete(null);
|
||||
});
|
||||
}
|
||||
return teleportFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<PersistentDataContainerData> getPersistentDataContainer() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final PersistentDataContainer container = player.getPersistentDataContainer();
|
||||
if (container.isEmpty()) {
|
||||
return new PersistentDataContainerData(new HashMap<>());
|
||||
}
|
||||
final HashMap<String, PersistentDataTag<?>> persistentDataMap = new HashMap<>();
|
||||
for (final NamespacedKey key : container.getKeys()) {
|
||||
PersistentDataType<?, ?> type = null;
|
||||
for (PersistentDataType<?, ?> dataType : PRIMITIVE_PERSISTENT_DATA_TYPES) {
|
||||
if (container.has(key, dataType)) {
|
||||
type = dataType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (type != null) {
|
||||
// This is absolutely disgusting code and needs to be swiftly put out of its misery with a refactor
|
||||
final Class<?> primitiveType = type.getPrimitiveType();
|
||||
if (String.class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.STRING,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.STRING))));
|
||||
} else if (int.class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.INTEGER,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.INTEGER))));
|
||||
} else if (double.class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.DOUBLE,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.DOUBLE))));
|
||||
} else if (float.class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.FLOAT,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.FLOAT))));
|
||||
} else if (long.class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.LONG,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.LONG))));
|
||||
} else if (short.class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.SHORT,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.SHORT))));
|
||||
} else if (byte.class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.BYTE,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.BYTE))));
|
||||
} else if (byte[].class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.BYTE_ARRAY,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.BYTE_ARRAY))));
|
||||
} else if (int[].class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.INTEGER_ARRAY,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.INTEGER_ARRAY))));
|
||||
} else if (long[].class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.LONG_ARRAY,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.LONG_ARRAY))));
|
||||
} else if (PersistentDataContainer.class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.TAG_CONTAINER,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.TAG_CONTAINER))));
|
||||
} else if (PersistentDataContainer[].class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.TAG_CONTAINER_ARRAY,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.TAG_CONTAINER_ARRAY))));
|
||||
}
|
||||
}
|
||||
}
|
||||
return new PersistentDataContainerData(persistentDataMap);
|
||||
}).exceptionally(throwable -> {
|
||||
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.WARNING,
|
||||
"Could not read " + player.getName() + "'s persistent data map, skipping!");
|
||||
throwable.printStackTrace();
|
||||
return new PersistentDataContainerData(new HashMap<>());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setPersistentDataContainer(@NotNull PersistentDataContainerData persistentDataContainerData) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
player.getPersistentDataContainer().getKeys().forEach(namespacedKey ->
|
||||
player.getPersistentDataContainer().remove(namespacedKey));
|
||||
persistentDataContainerData.getTags().forEach(keyString -> {
|
||||
final NamespacedKey key = NamespacedKey.fromString(keyString);
|
||||
if (key != null) {
|
||||
// Set a tag with the given key and value. This is crying out for a refactor.
|
||||
persistentDataContainerData.getTagType(keyString).ifPresentOrElse(dataType -> {
|
||||
switch (dataType) {
|
||||
case BYTE -> persistentDataContainerData.getTagValue(keyString, byte.class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.BYTE, value));
|
||||
case SHORT -> persistentDataContainerData.getTagValue(keyString, short.class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.SHORT, value));
|
||||
case INTEGER -> persistentDataContainerData.getTagValue(keyString, int.class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.INTEGER, value));
|
||||
case LONG -> persistentDataContainerData.getTagValue(keyString, long.class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.LONG, value));
|
||||
case FLOAT -> persistentDataContainerData.getTagValue(keyString, float.class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.FLOAT, value));
|
||||
case DOUBLE -> persistentDataContainerData.getTagValue(keyString, double.class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.DOUBLE, value));
|
||||
case STRING -> persistentDataContainerData.getTagValue(keyString, String.class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.STRING, value));
|
||||
case BYTE_ARRAY ->
|
||||
persistentDataContainerData.getTagValue(keyString, byte[].class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.BYTE_ARRAY, value));
|
||||
case INTEGER_ARRAY ->
|
||||
persistentDataContainerData.getTagValue(keyString, int[].class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.INTEGER_ARRAY, value));
|
||||
case LONG_ARRAY ->
|
||||
persistentDataContainerData.getTagValue(keyString, long[].class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.LONG_ARRAY, value));
|
||||
case TAG_CONTAINER ->
|
||||
persistentDataContainerData.getTagValue(keyString, PersistentDataContainer.class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.TAG_CONTAINER, value));
|
||||
case TAG_CONTAINER_ARRAY ->
|
||||
persistentDataContainerData.getTagValue(keyString, PersistentDataContainer[].class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.TAG_CONTAINER_ARRAY, value));
|
||||
}
|
||||
}, () -> BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.WARNING,
|
||||
"Could not set " + player.getName() + "'s persistent data key " + keyString +
|
||||
" as it has an invalid type. Skipping!"));
|
||||
}
|
||||
});
|
||||
}).exceptionally(throwable -> {
|
||||
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.WARNING,
|
||||
"Could not write " + player.getName() + "'s persistent data map, skipping!");
|
||||
throwable.printStackTrace();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOffline() {
|
||||
try {
|
||||
return player == null;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Version getMinecraftVersion() {
|
||||
return Version.minecraftVersion(Bukkit.getBukkitVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(@NotNull String node) {
|
||||
return player.hasPermission(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showMenu(@NotNull ItemEditorMenu menu) {
|
||||
BukkitSerializer.deserializeItemStackArray(menu.itemData.serializedItems).thenAccept(inventoryContents -> {
|
||||
final Inventory inventory = Bukkit.createInventory(player, menu.itemEditorMenuType.slotCount,
|
||||
BaseComponent.toLegacyText(menu.menuTitle.toComponent()));
|
||||
inventory.setContents(inventoryContents);
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> player.openInventory(inventory));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDead() {
|
||||
return player.getHealth() < 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendActionBar(@NotNull MineDown mineDown) {
|
||||
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, mineDown.replace().toComponent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(@NotNull MineDown mineDown) {
|
||||
player.spigot().sendMessage(mineDown.replace().toComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Player}'s maximum health, minus any health boost effects
|
||||
*
|
||||
* @param player The {@link Player} to get the maximum health of
|
||||
* @return The {@link Player}'s max health
|
||||
*/
|
||||
private static double getMaxHealth(@NotNull Player player) {
|
||||
double maxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).getBaseValue();
|
||||
|
||||
// If the player has additional health bonuses from synchronised potion effects, subtract these from this number as they are synchronised separately
|
||||
if (player.hasPotionEffect(PotionEffectType.HEALTH_BOOST) && maxHealth > 20D) {
|
||||
PotionEffect healthBoostEffect = player.getPotionEffect(PotionEffectType.HEALTH_BOOST);
|
||||
assert healthBoostEffect != null;
|
||||
double healthBoostBonus = 4 * (healthBoostEffect.getAmplifier() + 1);
|
||||
maxHealth -= healthBoostBonus;
|
||||
}
|
||||
return maxHealth;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitLogger extends Logger {
|
||||
|
||||
private final java.util.logging.Logger logger;
|
||||
|
||||
public BukkitLogger(@NotNull java.util.logging.Logger logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(@NotNull Level level, @NotNull String message, @NotNull Exception e) {
|
||||
logger.log(level, message, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(@NotNull Level level, @NotNull String message) {
|
||||
logger.log(level, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(@NotNull Level level, @NotNull MineDown mineDown) {
|
||||
logger.log(level, TextComponent.toLegacyText(mineDown.toComponent()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(@NotNull String message) {
|
||||
logger.info(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void severe(@NotNull String message) {
|
||||
logger.severe(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void config(@NotNull String message) {
|
||||
logger.config(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
public class BukkitResourceReader implements ResourceReader {
|
||||
|
||||
private final BukkitHuskSync plugin;
|
||||
|
||||
public BukkitResourceReader(BukkitHuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull InputStream getResource(String fileName) {
|
||||
return Objects.requireNonNull(plugin.getResource(fileName));
|
||||
}
|
||||
|
||||
}
|
||||
3
bukkit/src/main/resources/commodore/enderchest.commodore
Normal file
3
bukkit/src/main/resources/commodore/enderchest.commodore
Normal file
@@ -0,0 +1,3 @@
|
||||
inventory {
|
||||
name brigadier:string single_word;
|
||||
}
|
||||
5
bukkit/src/main/resources/commodore/husksync.commodore
Normal file
5
bukkit/src/main/resources/commodore/husksync.commodore
Normal file
@@ -0,0 +1,5 @@
|
||||
husksync {
|
||||
update;
|
||||
about;
|
||||
reload;
|
||||
}
|
||||
3
bukkit/src/main/resources/commodore/inventory.commodore
Normal file
3
bukkit/src/main/resources/commodore/inventory.commodore
Normal file
@@ -0,0 +1,3 @@
|
||||
enderchest {
|
||||
name brigadier:string single_word;
|
||||
}
|
||||
25
bukkit/src/main/resources/commodore/userdata.commodore
Normal file
25
bukkit/src/main/resources/commodore/userdata.commodore
Normal file
@@ -0,0 +1,25 @@
|
||||
userdata {
|
||||
view {
|
||||
name brigadier:string single_word {
|
||||
version brigadier:string single_word;
|
||||
}
|
||||
}
|
||||
list {
|
||||
name brigadier:string single_word;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
redis_settings:
|
||||
host: 'localhost'
|
||||
port: 6379
|
||||
password: ''
|
||||
use_ssl: false
|
||||
synchronisation_settings:
|
||||
inventories: true
|
||||
ender_chests: true
|
||||
health: true
|
||||
hunger: true
|
||||
experience: true
|
||||
potion_effects: true
|
||||
statistics: true
|
||||
game_mode: true
|
||||
advancements: true
|
||||
location: false
|
||||
flight: false
|
||||
cluster_id: 'main'
|
||||
check_for_updates: true
|
||||
synchronization_timeout_retry_delay: 15
|
||||
save_on_world_save: true
|
||||
native_advancement_synchronization: false
|
||||
@@ -1,8 +1,28 @@
|
||||
name: HuskSync
|
||||
version: ${version}
|
||||
main: net.william278.husksync.HuskSyncBukkit
|
||||
main: net.william278.husksync.BukkitHuskSync
|
||||
api-version: 1.16
|
||||
author: William278
|
||||
description: 'A modern, cross-server player data synchronization system'
|
||||
website: 'https://william278.net'
|
||||
softdepend: [MysqlPlayerDataBridge]
|
||||
softdepend:
|
||||
- MysqlPlayerDataBridge
|
||||
- Plan
|
||||
libraries:
|
||||
- 'redis.clients:jedis:${jedis_version}'
|
||||
- 'mysql:mysql-connector-java:${mysql_driver_version}'
|
||||
- 'org.xerial.snappy:snappy-java:${snappy_version}'
|
||||
|
||||
commands:
|
||||
husksync:
|
||||
usage: '/husksync <update/info/reload/migrate>'
|
||||
description: 'Manage the HuskSync plugin'
|
||||
userdata:
|
||||
usage: '/userdata <view/list/delete/restore/pin> <username> [version_uuid]'
|
||||
description: 'View, manage & restore player userdata'
|
||||
inventory:
|
||||
usage: '/inventory <username> [version_uuid]'
|
||||
description: 'View & edit a player''s inventory'
|
||||
enderchest:
|
||||
usage: '/enderchest <username> [version_uuid]'
|
||||
description: 'View & edit a player''s Ender Chest'
|
||||
@@ -1,23 +0,0 @@
|
||||
dependencies {
|
||||
implementation project(path: ':common')
|
||||
|
||||
implementation 'com.zaxxer:HikariCP:5.0.1'
|
||||
implementation 'org.bstats:bstats-bungeecord:3.0.0'
|
||||
implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
|
||||
implementation 'net.byteflux:libby-bungee:1.1.5'
|
||||
|
||||
compileOnly 'net.md-5:bungeecord-api:1.16-R0.5-SNAPSHOT'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'net.byteflux', 'net.william278.husksync.libraries'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.apache', 'net.william278.husksync.libraries'
|
||||
|
||||
dependencies {
|
||||
//noinspection GroovyAssignabilityCheck
|
||||
exclude dependency(':slf4j-api')
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import net.byteflux.libby.BungeeLibraryManager;
|
||||
import net.byteflux.libby.Library;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.william278.husksync.bungeecord.command.BungeeCommand;
|
||||
import net.william278.husksync.bungeecord.config.ConfigLoader;
|
||||
import net.william278.husksync.bungeecord.config.ConfigManager;
|
||||
import net.william278.husksync.bungeecord.listener.BungeeEventListener;
|
||||
import net.william278.husksync.bungeecord.listener.BungeeRedisListener;
|
||||
import net.william278.husksync.bungeecord.util.BungeeLogger;
|
||||
import net.william278.husksync.bungeecord.util.BungeeUpdateChecker;
|
||||
import net.william278.husksync.migrator.MPDBMigrator;
|
||||
import net.william278.husksync.proxy.data.DataManager;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.william278.husksync.util.Logger;
|
||||
import org.bstats.bungeecord.Metrics;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class HuskSyncBungeeCord extends Plugin {
|
||||
|
||||
// BungeeCord bStats ID (different to Bukkit)
|
||||
private static final int METRICS_ID = 13141;
|
||||
|
||||
private static HuskSyncBungeeCord instance;
|
||||
|
||||
public static HuskSyncBungeeCord getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Whether the plugin is ready to accept redis messages
|
||||
public static boolean readyForRedis = false;
|
||||
|
||||
// Whether the plugin is in the process of disabling and should skip responding to handshake confirmations
|
||||
public static boolean isDisabling = false;
|
||||
|
||||
/**
|
||||
* Set of all the {@link Server}s that have completed the synchronisation handshake with HuskSync on the proxy
|
||||
*/
|
||||
public static HashSet<Server> synchronisedServers;
|
||||
|
||||
public static DataManager dataManager;
|
||||
|
||||
public static MPDBMigrator mpdbMigrator;
|
||||
|
||||
public static BungeeRedisListener redisListener;
|
||||
|
||||
private Logger logger;
|
||||
|
||||
public Logger getBungeeLogger() {
|
||||
return logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
instance = this;
|
||||
logger = new BungeeLogger(getLogger());
|
||||
fetchDependencies();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// Plugin startup logic
|
||||
synchronisedServers = new HashSet<>();
|
||||
|
||||
// Load config
|
||||
ConfigManager.loadConfig();
|
||||
|
||||
// Load settings from config
|
||||
ConfigLoader.loadSettings(Objects.requireNonNull(ConfigManager.getConfig()));
|
||||
|
||||
// Load messages
|
||||
ConfigManager.loadMessages();
|
||||
|
||||
// Load locales from messages
|
||||
ConfigLoader.loadMessageStrings(Objects.requireNonNull(ConfigManager.getMessages()));
|
||||
|
||||
// Do update checker
|
||||
if (Settings.automaticUpdateChecks) {
|
||||
new BungeeUpdateChecker(getDescription().getVersion()).logToConsole();
|
||||
}
|
||||
|
||||
// Setup data manager
|
||||
dataManager = new DataManager(getBungeeLogger(), getDataFolder());
|
||||
|
||||
// Ensure the data manager initialized correctly
|
||||
if (dataManager.hasFailedInitialization) {
|
||||
getBungeeLogger().severe("Failed to initialize the HuskSync database(s).\n" +
|
||||
"HuskSync will now abort loading itself (" + getProxy().getName() + ") v" + getDescription().getVersion());
|
||||
}
|
||||
|
||||
// Setup player data cache
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
dataManager.playerDataCache.put(cluster, new DataManager.PlayerDataCache());
|
||||
}
|
||||
|
||||
// Initialize the redis listener
|
||||
redisListener = new BungeeRedisListener();
|
||||
|
||||
// Register listener
|
||||
getProxy().getPluginManager().registerListener(this, new BungeeEventListener());
|
||||
|
||||
// Register command
|
||||
getProxy().getPluginManager().registerCommand(this, new BungeeCommand());
|
||||
|
||||
// Prepare the migrator for use if needed
|
||||
mpdbMigrator = new MPDBMigrator(getBungeeLogger());
|
||||
|
||||
// Initialize bStats metrics
|
||||
try {
|
||||
new Metrics(this, METRICS_ID);
|
||||
} catch (Exception e) {
|
||||
getBungeeLogger().info("Skipped metrics initialization");
|
||||
}
|
||||
|
||||
// Log to console
|
||||
getBungeeLogger().info("Enabled HuskSync (" + getProxy().getName() + ") v" + getDescription().getVersion());
|
||||
|
||||
// Mark as ready for redis message processing
|
||||
readyForRedis = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
// Plugin shutdown logic
|
||||
isDisabling = true;
|
||||
|
||||
// Send terminating handshake message
|
||||
for (Server server : synchronisedServers) {
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.TERMINATE_HANDSHAKE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, server.clusterId()),
|
||||
server.serverUUID().toString(),
|
||||
ProxyServer.getInstance().getName()).send();
|
||||
} catch (IOException e) {
|
||||
getBungeeLogger().log(Level.SEVERE, "Failed to serialize Redis message for handshake termination", e);
|
||||
}
|
||||
}
|
||||
|
||||
dataManager.closeDatabases();
|
||||
|
||||
// Log to console
|
||||
getBungeeLogger().info("Disabled HuskSync (" + getProxy().getName() + ") v" + getDescription().getVersion());
|
||||
}
|
||||
|
||||
// Load dependencies
|
||||
private void fetchDependencies() {
|
||||
BungeeLibraryManager manager = new BungeeLibraryManager(getInstance());
|
||||
|
||||
Library mySqlLib = Library.builder()
|
||||
.groupId("mysql")
|
||||
.artifactId("mysql-connector-java")
|
||||
.version("8.0.29")
|
||||
.build();
|
||||
|
||||
Library sqLiteLib = Library.builder()
|
||||
.groupId("org.xerial")
|
||||
.artifactId("sqlite-jdbc")
|
||||
.version("3.36.0.3")
|
||||
.build();
|
||||
|
||||
manager.addMavenCentral();
|
||||
manager.loadLibrary(mySqlLib);
|
||||
manager.loadLibrary(sqLiteLib);
|
||||
}
|
||||
}
|
||||
@@ -1,424 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.command;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Server;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bungeecord.config.ConfigLoader;
|
||||
import net.william278.husksync.bungeecord.config.ConfigManager;
|
||||
import net.william278.husksync.bungeecord.util.BungeeUpdateChecker;
|
||||
import net.william278.husksync.migrator.MPDBMigrator;
|
||||
import net.william278.husksync.proxy.command.HuskSyncCommand;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.william278.husksync.util.MessageManager;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
import net.md_5.bungee.api.plugin.TabExecutor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BungeeCommand extends Command implements TabExecutor, HuskSyncCommand {
|
||||
|
||||
private final static HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
public BungeeCommand() {
|
||||
super("husksync", null, "hs");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
if (sender instanceof ProxiedPlayer player) {
|
||||
if (HuskSyncBungeeCord.synchronisedServers.size() == 0) {
|
||||
player.sendMessage(new MineDown(MessageManager.getMessage("error_no_servers_proxied")).toComponent());
|
||||
return;
|
||||
}
|
||||
if (args.length >= 1) {
|
||||
switch (args[0].toLowerCase(Locale.ROOT)) {
|
||||
case "about", "info" -> sendAboutInformation(player);
|
||||
case "update" -> {
|
||||
if (!player.hasPermission("husksync.command.inventory")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(new MineDown("[Checking for HuskSync updates...](gray)").toComponent());
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
// Check Bukkit servers needing updates
|
||||
int updatesNeeded = 0;
|
||||
String bukkitBrand = "Spigot";
|
||||
String bukkitVersion = "1.0";
|
||||
for (Server server : HuskSyncBungeeCord.synchronisedServers) {
|
||||
BungeeUpdateChecker updateChecker = new BungeeUpdateChecker(server.huskSyncVersion());
|
||||
if (!updateChecker.isUpToDate()) {
|
||||
updatesNeeded++;
|
||||
bukkitBrand = server.serverBrand();
|
||||
bukkitVersion = server.huskSyncVersion();
|
||||
}
|
||||
}
|
||||
|
||||
// Check Bungee servers needing updates and send message
|
||||
BungeeUpdateChecker proxyUpdateChecker = new BungeeUpdateChecker(plugin.getDescription().getVersion());
|
||||
if (proxyUpdateChecker.isUpToDate() && updatesNeeded == 0) {
|
||||
sender.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date, running Version " + proxyUpdateChecker.getLatestVersion() + "](#00fb9a)").toComponent());
|
||||
} else {
|
||||
sender.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| Your server(s) are not up-to-date:](#00fb9a)").toComponent());
|
||||
if (!proxyUpdateChecker.isUpToDate()) {
|
||||
sender.sendMessage(new MineDown("[•](white) [HuskSync on the " + ProxyServer.getInstance().getName() + " proxy is outdated (Latest: " + proxyUpdateChecker.getLatestVersion() + ", Running: " + proxyUpdateChecker.getCurrentVersion() + ")](#00fb9a)").toComponent());
|
||||
}
|
||||
if (updatesNeeded > 0) {
|
||||
sender.sendMessage(new MineDown("[•](white) [HuskSync on " + updatesNeeded + " connected " + bukkitBrand + " server(s) are outdated (Latest: " + proxyUpdateChecker.getLatestVersion() + ", Running: " + bukkitVersion + ")](#00fb9a)").toComponent());
|
||||
}
|
||||
sender.sendMessage(new MineDown("[•](white) [Download links:](#00fb9a) [[⏩ Spigot]](gray open_url=https://www.spigotmc.org/resources/husktowns.92672/updates) [•](#262626) [[⏩ Polymart]](gray open_url=https://polymart.org/resource/husktowns.1056/updates)").toComponent());
|
||||
}
|
||||
});
|
||||
}
|
||||
case "invsee", "openinv", "inventory" -> {
|
||||
if (!player.hasPermission("husksync.command.inventory")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
String clusterId;
|
||||
if (Settings.clusters.size() > 1) {
|
||||
if (args.length == 3) {
|
||||
clusterId = args[2];
|
||||
} else {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
clusterId = "main";
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
clusterId = cluster.clusterId();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (args.length == 2 || args.length == 3) {
|
||||
String playerName = args[1];
|
||||
openInventory(player, playerName, clusterId);
|
||||
} else {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax").replaceAll("%1%",
|
||||
"/husksync invsee <player>")).toComponent());
|
||||
}
|
||||
}
|
||||
case "echest", "enderchest" -> {
|
||||
if (!player.hasPermission("husksync.command.ender_chest")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
String clusterId;
|
||||
if (Settings.clusters.size() > 1) {
|
||||
if (args.length == 3) {
|
||||
clusterId = args[2];
|
||||
} else {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
clusterId = "main";
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
clusterId = cluster.clusterId();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (args.length == 2 || args.length == 3) {
|
||||
String playerName = args[1];
|
||||
openEnderChest(player, playerName, clusterId);
|
||||
} else {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax")
|
||||
.replaceAll("%1%", "/husksync echest <player>")).toComponent());
|
||||
}
|
||||
}
|
||||
case "migrate" -> {
|
||||
if (!player.hasPermission("husksync.command.admin")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_console_command_only")
|
||||
.replaceAll("%1%", ProxyServer.getInstance().getName())).toComponent());
|
||||
}
|
||||
case "status" -> {
|
||||
if (!player.hasPermission("husksync.command.admin")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
int playerDataSize = 0;
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
playerDataSize += HuskSyncBungeeCord.dataManager.playerDataCache.get(cluster).playerData.size();
|
||||
}
|
||||
sender.sendMessage(new MineDown(MessageManager.PLUGIN_STATUS.toString()
|
||||
.replaceAll("%1%", String.valueOf(HuskSyncBungeeCord.synchronisedServers.size()))
|
||||
.replaceAll("%2%", String.valueOf(playerDataSize))).toComponent());
|
||||
}
|
||||
case "reload" -> {
|
||||
if (!player.hasPermission("husksync.command.admin")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
ConfigManager.loadConfig();
|
||||
ConfigLoader.loadSettings(Objects.requireNonNull(ConfigManager.getConfig()));
|
||||
|
||||
ConfigManager.loadMessages();
|
||||
ConfigLoader.loadMessageStrings(Objects.requireNonNull(ConfigManager.getMessages()));
|
||||
|
||||
// Send reload request to all bukkit servers
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.RELOAD_CONFIG,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null),
|
||||
"reload")
|
||||
.send();
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.WARNING, "Failed to serialize reload notification message data");
|
||||
}
|
||||
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("reload_complete")).toComponent());
|
||||
}
|
||||
default -> sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax").replaceAll("%1%",
|
||||
"/husksync <about/status/invsee/echest>")).toComponent());
|
||||
}
|
||||
} else {
|
||||
sendAboutInformation(player);
|
||||
}
|
||||
} else {
|
||||
// Database migration wizard
|
||||
if (args.length >= 1) {
|
||||
if (args[0].equalsIgnoreCase("migrate")) {
|
||||
MPDBMigrator migrator = HuskSyncBungeeCord.mpdbMigrator;
|
||||
if (args.length == 1) {
|
||||
sender.sendMessage(new MineDown(
|
||||
"""
|
||||
=== MySQLPlayerDataBridge Migration Wizard ==========
|
||||
This will migrate data from the MySQLPlayerDataBridge
|
||||
plugin to HuskSync.
|
||||
|
||||
Data that will be migrated:
|
||||
- Inventories
|
||||
- Ender Chests
|
||||
- Experience points
|
||||
|
||||
Other non-vital data, such as current health, hunger
|
||||
& potion effects will not be migrated to ensure that
|
||||
migration does not take an excessive amount of time.
|
||||
|
||||
To do this, you need to have MySqlPlayerDataBridge
|
||||
and HuskSync installed on one Spigot server as well
|
||||
as HuskSync installed on the proxy (which you have)
|
||||
|
||||
>To proceed, type: husksync migrate setup""").toComponent());
|
||||
} else {
|
||||
switch (args[1].toLowerCase()) {
|
||||
case "setup" -> sender.sendMessage(new MineDown(
|
||||
"""
|
||||
=== MySQLPlayerDataBridge Migration Wizard ==========
|
||||
The following database settings will be used.
|
||||
Please make sure they match the correct settings to
|
||||
access your MySQLPlayerDataBridge Data
|
||||
|
||||
sourceHost: %1%
|
||||
sourcePort: %2%
|
||||
sourceDatabase: %3%
|
||||
sourceUsername: %4%
|
||||
sourcePassword: %5%
|
||||
|
||||
sourceInventoryTableName: %6%
|
||||
sourceEnderChestTableName: %7%
|
||||
sourceExperienceTableName: %8%
|
||||
|
||||
targetCluster: %9%
|
||||
|
||||
To change a setting, type:
|
||||
husksync migrate setting <settingName> <value>
|
||||
|
||||
Please ensure no players are logged in to the network
|
||||
and that at least one Spigot server is online with
|
||||
both HuskSync AND MySqlPlayerDataBridge installed AND
|
||||
that the server has been configured with the correct
|
||||
Redis credentials.
|
||||
|
||||
Warning: Data will be saved to your configured data
|
||||
source, which is currently a %10% database.
|
||||
Please make sure you are happy with this, or stop
|
||||
the proxy server and edit this in config.yml
|
||||
|
||||
Warning: Migration will overwrite any current data
|
||||
saved by HuskSync. It will not, however, delete any
|
||||
data from the source MySQLPlayerDataBridge database.
|
||||
|
||||
>When done, type: husksync migrate start"""
|
||||
.replaceAll("%1%", migrator.migrationSettings.sourceHost)
|
||||
.replaceAll("%2%", String.valueOf(migrator.migrationSettings.sourcePort))
|
||||
.replaceAll("%3%", migrator.migrationSettings.sourceDatabase)
|
||||
.replaceAll("%4%", migrator.migrationSettings.sourceUsername)
|
||||
.replaceAll("%5%", migrator.migrationSettings.sourcePassword)
|
||||
.replaceAll("%6%", migrator.migrationSettings.inventoryDataTable)
|
||||
.replaceAll("%7%", migrator.migrationSettings.enderChestDataTable)
|
||||
.replaceAll("%8%", migrator.migrationSettings.expDataTable)
|
||||
.replaceAll("%9%", migrator.migrationSettings.targetCluster)
|
||||
.replaceAll("%10%", Settings.dataStorageType.toString())
|
||||
).toComponent());
|
||||
case "setting" -> {
|
||||
if (args.length == 4) {
|
||||
String value = args[3];
|
||||
switch (args[2]) {
|
||||
case "sourceHost", "host" -> migrator.migrationSettings.sourceHost = value;
|
||||
case "sourcePort", "port" -> {
|
||||
try {
|
||||
migrator.migrationSettings.sourcePort = Integer.parseInt(value);
|
||||
} catch (NumberFormatException e) {
|
||||
sender.sendMessage(new MineDown("Error: Invalid value; port must be a number").toComponent());
|
||||
return;
|
||||
}
|
||||
}
|
||||
case "sourceDatabase", "database" -> migrator.migrationSettings.sourceDatabase = value;
|
||||
case "sourceUsername", "username" -> migrator.migrationSettings.sourceUsername = value;
|
||||
case "sourcePassword", "password" -> migrator.migrationSettings.sourcePassword = value;
|
||||
case "sourceInventoryTableName", "inventoryTableName", "inventoryTable" -> migrator.migrationSettings.inventoryDataTable = value;
|
||||
case "sourceEnderChestTableName", "enderChestTableName", "enderChestTable" -> migrator.migrationSettings.enderChestDataTable = value;
|
||||
case "sourceExperienceTableName", "experienceTableName", "experienceTable" -> migrator.migrationSettings.expDataTable = value;
|
||||
case "targetCluster", "cluster" -> migrator.migrationSettings.targetCluster = value;
|
||||
default -> {
|
||||
sender.sendMessage(new MineDown("Error: Invalid setting; please use \"husksync migrate setup\" to view a list").toComponent());
|
||||
return;
|
||||
}
|
||||
}
|
||||
sender.sendMessage(new MineDown("Successfully updated setting: \"" + args[2] + "\" --> \"" + value + "\"").toComponent());
|
||||
} else {
|
||||
sender.sendMessage(new MineDown("Error: Invalid usage. Syntax: husksync migrate setting <settingName> <value>").toComponent());
|
||||
}
|
||||
}
|
||||
case "start" -> {
|
||||
sender.sendMessage(new MineDown("Starting MySQLPlayerDataBridge migration!...").toComponent());
|
||||
|
||||
// If the migrator is ready, execute the migration asynchronously
|
||||
if (HuskSyncBungeeCord.mpdbMigrator.readyToMigrate(ProxyServer.getInstance().getOnlineCount(),
|
||||
HuskSyncBungeeCord.synchronisedServers)) {
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () ->
|
||||
HuskSyncBungeeCord.mpdbMigrator.executeMigrationOperations(HuskSyncBungeeCord.dataManager,
|
||||
HuskSyncBungeeCord.synchronisedServers, HuskSyncBungeeCord.redisListener));
|
||||
}
|
||||
}
|
||||
default -> sender.sendMessage(new MineDown("Error: Invalid argument for migration. Use \"husksync migrate\" to start the process").toComponent());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
sender.sendMessage(new MineDown("Error: Invalid syntax. Usage: husksync migrate <args>").toComponent());
|
||||
}
|
||||
}
|
||||
|
||||
// View the inventory of a player specified by their name
|
||||
private void openInventory(ProxiedPlayer viewer, String targetPlayerName, String clusterId) {
|
||||
if (viewer.getName().equalsIgnoreCase(targetPlayerName)) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_inventory")).toComponent());
|
||||
return;
|
||||
}
|
||||
if (ProxyServer.getInstance().getPlayer(targetPlayerName) != null) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_inventory_online")).toComponent());
|
||||
return;
|
||||
}
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (!cluster.clusterId().equals(clusterId)) continue;
|
||||
PlayerData playerData = HuskSyncBungeeCord.dataManager.getPlayerDataByName(targetPlayerName, cluster.clusterId());
|
||||
if (playerData == null) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.OPEN_INVENTORY,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId(), null),
|
||||
targetPlayerName, RedisMessage.serialize(playerData))
|
||||
.send();
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_inventory_of").replaceAll("%1%",
|
||||
targetPlayerName)).toComponent());
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.WARNING, "Failed to serialize inventory-see player data", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
|
||||
});
|
||||
}
|
||||
|
||||
// View the ender chest of a player specified by their name
|
||||
public void openEnderChest(ProxiedPlayer viewer, String targetPlayerName, String clusterId) {
|
||||
if (viewer.getName().equalsIgnoreCase(targetPlayerName)) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_ender_chest")).toComponent());
|
||||
return;
|
||||
}
|
||||
if (ProxyServer.getInstance().getPlayer(targetPlayerName) != null) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_ender_chest_online")).toComponent());
|
||||
return;
|
||||
}
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (!cluster.clusterId().equals(clusterId)) continue;
|
||||
PlayerData playerData = HuskSyncBungeeCord.dataManager.getPlayerDataByName(targetPlayerName, cluster.clusterId());
|
||||
if (playerData == null) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.OPEN_ENDER_CHEST,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId(), null),
|
||||
targetPlayerName, RedisMessage.serialize(playerData))
|
||||
.send();
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_ender_chest_of").replaceAll("%1%",
|
||||
targetPlayerName)).toComponent());
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.WARNING, "Failed to serialize inventory-see player data", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send information about the plugin
|
||||
*
|
||||
* @param player The player to send it to
|
||||
*/
|
||||
private void sendAboutInformation(ProxiedPlayer player) {
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.SEND_PLUGIN_INFORMATION,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, player.getUniqueId(), null),
|
||||
plugin.getProxy().getName(), plugin.getDescription().getVersion()).send();
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.WARNING, "Failed to serialize plugin information to send", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Tab completion
|
||||
@Override
|
||||
public Iterable<String> onTabComplete(CommandSender sender, String[] args) {
|
||||
if (sender instanceof ProxiedPlayer player) {
|
||||
if (args.length == 1) {
|
||||
final ArrayList<String> subCommands = new ArrayList<>();
|
||||
for (SubCommand subCommand : SUB_COMMANDS) {
|
||||
if (subCommand.permission() != null) {
|
||||
if (!player.hasPermission(subCommand.permission())) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
subCommands.add(subCommand.command());
|
||||
}
|
||||
// Automatically filter the sub commands' order in tab completion by what the player has typed
|
||||
return subCommands.stream().filter(val -> val.startsWith(args[0]))
|
||||
.sorted().collect(Collectors.toList());
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.config;
|
||||
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.util.MessageManager;
|
||||
import net.md_5.bungee.config.Configuration;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class ConfigLoader {
|
||||
|
||||
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
private static Configuration copyDefaults(Configuration config) {
|
||||
// Get the config version and update if needed
|
||||
String configVersion = config.getString("config_file_version", "1.0");
|
||||
if (configVersion.contains("-dev")) {
|
||||
configVersion = configVersion.replaceAll("-dev", "");
|
||||
}
|
||||
if (!configVersion.equals(plugin.getDescription().getVersion())) {
|
||||
if (configVersion.equalsIgnoreCase("1.0")) {
|
||||
config.set("check_for_updates", true);
|
||||
}
|
||||
if (configVersion.equalsIgnoreCase("1.0") || configVersion.equalsIgnoreCase("1.0.1") || configVersion.equalsIgnoreCase("1.0.2") || configVersion.equalsIgnoreCase("1.0.3")) {
|
||||
config.set("clusters.main.player_table", "husksync_players");
|
||||
config.set("clusters.main.data_table", "husksync_data");
|
||||
}
|
||||
config.set("config_file_version", plugin.getDescription().getVersion());
|
||||
}
|
||||
// Save the config back
|
||||
ConfigManager.saveConfig(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
public static void loadSettings(Configuration loadedConfig) throws IllegalArgumentException {
|
||||
Configuration config = copyDefaults(loadedConfig);
|
||||
|
||||
Settings.language = config.getString("language", "en-gb");
|
||||
|
||||
Settings.serverType = Settings.ServerType.PROXY;
|
||||
Settings.automaticUpdateChecks = config.getBoolean("check_for_updates", true);
|
||||
Settings.redisHost = config.getString("redis_settings.host", "localhost");
|
||||
Settings.redisPort = config.getInt("redis_settings.port", 6379);
|
||||
Settings.redisPassword = config.getString("redis_settings.password", "");
|
||||
Settings.redisSSL = config.getBoolean("redis_settings.use_ssl", false);
|
||||
|
||||
Settings.dataStorageType = Settings.DataStorageType.valueOf(config.getString("data_storage_settings.database_type", "sqlite").toUpperCase());
|
||||
if (Settings.dataStorageType == Settings.DataStorageType.MYSQL) {
|
||||
Settings.mySQLHost = config.getString("data_storage_settings.mysql_settings.host", "localhost");
|
||||
Settings.mySQLPort = config.getInt("data_storage_settings.mysql_settings.port", 3306);
|
||||
Settings.mySQLDatabase = config.getString("data_storage_settings.mysql_settings.database", "HuskSync");
|
||||
Settings.mySQLUsername = config.getString("data_storage_settings.mysql_settings.username", "root");
|
||||
Settings.mySQLPassword = config.getString("data_storage_settings.mysql_settings.password", "pa55w0rd");
|
||||
Settings.mySQLParams = config.getString("data_storage_settings.mysql_settings.params", "?autoReconnect=true&useSSL=false");
|
||||
}
|
||||
|
||||
Settings.hikariMaximumPoolSize = config.getInt("data_storage_settings.hikari_pool_settings.maximum_pool_size", 10);
|
||||
Settings.hikariMinimumIdle = config.getInt("data_storage_settings.hikari_pool_settings.minimum_idle", 10);
|
||||
Settings.hikariMaximumLifetime = config.getLong("data_storage_settings.hikari_pool_settings.maximum_lifetime", 1800000);
|
||||
Settings.hikariKeepAliveTime = config.getLong("data_storage_settings.hikari_pool_settings.keepalive_time", 0);
|
||||
Settings.hikariConnectionTimeOut = config.getLong("data_storage_settings.hikari_pool_settings.connection_timeout", 5000);
|
||||
|
||||
Settings.bounceBackSynchronisation = config.getBoolean("bounce_back_synchronization", true);
|
||||
|
||||
// Read cluster data
|
||||
Configuration section = config.getSection("clusters");
|
||||
final String settingDatabaseName = Settings.mySQLDatabase != null ? Settings.mySQLDatabase : "HuskSync";
|
||||
for (String clusterId : section.getKeys()) {
|
||||
final String playerTableName = config.getString("clusters." + clusterId + ".player_table", "husksync_players");
|
||||
final String dataTableName = config.getString("clusters." + clusterId + ".data_table", "husksync_data");
|
||||
final String databaseName = config.getString("clusters." + clusterId + ".database", settingDatabaseName);
|
||||
Settings.clusters.add(new Settings.SynchronisationCluster(clusterId, databaseName, playerTableName, dataTableName));
|
||||
}
|
||||
}
|
||||
|
||||
public static void loadMessageStrings(Configuration config) {
|
||||
final HashMap<String,String> messages = new HashMap<>();
|
||||
for (String messageId : config.getKeys()) {
|
||||
messages.put(messageId, config.getString(messageId));
|
||||
}
|
||||
MessageManager.setMessages(messages);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.config;
|
||||
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.md_5.bungee.config.Configuration;
|
||||
import net.md_5.bungee.config.ConfigurationProvider;
|
||||
import net.md_5.bungee.config.YamlConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class ConfigManager {
|
||||
|
||||
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
public static void loadConfig() {
|
||||
try {
|
||||
if (!plugin.getDataFolder().exists()) {
|
||||
if (plugin.getDataFolder().mkdir()) {
|
||||
plugin.getBungeeLogger().info("Created HuskSync data folder");
|
||||
}
|
||||
}
|
||||
File configFile = new File(plugin.getDataFolder(), "config.yml");
|
||||
if (!configFile.exists()) {
|
||||
Files.copy(plugin.getResourceAsStream("proxy-config.yml"), configFile.toPath());
|
||||
plugin.getBungeeLogger().info("Created HuskSync config file");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getBungeeLogger().log(Level.CONFIG, "An exception occurred loading the configuration file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveConfig(Configuration config) {
|
||||
try {
|
||||
ConfigurationProvider.getProvider(YamlConfiguration.class).save(config, new File(plugin.getDataFolder(), "config.yml"));
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.CONFIG, "An exception occurred loading the configuration file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void loadMessages() {
|
||||
try {
|
||||
if (!plugin.getDataFolder().exists()) {
|
||||
if (plugin.getDataFolder().mkdir()) {
|
||||
plugin.getBungeeLogger().info("Created HuskSync data folder");
|
||||
}
|
||||
}
|
||||
File messagesFile = new File(plugin.getDataFolder(), "messages_" + Settings.language + ".yml");
|
||||
if (!messagesFile.exists()) {
|
||||
Files.copy(plugin.getResourceAsStream("languages/" + Settings.language + ".yml"), messagesFile.toPath());
|
||||
plugin.getBungeeLogger().info("Created HuskSync messages file");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getBungeeLogger().log(Level.CONFIG, "An exception occurred loading the messages file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Configuration getConfig() {
|
||||
try {
|
||||
File configFile = new File(plugin.getDataFolder(), "config.yml");
|
||||
return ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile);
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.CONFIG, "An IOException occurred fetching the configuration file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Configuration getMessages() {
|
||||
try {
|
||||
File configFile = new File(plugin.getDataFolder(), "messages_" + Settings.language + ".yml");
|
||||
return ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile);
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.CONFIG, "An IOException occurred fetching the messages file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.listener;
|
||||
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BungeeEventListener implements Listener {
|
||||
|
||||
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPostLogin(PostLoginEvent event) {
|
||||
final ProxiedPlayer player = event.getPlayer();
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
// Ensure the player has data on SQL and that it is up-to-date
|
||||
HuskSyncBungeeCord.dataManager.ensurePlayerExists(player.getUniqueId(), player.getName());
|
||||
|
||||
// Get the player's data from SQL
|
||||
final Map<Settings.SynchronisationCluster, PlayerData> data = HuskSyncBungeeCord.dataManager.getPlayerData(player.getUniqueId());
|
||||
|
||||
// Update the player's data from SQL onto the cache
|
||||
assert data != null;
|
||||
for (Settings.SynchronisationCluster cluster : data.keySet()) {
|
||||
HuskSyncBungeeCord.dataManager.playerDataCache.get(cluster).updatePlayer(data.get(cluster));
|
||||
}
|
||||
|
||||
// Send a message asking the bukkit to request data on join
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.REQUEST_DATA_ON_JOIN,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null),
|
||||
RedisMessage.RequestOnJoinUpdateType.ADD_REQUESTER.toString(), player.getUniqueId().toString()).send();
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.SEVERE, "Failed to serialize request data on join message data");
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.listener;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.Server;
|
||||
import net.william278.husksync.util.MessageManager;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.migrator.MPDBMigrator;
|
||||
import net.william278.husksync.redis.RedisListener;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.md_5.bungee.api.ChatMessageType;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BungeeRedisListener extends RedisListener {
|
||||
|
||||
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
// Initialize the listener on the bungee
|
||||
public BungeeRedisListener() {
|
||||
super();
|
||||
listen();
|
||||
}
|
||||
|
||||
private PlayerData getPlayerCachedData(UUID uuid, String clusterId) {
|
||||
PlayerData data = null;
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (cluster.clusterId().equals(clusterId)) {
|
||||
// Get the player data from the cache
|
||||
PlayerData cachedData = HuskSyncBungeeCord.dataManager.playerDataCache.get(cluster).getPlayer(uuid);
|
||||
if (cachedData != null) {
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
data = Objects.requireNonNull(HuskSyncBungeeCord.dataManager.getPlayerData(uuid)).get(cluster); // Get their player data from MySQL
|
||||
HuskSyncBungeeCord.dataManager.playerDataCache.get(cluster).updatePlayer(data); // Update the cache
|
||||
break;
|
||||
}
|
||||
}
|
||||
return data; // Return the data
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming {@link RedisMessage}
|
||||
*
|
||||
* @param message The {@link RedisMessage} to handle
|
||||
*/
|
||||
@Override
|
||||
public void handleMessage(RedisMessage message) {
|
||||
// Ignore messages destined for Bukkit servers
|
||||
if (message.getMessageTarget().targetServerType() != Settings.ServerType.PROXY) {
|
||||
return;
|
||||
}
|
||||
// Only process redis messages when ready
|
||||
if (!HuskSyncBungeeCord.readyForRedis) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (message.getMessageType()) {
|
||||
case PLAYER_DATA_REQUEST -> ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
// Get the UUID of the requesting player
|
||||
final UUID requestingPlayerUUID = UUID.fromString(message.getMessageData());
|
||||
try {
|
||||
// Send the reply, serializing the message data
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, requestingPlayerUUID, message.getMessageTarget().targetClusterId()),
|
||||
RedisMessage.serialize(getPlayerCachedData(requestingPlayerUUID, message.getMessageTarget().targetClusterId())))
|
||||
.send();
|
||||
|
||||
// Send an update to all bukkit servers removing the player from the requester cache
|
||||
new RedisMessage(RedisMessage.MessageType.REQUEST_DATA_ON_JOIN,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
|
||||
RedisMessage.RequestOnJoinUpdateType.REMOVE_REQUESTER.toString(), requestingPlayerUUID.toString())
|
||||
.send();
|
||||
|
||||
// Send synchronisation complete message
|
||||
ProxiedPlayer player = ProxyServer.getInstance().getPlayer(requestingPlayerUUID);
|
||||
if (player != null) {
|
||||
player.sendMessage(ChatMessageType.ACTION_BAR, new MineDown(MessageManager.getMessage("synchronisation_complete")).toComponent());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log(Level.SEVERE, "Failed to serialize data when replying to a data request");
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
case PLAYER_DATA_UPDATE -> ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
// Deserialize the PlayerData received
|
||||
PlayerData playerData;
|
||||
final String serializedPlayerData = message.getMessageDataElements()[0];
|
||||
final boolean bounceBack = Boolean.parseBoolean(message.getMessageDataElements()[1]);
|
||||
try {
|
||||
playerData = (PlayerData) RedisMessage.deserialize(serializedPlayerData);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to deserialize PlayerData when handling a player update request");
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the data in the cache and SQL
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (cluster.clusterId().equals(message.getMessageTarget().targetClusterId())) {
|
||||
HuskSyncBungeeCord.dataManager.updatePlayerData(playerData, cluster);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Reply with the player data if they are still online (switching server)
|
||||
if (Settings.bounceBackSynchronisation && bounceBack) {
|
||||
try {
|
||||
ProxiedPlayer player = ProxyServer.getInstance().getPlayer(playerData.getPlayerUUID());
|
||||
if (player != null) {
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, playerData.getPlayerUUID(), message.getMessageTarget().targetClusterId()),
|
||||
serializedPlayerData)
|
||||
.send();
|
||||
|
||||
// Send synchronisation complete message
|
||||
player.sendMessage(ChatMessageType.ACTION_BAR, new MineDown(MessageManager.getMessage("synchronisation_complete")).toComponent());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log(Level.SEVERE, "Failed to re-serialize PlayerData when handling a player update request");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
case CONNECTION_HANDSHAKE -> {
|
||||
// Reply to a Bukkit server's connection handshake to complete the process
|
||||
if (HuskSyncBungeeCord.isDisabling) return; // Return if the Proxy is disabling
|
||||
final UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
final boolean hasMySqlPlayerDataBridge = Boolean.parseBoolean(message.getMessageDataElements()[1]);
|
||||
final String bukkitBrand = message.getMessageDataElements()[2];
|
||||
final String huskSyncVersion = message.getMessageDataElements()[3];
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.CONNECTION_HANDSHAKE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
|
||||
serverUUID.toString(), plugin.getProxy().getName())
|
||||
.send();
|
||||
HuskSyncBungeeCord.synchronisedServers.add(
|
||||
new Server(serverUUID, hasMySqlPlayerDataBridge,
|
||||
huskSyncVersion, bukkitBrand, message.getMessageTarget().targetClusterId()));
|
||||
log(Level.INFO, "Completed handshake with " + bukkitBrand + " server (" + serverUUID + ")");
|
||||
} catch (IOException e) {
|
||||
log(Level.SEVERE, "Failed to serialize handshake message data");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
case TERMINATE_HANDSHAKE -> {
|
||||
// Terminate the handshake with a Bukkit server
|
||||
final UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
final String bukkitBrand = message.getMessageDataElements()[1];
|
||||
|
||||
// Remove a server from the synchronised server list
|
||||
Server serverToRemove = null;
|
||||
for (Server server : HuskSyncBungeeCord.synchronisedServers) {
|
||||
if (server.serverUUID().equals(serverUUID)) {
|
||||
serverToRemove = server;
|
||||
break;
|
||||
}
|
||||
}
|
||||
HuskSyncBungeeCord.synchronisedServers.remove(serverToRemove);
|
||||
log(Level.INFO, "Terminated the handshake with " + bukkitBrand + " server (" + serverUUID + ")");
|
||||
}
|
||||
case DECODED_MPDB_DATA_SET -> {
|
||||
// Deserialize the PlayerData received
|
||||
PlayerData playerData;
|
||||
final String serializedPlayerData = message.getMessageDataElements()[0];
|
||||
final String playerName = message.getMessageDataElements()[1];
|
||||
try {
|
||||
playerData = (PlayerData) RedisMessage.deserialize(serializedPlayerData);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to deserialize PlayerData when handling incoming decoded MPDB data");
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the migrator
|
||||
MPDBMigrator migrator = HuskSyncBungeeCord.mpdbMigrator;
|
||||
|
||||
// Add the incoming data to the data to be saved
|
||||
migrator.incomingPlayerData.put(playerData, playerName);
|
||||
|
||||
// Increment players migrated
|
||||
migrator.playersMigrated++;
|
||||
plugin.getBungeeLogger().log(Level.INFO, "Migrated " + migrator.playersMigrated + "/" + migrator.migratedDataSent + " players.");
|
||||
|
||||
// When all the data has been received, save it
|
||||
if (migrator.migratedDataSent == migrator.playersMigrated) {
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> migrator.loadIncomingData(migrator.incomingPlayerData,
|
||||
HuskSyncBungeeCord.dataManager));
|
||||
}
|
||||
}
|
||||
case API_DATA_REQUEST -> ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
final UUID playerUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[1]);
|
||||
try {
|
||||
final PlayerData data = getPlayerCachedData(playerUUID, message.getMessageTarget().targetClusterId());
|
||||
|
||||
if (data == null) {
|
||||
new RedisMessage(RedisMessage.MessageType.API_DATA_CANCEL,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
|
||||
requestUUID.toString())
|
||||
.send();
|
||||
} else {
|
||||
// Send the reply alongside the request UUID, serializing the requested message data
|
||||
new RedisMessage(RedisMessage.MessageType.API_DATA_RETURN,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
|
||||
requestUUID.toString(),
|
||||
RedisMessage.serialize(data))
|
||||
.send();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.SEVERE, "Failed to serialize PlayerData requested via the API");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log to console
|
||||
*
|
||||
* @param level The {@link Level} to log
|
||||
* @param message Message to log
|
||||
*/
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
plugin.getBungeeLogger().log(level, message);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.util;
|
||||
|
||||
import net.william278.husksync.util.Logger;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public record BungeeLogger(java.util.logging.Logger parent) implements Logger {
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message, Exception e) {
|
||||
parent.log(level, message, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
parent.log(level, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(String message) {
|
||||
parent.info(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void severe(String message) {
|
||||
parent.severe(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void config(String message) {
|
||||
parent.config(message);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.util;
|
||||
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.util.UpdateChecker;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BungeeUpdateChecker extends UpdateChecker {
|
||||
|
||||
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
public BungeeUpdateChecker(String versionToCheck) {
|
||||
super(versionToCheck);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
plugin.getBungeeLogger().log(level, message);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
name: HuskSync
|
||||
version: ${version}
|
||||
main: net.william278.husksync.HuskSyncBungeeCord
|
||||
author: William278
|
||||
description: 'A modern, cross-server player data synchronization system'
|
||||
@@ -1,11 +1,29 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
dependencies {
|
||||
implementation 'commons-io:commons-io:2.11.0'
|
||||
implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
implementation 'dev.dejvokep:boosted-yaml:1.3'
|
||||
implementation ('com.zaxxer:HikariCP:5.0.1') {
|
||||
exclude module: 'slf4j-api'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly 'com.zaxxer:HikariCP:5.0.1'
|
||||
compileOnly 'redis.clients:jedis:' + jedis_version
|
||||
compileOnly 'org.xerial.snappy:snappy-java:' + snappy_version
|
||||
compileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
compileOnly 'com.github.plan-player-analytics:Plan:5.4.1690'
|
||||
|
||||
testImplementation 'org.xerial.snappy:snappy-java:1.1.8.4'
|
||||
testImplementation 'com.github.plan-player-analytics:Plan:5.4.1690'
|
||||
testCompileOnly 'dev.dejvokep:boosted-yaml:1.3'
|
||||
testCompileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'org.apache.commons.io', 'net.william278.husksync.libraries.commons.io'
|
||||
relocate 'com.google.gson', 'net.william278.husksync.libraries.gson'
|
||||
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 'dev.dejvokep', 'net.william278.husksync.libraries'
|
||||
}
|
||||
149
common/src/main/java/net/william278/husksync/HuskSync.java
Normal file
149
common/src/main/java/net/william278/husksync/HuskSync.java
Normal file
@@ -0,0 +1,149 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.DataAdapter;
|
||||
import net.william278.husksync.editor.DataEditor;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.event.EventCannon;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.util.Logger;
|
||||
import net.william278.husksync.util.ResourceReader;
|
||||
import net.william278.husksync.util.Version;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Abstract implementation of the HuskSync plugin.
|
||||
*/
|
||||
public interface HuskSync {
|
||||
|
||||
/**
|
||||
* Returns a set of online players.
|
||||
*
|
||||
* @return a set of online players as {@link OnlineUser}
|
||||
*/
|
||||
@NotNull
|
||||
Set<OnlineUser> getOnlineUsers();
|
||||
|
||||
/**
|
||||
* Returns an online user by UUID if they exist
|
||||
*
|
||||
* @param uuid the UUID of the user to get
|
||||
* @return an online user as {@link OnlineUser}
|
||||
*/
|
||||
@NotNull
|
||||
Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid);
|
||||
|
||||
/**
|
||||
* Returns the database implementation
|
||||
*
|
||||
* @return the {@link Database} implementation
|
||||
*/
|
||||
@NotNull
|
||||
Database getDatabase();
|
||||
|
||||
/**
|
||||
* Returns the redis manager implementation
|
||||
*
|
||||
* @return the {@link RedisManager} implementation
|
||||
*/
|
||||
|
||||
@NotNull
|
||||
RedisManager getRedisManager();
|
||||
|
||||
/**
|
||||
* Returns the data adapter implementation
|
||||
*
|
||||
* @return the {@link DataAdapter} implementation
|
||||
*/
|
||||
@NotNull
|
||||
DataAdapter getDataAdapter();
|
||||
|
||||
/**
|
||||
* Returns the data editor implementation
|
||||
*
|
||||
* @return the {@link DataEditor} implementation
|
||||
*/
|
||||
@NotNull
|
||||
DataEditor getDataEditor();
|
||||
|
||||
/**
|
||||
* Returns the event firing cannon
|
||||
*
|
||||
* @return the {@link EventCannon} implementation
|
||||
*/
|
||||
@NotNull
|
||||
EventCannon getEventCannon();
|
||||
|
||||
/**
|
||||
* Returns a list of available data {@link Migrator}s
|
||||
*
|
||||
* @return a list of {@link Migrator}s
|
||||
*/
|
||||
@NotNull
|
||||
List<Migrator> getAvailableMigrators();
|
||||
|
||||
/**
|
||||
* Returns the plugin {@link Settings}
|
||||
*
|
||||
* @return the {@link Settings}
|
||||
*/
|
||||
@NotNull
|
||||
Settings getSettings();
|
||||
|
||||
/**
|
||||
* Returns the plugin {@link Locales}
|
||||
*
|
||||
* @return the {@link Locales}
|
||||
*/
|
||||
@NotNull
|
||||
Locales getLocales();
|
||||
|
||||
/**
|
||||
* Returns the plugin {@link Logger}
|
||||
*
|
||||
* @return the {@link Logger}
|
||||
*/
|
||||
@NotNull
|
||||
Logger getLoggingAdapter();
|
||||
|
||||
/**
|
||||
* Returns the plugin resource file reader
|
||||
*
|
||||
* @return the {@link ResourceReader}
|
||||
*/
|
||||
@NotNull
|
||||
ResourceReader getResourceReader();
|
||||
|
||||
/**
|
||||
* Returns the plugin version
|
||||
*
|
||||
* @return the plugin {@link Version}
|
||||
*/
|
||||
@NotNull
|
||||
Version getPluginVersion();
|
||||
|
||||
/**
|
||||
* Returns the Minecraft version implementation
|
||||
*
|
||||
* @return the Minecraft {@link Version}
|
||||
*/
|
||||
@NotNull
|
||||
Version getMinecraftVersion();
|
||||
|
||||
/**
|
||||
* Reloads the {@link Settings} and {@link Locales} from their respective config files
|
||||
*
|
||||
* @return a {@link CompletableFuture} that will be completed when the plugin reload is complete and if it was successful
|
||||
*/
|
||||
CompletableFuture<Boolean> reload();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Indicates an exception occurred while initialising the HuskSync plugin
|
||||
*/
|
||||
public class HuskSyncInitializationException extends RuntimeException {
|
||||
public HuskSyncInitializationException(@NotNull String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,533 +0,0 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import java.io.*;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Cross-platform class used to represent a player's data. Data from this can be deserialized using the DataSerializer class on Bukkit platforms.
|
||||
*/
|
||||
public class PlayerData implements Serializable {
|
||||
|
||||
/**
|
||||
* The UUID of the player who this data belongs to
|
||||
*/
|
||||
private final UUID playerUUID;
|
||||
|
||||
/**
|
||||
* The unique version UUID of this data
|
||||
*/
|
||||
private final UUID dataVersionUUID;
|
||||
|
||||
/**
|
||||
* Epoch time identifying when the data was last updated or created
|
||||
*/
|
||||
private long timestamp;
|
||||
|
||||
/**
|
||||
* A special flag that will be {@code true} if the player is new to the network and should not have their data set when joining the Bukkit
|
||||
*/
|
||||
public boolean useDefaultData = false;
|
||||
|
||||
/*
|
||||
* Player data records
|
||||
*/
|
||||
private String serializedInventory;
|
||||
private String serializedEnderChest;
|
||||
private double health;
|
||||
private double maxHealth;
|
||||
private double healthScale;
|
||||
private int hunger;
|
||||
private float saturation;
|
||||
private float saturationExhaustion;
|
||||
private int selectedSlot;
|
||||
private String serializedEffectData;
|
||||
private int totalExperience;
|
||||
private int expLevel;
|
||||
private float expProgress;
|
||||
private String gameMode;
|
||||
private String serializedStatistics;
|
||||
private boolean isFlying;
|
||||
private String serializedAdvancements;
|
||||
private String serializedLocation;
|
||||
|
||||
/**
|
||||
* Constructor to create new PlayerData from a bukkit {@code Player}'s data
|
||||
*
|
||||
* @param playerUUID The Player's UUID
|
||||
* @param serializedInventory Their serialized inventory
|
||||
* @param serializedEnderChest Their serialized ender chest
|
||||
* @param health Their health
|
||||
* @param maxHealth Their max health
|
||||
* @param healthScale Their health scale
|
||||
* @param hunger Their hunger
|
||||
* @param saturation Their saturation
|
||||
* @param saturationExhaustion Their saturation exhaustion
|
||||
* @param selectedSlot Their selected hot bar slot
|
||||
* @param serializedStatusEffects Their serialized status effects
|
||||
* @param totalExperience Their total experience points ("Score")
|
||||
* @param expLevel Their exp level
|
||||
* @param expProgress Their exp progress to the next level
|
||||
* @param gameMode Their game mode ({@code SURVIVAL}, {@code CREATIVE}, etc.)
|
||||
* @param serializedStatistics Their serialized statistics data (Displayed in Statistics menu in ESC menu)
|
||||
*/
|
||||
public PlayerData(UUID playerUUID, String serializedInventory, String serializedEnderChest, double health, double maxHealth,
|
||||
double healthScale, int hunger, float saturation, float saturationExhaustion, int selectedSlot,
|
||||
String serializedStatusEffects, int totalExperience, int expLevel, float expProgress, String gameMode,
|
||||
String serializedStatistics, boolean isFlying, String serializedAdvancements, String serializedLocation) {
|
||||
this.dataVersionUUID = UUID.randomUUID();
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
this.playerUUID = playerUUID;
|
||||
this.serializedInventory = serializedInventory;
|
||||
this.serializedEnderChest = serializedEnderChest;
|
||||
this.health = health;
|
||||
this.maxHealth = maxHealth;
|
||||
this.healthScale = healthScale;
|
||||
this.hunger = hunger;
|
||||
this.saturation = saturation;
|
||||
this.saturationExhaustion = saturationExhaustion;
|
||||
this.selectedSlot = selectedSlot;
|
||||
this.serializedEffectData = serializedStatusEffects;
|
||||
this.totalExperience = totalExperience;
|
||||
this.expLevel = expLevel;
|
||||
this.expProgress = expProgress;
|
||||
this.gameMode = gameMode;
|
||||
this.serializedStatistics = serializedStatistics;
|
||||
this.isFlying = isFlying;
|
||||
this.serializedAdvancements = serializedAdvancements;
|
||||
this.serializedLocation = serializedLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for a PlayerData object from an existing object that was stored in SQL
|
||||
*
|
||||
* @param playerUUID The player whose data this is' UUID
|
||||
* @param dataVersionUUID The PlayerData version UUID
|
||||
* @param serializedInventory Their serialized inventory
|
||||
* @param serializedEnderChest Their serialized ender chest
|
||||
* @param health Their health
|
||||
* @param maxHealth Their max health
|
||||
* @param healthScale Their health scale
|
||||
* @param hunger Their hunger
|
||||
* @param saturation Their saturation
|
||||
* @param saturationExhaustion Their saturation exhaustion
|
||||
* @param selectedSlot Their selected hot bar slot
|
||||
* @param serializedStatusEffects Their serialized status effects
|
||||
* @param totalExperience Their total experience points ("Score")
|
||||
* @param expLevel Their exp level
|
||||
* @param expProgress Their exp progress to the next level
|
||||
* @param gameMode Their game mode ({@code SURVIVAL}, {@code CREATIVE}, etc.)
|
||||
* @param serializedStatistics Their serialized statistics data (Displayed in Statistics menu in ESC menu)
|
||||
*/
|
||||
public PlayerData(UUID playerUUID, UUID dataVersionUUID, long timestamp, String serializedInventory, String serializedEnderChest,
|
||||
double health, double maxHealth, double healthScale, int hunger, float saturation, float saturationExhaustion,
|
||||
int selectedSlot, String serializedStatusEffects, int totalExperience, int expLevel, float expProgress,
|
||||
String gameMode, String serializedStatistics, boolean isFlying, String serializedAdvancements,
|
||||
String serializedLocation) {
|
||||
this.playerUUID = playerUUID;
|
||||
this.dataVersionUUID = dataVersionUUID;
|
||||
this.timestamp = timestamp;
|
||||
this.serializedInventory = serializedInventory;
|
||||
this.serializedEnderChest = serializedEnderChest;
|
||||
this.health = health;
|
||||
this.maxHealth = maxHealth;
|
||||
this.healthScale = healthScale;
|
||||
this.hunger = hunger;
|
||||
this.saturation = saturation;
|
||||
this.saturationExhaustion = saturationExhaustion;
|
||||
this.selectedSlot = selectedSlot;
|
||||
this.serializedEffectData = serializedStatusEffects;
|
||||
this.totalExperience = totalExperience;
|
||||
this.expLevel = expLevel;
|
||||
this.expProgress = expProgress;
|
||||
this.gameMode = gameMode;
|
||||
this.serializedStatistics = serializedStatistics;
|
||||
this.isFlying = isFlying;
|
||||
this.serializedAdvancements = serializedAdvancements;
|
||||
this.serializedLocation = serializedLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default PlayerData for a new user
|
||||
*
|
||||
* @param playerUUID The bukkit Player's UUID
|
||||
* @return Default {@link PlayerData}
|
||||
*/
|
||||
public static PlayerData DEFAULT_PLAYER_DATA(UUID playerUUID) {
|
||||
PlayerData data = new PlayerData(playerUUID, "", "", 20,
|
||||
20, 20, 20, 10, 1, 0,
|
||||
"", 0, 0, 0, "SURVIVAL",
|
||||
"", false, "", "");
|
||||
data.useDefaultData = true;
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link UUID} of the player whose data this is
|
||||
*
|
||||
* @return the player's {@link UUID}
|
||||
*/
|
||||
public UUID getPlayerUUID() {
|
||||
return playerUUID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique version {@link UUID} of the PlayerData
|
||||
*
|
||||
* @return The unique data version
|
||||
*/
|
||||
public UUID getDataVersionUUID() {
|
||||
return dataVersionUUID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timestamp when this data was created or last updated
|
||||
*
|
||||
* @return time since epoch of last data update or creation
|
||||
*/
|
||||
public long getDataTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized player {@code ItemStack[]} inventory
|
||||
*
|
||||
* @return The player's serialized inventory
|
||||
*/
|
||||
public String getSerializedInventory() {
|
||||
return serializedInventory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized player {@code ItemStack[]} ender chest
|
||||
*
|
||||
* @return The player's serialized ender chest
|
||||
*/
|
||||
public String getSerializedEnderChest() {
|
||||
return serializedEnderChest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's health value
|
||||
*
|
||||
* @return the player's health
|
||||
*/
|
||||
public double getHealth() {
|
||||
return health;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's max health value
|
||||
*
|
||||
* @return the player's max health
|
||||
*/
|
||||
public double getMaxHealth() {
|
||||
return maxHealth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's health scale value {@see https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/entity/Player.html#getHealthScale()}
|
||||
*
|
||||
* @return the player's health scaling value
|
||||
*/
|
||||
public double getHealthScale() {
|
||||
return healthScale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's hunger points
|
||||
*
|
||||
* @return the player's hunger level
|
||||
*/
|
||||
public int getHunger() {
|
||||
return hunger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's saturation points
|
||||
*
|
||||
* @return the player's saturation level
|
||||
*/
|
||||
public float getSaturation() {
|
||||
return saturation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's saturation exhaustion value {@see https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/entity/HumanEntity.html#getExhaustion()}
|
||||
*
|
||||
* @return the player's saturation exhaustion
|
||||
*/
|
||||
public float getSaturationExhaustion() {
|
||||
return saturationExhaustion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of the player's currently selected hotbar slot
|
||||
*
|
||||
* @return the player's selected hotbar slot
|
||||
*/
|
||||
public int getSelectedSlot() {
|
||||
return selectedSlot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of the player's current status effects
|
||||
*
|
||||
* @return the player's serialized status effect data
|
||||
*/
|
||||
public String getSerializedEffectData() {
|
||||
return serializedEffectData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's total experience score (used for presenting the death screen score value)
|
||||
*
|
||||
* @return the player's total experience score
|
||||
*/
|
||||
public int getTotalExperience() {
|
||||
return totalExperience;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of the player's statistics
|
||||
*
|
||||
* @return the player's serialized statistic records
|
||||
*/
|
||||
public String getSerializedStatistics() {
|
||||
return serializedStatistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's current experience level
|
||||
*
|
||||
* @return the player's exp level
|
||||
*/
|
||||
public int getExpLevel() {
|
||||
return expLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's progress to the next experience level
|
||||
*
|
||||
* @return the player's exp progress
|
||||
*/
|
||||
public float getExpProgress() {
|
||||
return expProgress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's current game mode as a string ({@code SURVIVAL}, {@code CREATIVE}, etc.)
|
||||
*
|
||||
* @return the player's game mode
|
||||
*/
|
||||
public String getGameMode() {
|
||||
return gameMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the player is currently flying
|
||||
*
|
||||
* @return {@code true} if the player is in flight; {@code false} otherwise
|
||||
*/
|
||||
public boolean isFlying() {
|
||||
return isFlying;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of the player's advancements
|
||||
*
|
||||
* @return the player's serialized advancement data
|
||||
*/
|
||||
public String getSerializedAdvancements() {
|
||||
return serializedAdvancements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of the player's current location
|
||||
*
|
||||
* @return the player's serialized location
|
||||
*/
|
||||
public String getSerializedLocation() {
|
||||
return serializedLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's inventory data
|
||||
*
|
||||
* @param serializedInventory A serialized {@code String}; new inventory data
|
||||
*/
|
||||
public void setSerializedInventory(String serializedInventory) {
|
||||
this.serializedInventory = serializedInventory;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's ender chest data
|
||||
*
|
||||
* @param serializedEnderChest A serialized {@code String}; new ender chest inventory data
|
||||
*/
|
||||
public void setSerializedEnderChest(String serializedEnderChest) {
|
||||
this.serializedEnderChest = serializedEnderChest;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's health
|
||||
*
|
||||
* @param health new health value
|
||||
*/
|
||||
public void setHealth(double health) {
|
||||
this.health = health;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's max health
|
||||
*
|
||||
* @param maxHealth new maximum health value
|
||||
*/
|
||||
public void setMaxHealth(double maxHealth) {
|
||||
this.maxHealth = maxHealth;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's health scale
|
||||
*
|
||||
* @param healthScale new health scaling value
|
||||
*/
|
||||
public void setHealthScale(double healthScale) {
|
||||
this.healthScale = healthScale;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's hunger meter
|
||||
*
|
||||
* @param hunger new hunger value
|
||||
*/
|
||||
public void setHunger(int hunger) {
|
||||
this.hunger = hunger;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's saturation level
|
||||
*
|
||||
* @param saturation new saturation value
|
||||
*/
|
||||
public void setSaturation(float saturation) {
|
||||
this.saturation = saturation;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's saturation exhaustion value
|
||||
*
|
||||
* @param saturationExhaustion new exhaustion value
|
||||
*/
|
||||
public void setSaturationExhaustion(float saturationExhaustion) {
|
||||
this.saturationExhaustion = saturationExhaustion;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's selected hotbar slot
|
||||
*
|
||||
* @param selectedSlot new hotbar slot number (0-9)
|
||||
*/
|
||||
public void setSelectedSlot(int selectedSlot) {
|
||||
this.selectedSlot = selectedSlot;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's status effect data
|
||||
*
|
||||
* @param serializedEffectData A serialized {@code String} of the player's new status effect data
|
||||
*/
|
||||
public void setSerializedEffectData(String serializedEffectData) {
|
||||
this.serializedEffectData = serializedEffectData;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the player's total experience points (used to display score on death screen)
|
||||
*
|
||||
* @param totalExperience the player's new total experience score
|
||||
*/
|
||||
public void setTotalExperience(int totalExperience) {
|
||||
this.totalExperience = totalExperience;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the player's exp level
|
||||
*
|
||||
* @param expLevel the player's new exp level
|
||||
*/
|
||||
public void setExpLevel(int expLevel) {
|
||||
this.expLevel = expLevel;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the player's progress to their next exp level
|
||||
*
|
||||
* @param expProgress the player's new experience progress
|
||||
*/
|
||||
public void setExpProgress(float expProgress) {
|
||||
this.expProgress = expProgress;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the player's game mode
|
||||
*
|
||||
* @param gameMode the player's new game mode ({@code SURVIVAL}, {@code CREATIVE}, etc.)
|
||||
*/
|
||||
public void setGameMode(String gameMode) {
|
||||
this.gameMode = gameMode;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's statistics data
|
||||
*
|
||||
* @param serializedStatistics A serialized {@code String}; new statistic data
|
||||
*/
|
||||
public void setSerializedStatistics(String serializedStatistics) {
|
||||
this.serializedStatistics = serializedStatistics;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if the player is flying
|
||||
*
|
||||
* @param flying whether the player is flying
|
||||
*/
|
||||
public void setFlying(boolean flying) {
|
||||
isFlying = flying;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's advancement data
|
||||
*
|
||||
* @param serializedAdvancements A serialized {@code String}; new advancement data
|
||||
*/
|
||||
public void setSerializedAdvancements(String serializedAdvancements) {
|
||||
this.serializedAdvancements = serializedAdvancements;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's location data
|
||||
*
|
||||
* @param serializedLocation A serialized {@code String}; new location data
|
||||
*/
|
||||
public void setSerializedLocation(String serializedLocation) {
|
||||
this.serializedLocation = serializedLocation;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A record representing a server synchronised on the network and whether it has MySqlPlayerDataBridge installed
|
||||
*/
|
||||
public record Server(UUID serverUUID, boolean hasMySqlPlayerDataBridge, String huskSyncVersion, String serverBrand,
|
||||
String clusterId) {
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Settings class, holds values loaded from the plugin config (either Bukkit or Bungee)
|
||||
*/
|
||||
public class Settings {
|
||||
|
||||
/*
|
||||
* General settings
|
||||
*/
|
||||
|
||||
// Whether to do automatic update checks on startup
|
||||
public static boolean automaticUpdateChecks;
|
||||
|
||||
// The type of THIS server (Bungee or Bukkit)
|
||||
public static ServerType serverType;
|
||||
|
||||
// Redis settings
|
||||
public static String redisHost;
|
||||
public static int redisPort;
|
||||
public static String redisPassword;
|
||||
public static boolean redisSSL;
|
||||
|
||||
/*
|
||||
* Bungee / Proxy server-only settings
|
||||
*/
|
||||
|
||||
// Messages language
|
||||
public static String language;
|
||||
|
||||
// Cluster IDs
|
||||
public static ArrayList<SynchronisationCluster> clusters = new ArrayList<>();
|
||||
|
||||
// SQL settings
|
||||
public static DataStorageType dataStorageType;
|
||||
|
||||
// Bounce-back synchronisation (default)
|
||||
public static boolean bounceBackSynchronisation;
|
||||
|
||||
// MySQL specific settings
|
||||
public static String mySQLHost;
|
||||
public static String mySQLDatabase;
|
||||
public static String mySQLUsername;
|
||||
public static String mySQLPassword;
|
||||
public static int mySQLPort;
|
||||
public static String mySQLParams;
|
||||
|
||||
// Hikari connection pooling settings
|
||||
public static int hikariMaximumPoolSize;
|
||||
public static int hikariMinimumIdle;
|
||||
public static long hikariMaximumLifetime;
|
||||
public static long hikariKeepAliveTime;
|
||||
public static long hikariConnectionTimeOut;
|
||||
|
||||
/*
|
||||
* Bukkit server-only settings
|
||||
*/
|
||||
|
||||
// Synchronisation options
|
||||
public static boolean syncInventories;
|
||||
public static boolean syncEnderChests;
|
||||
public static boolean syncHealth;
|
||||
public static boolean syncHunger;
|
||||
public static boolean syncExperience;
|
||||
public static boolean syncPotionEffects;
|
||||
public static boolean syncStatistics;
|
||||
public static boolean syncGameMode;
|
||||
public static boolean syncAdvancements;
|
||||
public static boolean syncLocation;
|
||||
public static boolean syncFlight;
|
||||
public static long synchronizationTimeoutRetryDelay;
|
||||
public static boolean saveOnWorldSave;
|
||||
public static boolean useNativeImplementation;
|
||||
|
||||
// This Cluster ID
|
||||
public static String cluster;
|
||||
|
||||
/*
|
||||
* Enum definitions
|
||||
*/
|
||||
|
||||
public enum ServerType {
|
||||
BUKKIT,
|
||||
PROXY,
|
||||
}
|
||||
|
||||
public enum DataStorageType {
|
||||
MYSQL,
|
||||
SQLITE
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines information for a synchronisation cluster as listed on the proxy
|
||||
*/
|
||||
public record SynchronisationCluster(String clusterId, String databaseName, String playerTableName, String dataTableName) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package net.william278.husksync.api;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* The base implementation of the HuskSync API, containing cross-platform API calls.
|
||||
* </p>
|
||||
* This class should not be used directly, but rather through platform-specific extending API classes.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class BaseHuskSyncAPI {
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Instance of the implementing plugin.
|
||||
*/
|
||||
protected final HuskSync plugin;
|
||||
|
||||
protected BaseHuskSyncAPI(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link User} by the given player's account {@link UUID}, if they exist.
|
||||
*
|
||||
* @param uuid the unique id of the player to get the {@link User} instance for
|
||||
* @return future returning the {@link User} instance for the given player's unique id if they exist, otherwise an empty {@link Optional}
|
||||
* @apiNote The player does not have to be online
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid) {
|
||||
return plugin.getDatabase().getUser(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link User} by the given player's username (case-insensitive), if they exist.
|
||||
*
|
||||
* @param username the username of the {@link User} instance for
|
||||
* @return future returning the {@link User} instance for the given player's username if they exist,
|
||||
* otherwise an empty {@link Optional}
|
||||
* @apiNote The player does not have to be online, though their username has to be the username
|
||||
* they had when they last joined the server.
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Optional<User>> getUser(@NotNull String username) {
|
||||
return plugin.getDatabase().getUserByName(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link User}'s current {@link UserData}
|
||||
*
|
||||
* @param user the {@link User} to get the {@link UserData} for
|
||||
* @return future returning the {@link UserData} for the given {@link User} if they exist, otherwise an empty {@link Optional}
|
||||
* @apiNote If the user is not online on the implementing bukkit server,
|
||||
* the {@link UserData} returned will be their last database-saved UserData.
|
||||
* </p>
|
||||
* Because of this, if the user is online on another server on the network,
|
||||
* then the {@link UserData} returned by this method will <i>not necessarily reflective of
|
||||
* their current state</i>
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Optional<UserData>> getUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (user instanceof OnlineUser) {
|
||||
return ((OnlineUser) user).getUserData(plugin.getLoggingAdapter(), plugin.getSettings()).join();
|
||||
} else {
|
||||
return plugin.getDatabase().getCurrentUserData(user).join().map(UserDataSnapshot::userData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link UserData} to the database for the given {@link User}.
|
||||
* </p>
|
||||
* If the user is online and on the same cluster, their data will be updated in game.
|
||||
*
|
||||
* @param user the {@link User} to set the {@link UserData} for
|
||||
* @param userData the {@link UserData} to set for the given {@link User}
|
||||
* @return future returning void when complete
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData) {
|
||||
return CompletableFuture.runAsync(() ->
|
||||
plugin.getDatabase().setUserData(user, userData, DataSaveCause.API)
|
||||
.thenRun(() -> plugin.getRedisManager().sendUserDataUpdate(user, userData).join()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the {@link UserData} of an {@link OnlineUser} to the database
|
||||
*
|
||||
* @param user the {@link OnlineUser} to save the {@link UserData} of
|
||||
* @return future returning void when complete
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Void> saveUserData(@NotNull OnlineUser user) {
|
||||
return CompletableFuture.runAsync(() -> user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings())
|
||||
.thenAccept(optionalUserData -> optionalUserData.ifPresent(
|
||||
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.API).join())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the saved {@link UserDataSnapshot} records for the given {@link User}
|
||||
*
|
||||
* @param user the {@link User} to get the {@link UserDataSnapshot} for
|
||||
* @return future returning a list {@link UserDataSnapshot} for the given {@link User} if they exist,
|
||||
* otherwise an empty {@link Optional}
|
||||
* @apiNote The length of the list of VersionedUserData will correspond to the configured
|
||||
* {@code max_user_data_records} config option
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<List<UserDataSnapshot>> getSavedUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> plugin.getDatabase().getUserData(user).join());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON string representation of the given {@link UserData}
|
||||
*
|
||||
* @param userData the {@link UserData} to get the JSON string representation of
|
||||
* @param prettyPrint whether to pretty print the JSON string
|
||||
* @return the JSON string representation of the given {@link UserData}
|
||||
* @since 2.0
|
||||
*/
|
||||
@NotNull
|
||||
public final String getUserDataJson(@NotNull UserData userData, boolean prettyPrint) {
|
||||
return plugin.getDataAdapter().toJson(userData, prettyPrint);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Represents an abstract cross-platform representation for a plugin command
|
||||
*/
|
||||
public abstract class CommandBase {
|
||||
|
||||
/**
|
||||
* The input string to match for this command
|
||||
*/
|
||||
public final String command;
|
||||
|
||||
/**
|
||||
* The permission node required to use this command
|
||||
*/
|
||||
public final String permission;
|
||||
|
||||
/**
|
||||
* Alias input strings for this command
|
||||
*/
|
||||
public final String[] aliases;
|
||||
|
||||
/**
|
||||
* Instance of the implementing plugin
|
||||
*/
|
||||
public final HuskSync plugin;
|
||||
|
||||
|
||||
public CommandBase(@NotNull String command, @NotNull Permission permission, @NotNull HuskSync implementor, String... aliases) {
|
||||
this.command = command;
|
||||
this.permission = permission.node;
|
||||
this.plugin = implementor;
|
||||
this.aliases = aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when the command is executed
|
||||
*
|
||||
* @param player {@link OnlineUser} executing the command
|
||||
* @param args Command arguments
|
||||
*/
|
||||
public abstract void onExecute(@NotNull OnlineUser player, @NotNull String[] args);
|
||||
|
||||
/**
|
||||
* Returns the localised description string of this command
|
||||
*
|
||||
* @return the command description
|
||||
*/
|
||||
public String getDescription() {
|
||||
return plugin.getLocales().getRawLocale(command + "_command_description")
|
||||
.orElse("A HuskHomes command");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Interface providing console execution of commands
|
||||
*/
|
||||
public interface ConsoleExecutable {
|
||||
|
||||
/**
|
||||
* What to do when console executes a command
|
||||
*
|
||||
* @param args command argument strings
|
||||
*/
|
||||
void onConsoleExecute(@NotNull String[] args);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.editor.ItemEditorMenu;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class EnderChestCommand extends CommandBase implements TabCompletable {
|
||||
|
||||
public EnderChestCommand(@NotNull HuskSync implementor) {
|
||||
super("enderchest", Permission.COMMAND_ENDER_CHEST, implementor, "echest", "openechest");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
|
||||
if (args.length == 0 || args.length > 2) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax", "/enderchest <player>")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAccept(optionalUser ->
|
||||
optionalUser.ifPresentOrElse(user -> {
|
||||
if (args.length == 2) {
|
||||
// View user data by specified UUID
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[1]);
|
||||
plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> data.ifPresentOrElse(
|
||||
userData -> showEnderChestMenu(player, userData, user, false),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/enderchest <player> [version_uuid]").ifPresent(player::sendMessage);
|
||||
}
|
||||
} else {
|
||||
// View latest user data
|
||||
plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse(
|
||||
versionedUserData -> showEnderChestMenu(player, versionedUserData, user, true),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(player::sendMessage)));
|
||||
}
|
||||
}, () -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage)));
|
||||
}
|
||||
|
||||
private void showEnderChestMenu(@NotNull OnlineUser player, @NotNull UserDataSnapshot userDataSnapshot,
|
||||
@NotNull User dataOwner, final boolean allowEdit) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
final UserData data = userDataSnapshot.userData();
|
||||
final ItemEditorMenu menu = ItemEditorMenu.createEnderChestMenu(data.getEnderChestData(),
|
||||
dataOwner, player, plugin.getLocales(), allowEdit);
|
||||
plugin.getLocales().getLocale("viewing_ender_chest_of", dataOwner.username,
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
||||
.format(userDataSnapshot.versionTimestamp()))
|
||||
.ifPresent(player::sendMessage);
|
||||
plugin.getDataEditor().openItemEditorMenu(player, menu).thenAccept(enderChestDataOnClose -> {
|
||||
if (!menu.canEdit) {
|
||||
return;
|
||||
}
|
||||
final UserData updatedUserData = new UserData(data.getStatusData(), data.getInventoryData(),
|
||||
enderChestDataOnClose, data.getPotionEffectsData(), data.getAdvancementData(),
|
||||
data.getStatisticsData(), data.getLocationData(),
|
||||
data.getPersistentDataContainerData(),
|
||||
plugin.getMinecraftVersion().toString());
|
||||
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.ENDERCHEST_COMMAND).join();
|
||||
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull String[] args) {
|
||||
return plugin.getOnlineUsers().stream().map(user -> user.username)
|
||||
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
|
||||
.sorted().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.util.UpdateChecker;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class HuskSyncCommand extends CommandBase implements TabCompletable, ConsoleExecutable {
|
||||
|
||||
private final String[] COMMAND_ARGUMENTS = {"update", "about", "reload", "migrate"};
|
||||
|
||||
public HuskSyncCommand(@NotNull HuskSync implementor) {
|
||||
super("husksync", Permission.COMMAND_HUSKSYNC, implementor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
|
||||
if (args.length < 1) {
|
||||
displayPluginInformation(player);
|
||||
return;
|
||||
}
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "update", "version" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_UPDATE.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
final UpdateChecker updateChecker = new UpdateChecker(plugin.getPluginVersion(), plugin.getLoggingAdapter());
|
||||
updateChecker.fetchLatestVersion().thenAccept(latestVersion -> {
|
||||
if (updateChecker.isUpdateAvailable(latestVersion)) {
|
||||
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| A new update is available:](#00fb9a) [HuskSync " + latestVersion + "](#00fb9a bold)" +
|
||||
"[•](white) [Currently running:](#00fb9a) [Version " + updateChecker.getCurrentVersion() + "](gray)" +
|
||||
"[•](white) [Download links:](#00fb9a) [[⏩ Spigot]](gray open_url=https://www.spigotmc.org/resources/husksync.97144/updates) [•](#262626) [[⏩ Polymart]](gray open_url=https://polymart.org/resource/husksync.1634/updates) [•](#262626) [[⏩ Songoda]](gray open_url=https://songoda.com/marketplace/product/husksync-a-modern-cross-server-player-data-synchronization-system.758)"));
|
||||
} else {
|
||||
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date, running version " + updateChecker.getCurrentVersion() + "](#00fb9a)"));
|
||||
}
|
||||
});
|
||||
}
|
||||
case "info", "about" -> displayPluginInformation(player);
|
||||
case "reload" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_RELOAD.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.reload();
|
||||
plugin.getLocales().getLocale("reload_complete").ifPresent(player::sendMessage);
|
||||
}
|
||||
case "migrate" ->
|
||||
plugin.getLocales().getLocale("error_console_command_only").ifPresent(player::sendMessage);
|
||||
default -> plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/husksync <update/about/reload>")
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConsoleExecute(@NotNull String[] args) {
|
||||
if (args.length < 1) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Console usage: \"husksync <update/about/reload/migrate>\"");
|
||||
return;
|
||||
}
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "update", "version" ->
|
||||
new UpdateChecker(plugin.getPluginVersion(), plugin.getLoggingAdapter()).logToConsole();
|
||||
case "info", "about" ->
|
||||
plugin.getLoggingAdapter().log(Level.INFO, new MineDown(plugin.getLocales().stripMineDown(
|
||||
Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getPluginVersion().toString()))));
|
||||
case "reload" -> {
|
||||
plugin.reload();
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Reloaded config & message files.");
|
||||
}
|
||||
case "migrate" -> {
|
||||
if (args.length < 2) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO,
|
||||
"Please choose a migrator, then run \"husksync migrate <migrator>\"");
|
||||
logMigratorsList();
|
||||
return;
|
||||
}
|
||||
final Optional<Migrator> selectedMigrator = plugin.getAvailableMigrators().stream().filter(availableMigrator ->
|
||||
availableMigrator.getIdentifier().equalsIgnoreCase(args[1])).findFirst();
|
||||
selectedMigrator.ifPresentOrElse(migrator -> {
|
||||
if (args.length < 3) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, migrator.getHelpMenu());
|
||||
return;
|
||||
}
|
||||
switch (args[2]) {
|
||||
case "start" -> migrator.start().thenAccept(succeeded -> {
|
||||
if (succeeded) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Migration completed successfully!");
|
||||
} else {
|
||||
plugin.getLoggingAdapter().log(Level.WARNING, "Migration failed!");
|
||||
}
|
||||
});
|
||||
case "set" -> migrator.handleConfigurationCommand(Arrays.copyOfRange(args, 3, args.length));
|
||||
default -> plugin.getLoggingAdapter().log(Level.INFO,
|
||||
"Invalid syntax. Console usage: \"husksync migrate " + args[1] + " <start/set>");
|
||||
}
|
||||
}, () -> {
|
||||
plugin.getLoggingAdapter().log(Level.INFO,
|
||||
"Please specify a valid migrator.\n" +
|
||||
"If a migrator is not available, please verify that you meet the prerequisites to use it.");
|
||||
logMigratorsList();
|
||||
});
|
||||
}
|
||||
default -> plugin.getLoggingAdapter().log(Level.INFO,
|
||||
"Invalid syntax. Console usage: \"husksync <update/about/reload/migrate>\"");
|
||||
}
|
||||
}
|
||||
|
||||
private void logMigratorsList() {
|
||||
plugin.getLoggingAdapter().log(Level.INFO,
|
||||
"List of available migrators:\nMigrator ID / Migrator Name:\n" +
|
||||
plugin.getAvailableMigrators().stream()
|
||||
.map(migrator -> migrator.getIdentifier() + " - " + migrator.getName())
|
||||
.collect(Collectors.joining("\n")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull String[] args) {
|
||||
if (args.length <= 1) {
|
||||
return Arrays.stream(COMMAND_ARGUMENTS)
|
||||
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
|
||||
.sorted().collect(Collectors.toList());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private void displayPluginInformation(@NotNull OnlineUser player) {
|
||||
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_INFO.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
player.sendMessage(new MineDown(Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getPluginVersion().toString())));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.editor.ItemEditorMenu;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class InventoryCommand extends CommandBase implements TabCompletable {
|
||||
|
||||
public InventoryCommand(@NotNull HuskSync implementor) {
|
||||
super("inventory", Permission.COMMAND_INVENTORY, implementor, "invsee", "openinv");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
|
||||
if (args.length == 0 || args.length > 2) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax", "/inventory <player>")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAccept(optionalUser ->
|
||||
optionalUser.ifPresentOrElse(user -> {
|
||||
if (args.length == 2) {
|
||||
// View user data by specified UUID
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[1]);
|
||||
plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> data.ifPresentOrElse(
|
||||
userData -> showInventoryMenu(player, userData, user, false),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/inventory <player> [version_uuid]").ifPresent(player::sendMessage);
|
||||
}
|
||||
} else {
|
||||
// View latest user data
|
||||
plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse(
|
||||
versionedUserData -> showInventoryMenu(player, versionedUserData, user, true),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(player::sendMessage)));
|
||||
}
|
||||
}, () -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage)));
|
||||
}
|
||||
|
||||
private void showInventoryMenu(@NotNull OnlineUser player, @NotNull UserDataSnapshot userDataSnapshot,
|
||||
@NotNull User dataOwner, boolean allowEdit) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
final UserData data = userDataSnapshot.userData();
|
||||
final ItemEditorMenu menu = ItemEditorMenu.createInventoryMenu(data.getInventoryData(),
|
||||
dataOwner, player, plugin.getLocales(), allowEdit);
|
||||
plugin.getLocales().getLocale("viewing_inventory_of", dataOwner.username,
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
||||
.format(userDataSnapshot.versionTimestamp()))
|
||||
.ifPresent(player::sendMessage);
|
||||
plugin.getDataEditor().openItemEditorMenu(player, menu).thenAccept(inventoryDataOnClose -> {
|
||||
if (!menu.canEdit) {
|
||||
return;
|
||||
}
|
||||
final UserData updatedUserData = new UserData(data.getStatusData(), inventoryDataOnClose,
|
||||
data.getEnderChestData(), data.getPotionEffectsData(), data.getAdvancementData(),
|
||||
data.getStatisticsData(), data.getLocationData(),
|
||||
data.getPersistentDataContainerData(),
|
||||
plugin.getMinecraftVersion().toString());
|
||||
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND).join();
|
||||
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull String[] args) {
|
||||
return plugin.getOnlineUsers().stream().map(user -> user.username)
|
||||
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
|
||||
.sorted().collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Static plugin permission nodes required to execute commands
|
||||
*/
|
||||
public enum Permission {
|
||||
|
||||
/*
|
||||
* /husksync command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user use the {@code /husksync} command (subcommand permissions required)
|
||||
*/
|
||||
COMMAND_HUSKSYNC("husksync.command.husksync", DefaultAccess.EVERYONE),
|
||||
/**
|
||||
* Lets the user view plugin info {@code /husksync info}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_INFO("husksync.command.husksync.info", DefaultAccess.EVERYONE),
|
||||
/**
|
||||
* Lets the user reload the plugin {@code /husksync reload}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_RELOAD("husksync.command.husksync.reload", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user view the plugin version and check for updates {@code /husksync update}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_UPDATE("husksync.command.husksync.update", DefaultAccess.OPERATORS),
|
||||
|
||||
/*
|
||||
* /userdata command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user view user data {@code /userdata view/list (player) (version_uuid)}
|
||||
*/
|
||||
COMMAND_USER_DATA("husksync.command.userdata", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user restore and delete user data {@code /userdata restore/delete (player) (version_uuid)}
|
||||
*/
|
||||
COMMAND_USER_DATA_MANAGE("husksync.command.userdata.manage", DefaultAccess.OPERATORS),
|
||||
|
||||
/*
|
||||
* /inventory command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user use the {@code /inventory (player)} command and view offline players' inventories
|
||||
*/
|
||||
COMMAND_INVENTORY("husksync.command.inventory", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user edit the contents of offline players' inventories
|
||||
*/
|
||||
COMMAND_INVENTORY_EDIT("husksync.command.inventory.edit", DefaultAccess.OPERATORS),
|
||||
|
||||
/*
|
||||
* /enderchest command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user use the {@code /enderchest (player)} command and view offline players' ender chests
|
||||
*/
|
||||
COMMAND_ENDER_CHEST("husksync.command.enderchest", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user edit the contents of offline players' ender chests
|
||||
*/
|
||||
COMMAND_ENDER_CHEST_EDIT("husksync.command.enderchest.edit", DefaultAccess.OPERATORS);
|
||||
|
||||
|
||||
public final String node;
|
||||
public final DefaultAccess defaultAccess;
|
||||
|
||||
Permission(@NotNull String node, @NotNull DefaultAccess defaultAccess) {
|
||||
this.node = node;
|
||||
this.defaultAccess = defaultAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies who gets what permissions by default
|
||||
*/
|
||||
public enum DefaultAccess {
|
||||
/**
|
||||
* Everyone gets this permission node by default
|
||||
*/
|
||||
EVERYONE,
|
||||
/**
|
||||
* Nobody gets this permission node by default
|
||||
*/
|
||||
NOBODY,
|
||||
/**
|
||||
* Server operators ({@code /op}) get this permission node by default
|
||||
*/
|
||||
OPERATORS
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface providing tab completions for a command
|
||||
*/
|
||||
public interface TabCompletable {
|
||||
|
||||
/**
|
||||
* What should be returned when the player or console attempts to TAB-complete a command
|
||||
*
|
||||
* @param args Current command arguments
|
||||
* @return List of String arguments to offer TAB suggestions
|
||||
*/
|
||||
List<String> onTabComplete(@NotNull String[] args);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class UserDataCommand extends CommandBase implements TabCompletable {
|
||||
|
||||
private final String[] COMMAND_ARGUMENTS = {"view", "list", "delete", "restore", "pin"};
|
||||
|
||||
public UserDataCommand(@NotNull HuskSync implementor) {
|
||||
super("userdata", Permission.COMMAND_USER_DATA, implementor, "playerdata");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
|
||||
if (args.length < 1) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata <view/list/delete/restore/pin> <username> [version_uuid]")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "view" -> {
|
||||
if (args.length < 2) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata view <username> [version_uuid]")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
final String username = args[1];
|
||||
if (args.length >= 3) {
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data ->
|
||||
data.ifPresentOrElse(userData -> plugin.getDataEditor()
|
||||
.displayDataOverview(player, userData, user),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage))),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata view <username> [version_uuid]")
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
} else {
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getCurrentUserData(user).thenAccept(
|
||||
latestData -> latestData.ifPresentOrElse(
|
||||
userData -> plugin.getDataEditor()
|
||||
.displayDataOverview(player, userData, user),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(player::sendMessage))),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
}
|
||||
}
|
||||
case "list" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
if (args.length < 2) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata list <username>")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
final String username = args[1];
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getUserData(user).thenAccept(dataList -> {
|
||||
if (dataList.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getDataEditor().displayDataList(player, dataList, user);
|
||||
}),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
}
|
||||
case "delete" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
// Delete user data by specified UUID
|
||||
if (args.length < 3) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata delete <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
final String username = args[1];
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().deleteUserData(user, versionUuid).thenAccept(deleted -> {
|
||||
if (deleted) {
|
||||
plugin.getLocales().getLocale("data_deleted",
|
||||
versionUuid.toString().split("-")[0],
|
||||
versionUuid.toString(),
|
||||
user.username,
|
||||
user.uuid.toString())
|
||||
.ifPresent(player::sendMessage);
|
||||
} else {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
}),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata delete <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
}
|
||||
case "restore" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
// Get user data by specified uuid and username
|
||||
if (args.length < 3) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata restore <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
final String username = args[1];
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> {
|
||||
if (data.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getDatabase().setUserData(user, data.get().userData(),
|
||||
DataSaveCause.BACKUP_RESTORE);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, data.get().userData()).join();
|
||||
plugin.getLocales().getLocale("data_restored",
|
||||
user.username,
|
||||
user.uuid.toString(),
|
||||
versionUuid.toString().split("-")[0],
|
||||
versionUuid.toString())
|
||||
.ifPresent(player::sendMessage);
|
||||
}),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata restore <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
}
|
||||
case "pin" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
if (args.length < 3) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata pin <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
final String username = args[1];
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(
|
||||
optionalUserData -> optionalUserData.ifPresentOrElse(userData -> {
|
||||
if (userData.pinned()) {
|
||||
plugin.getDatabase().unpinUserData(user, versionUuid).join();
|
||||
plugin.getLocales().getLocale("data_unpinned",
|
||||
versionUuid.toString().split("-")[0],
|
||||
versionUuid.toString(),
|
||||
user.username,
|
||||
user.uuid.toString())
|
||||
.ifPresent(player::sendMessage);
|
||||
} else {
|
||||
plugin.getDatabase().pinUserData(user, versionUuid).join();
|
||||
plugin.getLocales().getLocale("data_pinned",
|
||||
versionUuid.toString().split("-")[0],
|
||||
versionUuid.toString(),
|
||||
user.username,
|
||||
user.uuid.toString())
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
}, () -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage))),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata pin <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull String[] args) {
|
||||
switch (args.length) {
|
||||
case 0, 1 -> {
|
||||
return Arrays.stream(COMMAND_ARGUMENTS)
|
||||
.filter(argument -> argument.startsWith(args.length == 1 ? args[0] : ""))
|
||||
.sorted().collect(Collectors.toList());
|
||||
}
|
||||
case 2 -> {
|
||||
return plugin.getOnlineUsers().stream().map(user -> user.username)
|
||||
.filter(argument -> argument.startsWith(args[1]))
|
||||
.sorted().collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
139
common/src/main/java/net/william278/husksync/config/Locales.java
Normal file
139
common/src/main/java/net/william278/husksync/config/Locales.java
Normal file
@@ -0,0 +1,139 @@
|
||||
package net.william278.husksync.config;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Loaded locales used by the plugin to display various locales
|
||||
*/
|
||||
public class Locales {
|
||||
|
||||
public static final String PLUGIN_INFORMATION = """
|
||||
[HuskSync](#00fb9a bold) [| Version %version%](#00fb9a)
|
||||
[A modern, cross-server player data synchronization system](gray)
|
||||
[• Author:](white) [William278](gray show_text=&7Click to visit website open_url=https://william278.net)
|
||||
[• Contributors:](white) [HarvelsX](gray show_text=&7Code), [HookWoods](gray show_text=&7Code)
|
||||
[• Translators:](white) [Namiu](gray show_text=&7\\(うにたろう\\) - Japanese, ja-jp), [anchelthe](gray show_text=&7Spanish, es-es), [Melonzio](gray show_text=&7Spanish, es-es), [Ceddix](gray show_text=&7German, de-de), [Pukejoy_1](gray show_text=&7Bulgarian, bg-bg), [mateusneresrb](gray show_text=&7Brazilian Portuguese, pt-br], [小蔡](gray show_text=&7Traditional Chinese, zh-tw), [Ghost-chu](gray show_text=&7Simplified Chinese, zh-cn), [DJelly4K](gray show_text=&7Simplified Chinese, zh-cn), [Thourgard](gray show_text=&7Ukrainian, uk-ua), [xF3d3](gray show_text=&7Italian, it-it)
|
||||
[• Documentation:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://william278.net/docs/husksync/Home/)
|
||||
[• Bug reporting:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/issues)
|
||||
[• Discord support:](white) [[Link]](#00fb9a show_text=&7Click to join open_url=https://discord.gg/tVYhJfyDWG)""";
|
||||
|
||||
@NotNull
|
||||
private final HashMap<String, String> rawLocales;
|
||||
|
||||
private Locales(@NotNull YamlDocument localesConfig) {
|
||||
this.rawLocales = new HashMap<>();
|
||||
for (String localeId : localesConfig.getRoutesAsStrings(false)) {
|
||||
rawLocales.put(localeId, localesConfig.getString(localeId));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an un-formatted locale loaded from the locales file
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @return An {@link Optional} containing the locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<String> getRawLocale(@NotNull String localeId) {
|
||||
if (rawLocales.containsKey(localeId)) {
|
||||
return Optional.of(rawLocales.get(localeId).replaceAll(Pattern.quote("\\n"), "\n"));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an un-formatted locale loaded from the locales file, with replacements applied
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @param replacements Ordered array of replacement strings to fill in placeholders with
|
||||
* @return An {@link Optional} containing the replacement-applied locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<String> getRawLocale(@NotNull String localeId, @NotNull String... replacements) {
|
||||
return getRawLocale(localeId).map(locale -> applyReplacements(locale, replacements));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a MineDown-formatted locale from the locales file
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @return An {@link Optional} containing the formatted locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<MineDown> getLocale(@NotNull String localeId) {
|
||||
return getRawLocale(localeId).map(MineDown::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a MineDown-formatted locale from the locales file, with replacements applied
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @param replacements Ordered array of replacement strings to fill in placeholders with
|
||||
* @return An {@link Optional} containing the replacement-applied, formatted locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<MineDown> getLocale(@NotNull String localeId, @NotNull String... replacements) {
|
||||
return getRawLocale(localeId, replacements).map(MineDown::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply placeholder replacements to a raw locale
|
||||
*
|
||||
* @param rawLocale The raw, unparsed locale
|
||||
* @param replacements Ordered array of replacement strings to fill in placeholders with
|
||||
* @return the raw locale, with inserted placeholders
|
||||
*/
|
||||
private String applyReplacements(@NotNull String rawLocale, @NotNull String... replacements) {
|
||||
int replacementIndexer = 1;
|
||||
for (String replacement : replacements) {
|
||||
String replacementString = "%" + replacementIndexer + "%";
|
||||
rawLocale = rawLocale.replace(replacementString, replacement);
|
||||
replacementIndexer = replacementIndexer + 1;
|
||||
}
|
||||
return rawLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the locales from a BoostedYaml {@link YamlDocument} locales file
|
||||
*
|
||||
* @param localesConfig The loaded {@link YamlDocument} locales.yml file
|
||||
* @return the loaded {@link Locales}
|
||||
*/
|
||||
public static Locales load(@NotNull YamlDocument localesConfig) {
|
||||
return new Locales(localesConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips a string of basic MineDown formatting, used for displaying plugin info to console
|
||||
*
|
||||
* @param string The string to strip
|
||||
* @return The MineDown-stripped string
|
||||
*/
|
||||
public String stripMineDown(@NotNull String string) {
|
||||
final String[] in = string.split("\n");
|
||||
final StringBuilder out = new StringBuilder();
|
||||
String regex = "[^\\[\\]() ]*\\[([^()]+)]\\([^()]+open_url=(\\S+).*\\)";
|
||||
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher m = pattern.matcher(in[i]);
|
||||
|
||||
if (m.find()) {
|
||||
out.append(in[i].replace(m.group(0), ""));
|
||||
out.append(m.group(2));
|
||||
} else {
|
||||
out.append(in[i]);
|
||||
}
|
||||
|
||||
if (i + 1 != in.length) {
|
||||
out.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package net.william278.husksync.config;
|
||||
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Settings used for the plugin, as read from the config file
|
||||
*/
|
||||
public class Settings {
|
||||
|
||||
/**
|
||||
* Map of {@link ConfigOption}s read from the config file
|
||||
*/
|
||||
private final Map<ConfigOption, Object> configOptions;
|
||||
|
||||
// Load the settings from the document
|
||||
private Settings(@NotNull YamlDocument config) {
|
||||
this.configOptions = new HashMap<>();
|
||||
Arrays.stream(ConfigOption.values()).forEach(configOption -> configOptions
|
||||
.put(configOption, switch (configOption.optionType) {
|
||||
case BOOLEAN -> configOption.getBooleanValue(config);
|
||||
case STRING -> configOption.getStringValue(config);
|
||||
case DOUBLE -> configOption.getDoubleValue(config);
|
||||
case FLOAT -> configOption.getFloatValue(config);
|
||||
case INTEGER -> configOption.getIntValue(config);
|
||||
case STRING_LIST -> configOption.getStringListValue(config);
|
||||
}));
|
||||
}
|
||||
|
||||
// Default constructor for empty settings
|
||||
protected Settings(@NotNull Map<ConfigOption, Object> configOptions) {
|
||||
this.configOptions = configOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as a boolean
|
||||
* @throws ClassCastException if the option is not a boolean
|
||||
*/
|
||||
public boolean getBooleanValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (Boolean) configOptions.get(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as a string
|
||||
* @throws ClassCastException if the option is not a string
|
||||
*/
|
||||
public String getStringValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (String) configOptions.get(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as a double
|
||||
* @throws ClassCastException if the option is not a double
|
||||
*/
|
||||
public double getDoubleValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (Double) configOptions.get(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as a float
|
||||
* @throws ClassCastException if the option is not a float
|
||||
*/
|
||||
public double getFloatValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (Float) configOptions.get(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as an integer
|
||||
* @throws ClassCastException if the option is not an integer
|
||||
*/
|
||||
public int getIntegerValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (Integer) configOptions.get(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as a string {@link List}
|
||||
* @throws ClassCastException if the option is not a string list
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<String> getStringListValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (List<String>) configOptions.get(option);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load the settings from a BoostedYaml {@link YamlDocument} config file
|
||||
*
|
||||
* @param config The loaded {@link YamlDocument} config.yml file
|
||||
* @return the loaded {@link Settings}
|
||||
*/
|
||||
public static Settings load(@NotNull YamlDocument config) {
|
||||
return new Settings(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an option stored by a path in config.yml
|
||||
*/
|
||||
public enum ConfigOption {
|
||||
LANGUAGE("language", OptionType.STRING, "en-gb"),
|
||||
CHECK_FOR_UPDATES("check_for_updates", OptionType.BOOLEAN, true),
|
||||
|
||||
CLUSTER_ID("cluster_id", OptionType.STRING, ""),
|
||||
DEBUG_LOGGING("debug_logging", OptionType.BOOLEAN, false),
|
||||
|
||||
DATABASE_HOST("database.credentials.host", OptionType.STRING, "localhost"),
|
||||
DATABASE_PORT("database.credentials.port", OptionType.INTEGER, 3306),
|
||||
DATABASE_NAME("database.credentials.database", OptionType.STRING, "HuskSync"),
|
||||
DATABASE_USERNAME("database.credentials.username", OptionType.STRING, "root"),
|
||||
DATABASE_PASSWORD("database.credentials.password", OptionType.STRING, "pa55w0rd"),
|
||||
DATABASE_CONNECTION_PARAMS("database.credentials.params", OptionType.STRING, "?autoReconnect=true&useSSL=false"),
|
||||
DATABASE_CONNECTION_POOL_MAX_SIZE("database.connection_pool.maximum_pool_size", OptionType.INTEGER, 10),
|
||||
DATABASE_CONNECTION_POOL_MIN_IDLE("database.connection_pool.minimum_idle", OptionType.INTEGER, 10),
|
||||
DATABASE_CONNECTION_POOL_MAX_LIFETIME("database.connection_pool.maximum_lifetime", OptionType.INTEGER, 1800000),
|
||||
DATABASE_CONNECTION_POOL_KEEPALIVE("database.connection_pool.keepalive_time", OptionType.INTEGER, 0),
|
||||
DATABASE_CONNECTION_POOL_TIMEOUT("database.connection_pool.connection_timeout", OptionType.INTEGER, 5000),
|
||||
DATABASE_USERS_TABLE_NAME("database.table_names.users_table", OptionType.STRING, "husksync_users"),
|
||||
DATABASE_USER_DATA_TABLE_NAME("database.table_names.user_data_table", OptionType.STRING, "husksync_user_data"),
|
||||
|
||||
REDIS_HOST("redis.credentials.host", OptionType.STRING, "localhost"),
|
||||
REDIS_PORT("redis.credentials.port", OptionType.INTEGER, 6379),
|
||||
REDIS_PASSWORD("redis.credentials.password", OptionType.STRING, ""),
|
||||
REDIS_USE_SSL("redis.use_ssl", OptionType.BOOLEAN, false),
|
||||
|
||||
SYNCHRONIZATION_MAX_USER_DATA_SNAPSHOTS("synchronization.max_user_data_snapshots", OptionType.INTEGER, 5),
|
||||
SYNCHRONIZATION_SAVE_ON_WORLD_SAVE("synchronization.save_on_world_save", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_COMPRESS_DATA("synchronization.compress_data", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_NETWORK_LATENCY_MILLISECONDS("synchronization.network_latency_milliseconds", OptionType.INTEGER, 500),
|
||||
SYNCHRONIZATION_SAVE_DEAD_PLAYER_INVENTORIES("synchronization.save_dead_player_inventories", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_INVENTORIES("synchronization.features.inventories", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_ENDER_CHESTS("synchronization.features.ender_chests", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_HEALTH("synchronization.features.health", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_MAX_HEALTH("synchronization.features.max_health", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_HUNGER("synchronization.features.hunger", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_EXPERIENCE("synchronization.features.experience", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_POTION_EFFECTS("synchronization.features.potion_effects", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_ADVANCEMENTS("synchronization.features.advancements", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_GAME_MODE("synchronization.features.game_mode", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_STATISTICS("synchronization.features.statistics", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER("synchronization.features.persistent_data_container", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_LOCATION("synchronization.features.location", OptionType.BOOLEAN, true);
|
||||
|
||||
/**
|
||||
* The path in the config.yml file to the value
|
||||
*/
|
||||
@NotNull
|
||||
public final String configPath;
|
||||
|
||||
/**
|
||||
* The {@link OptionType} of this option
|
||||
*/
|
||||
@NotNull
|
||||
public final OptionType optionType;
|
||||
|
||||
/**
|
||||
* The default value of this option if not set in config
|
||||
*/
|
||||
@Nullable
|
||||
private final Object defaultValue;
|
||||
|
||||
ConfigOption(@NotNull String configPath, @NotNull OptionType optionType, @Nullable Object defaultValue) {
|
||||
this.configPath = configPath;
|
||||
this.optionType = optionType;
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
ConfigOption(@NotNull String configPath, @NotNull OptionType optionType) {
|
||||
this.configPath = configPath;
|
||||
this.optionType = optionType;
|
||||
this.defaultValue = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a string
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a string
|
||||
*/
|
||||
public String getStringValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getString(configPath, (String) defaultValue)
|
||||
: config.getString(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a boolean
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a boolean
|
||||
*/
|
||||
public boolean getBooleanValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getBoolean(configPath, (Boolean) defaultValue)
|
||||
: config.getBoolean(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a double
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a double
|
||||
*/
|
||||
public double getDoubleValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getDouble(configPath, (Double) defaultValue)
|
||||
: config.getDouble(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a float
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a float
|
||||
*/
|
||||
public float getFloatValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getFloat(configPath, (Float) defaultValue)
|
||||
: config.getFloat(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as an int
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as an int
|
||||
*/
|
||||
public int getIntValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getInt(configPath, (Integer) defaultValue)
|
||||
: config.getInt(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a string {@link List}
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a string {@link List}
|
||||
*/
|
||||
public List<String> getStringListValue(@NotNull YamlDocument config) {
|
||||
return config.getStringList(configPath, new ArrayList<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the type of the object
|
||||
*/
|
||||
public enum OptionType {
|
||||
BOOLEAN,
|
||||
STRING,
|
||||
DOUBLE,
|
||||
FLOAT,
|
||||
INTEGER,
|
||||
STRING_LIST
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A mapped piece of advancement data
|
||||
*/
|
||||
public class AdvancementData {
|
||||
|
||||
/**
|
||||
* The advancement namespaced key
|
||||
*/
|
||||
@SerializedName("key")
|
||||
public String key;
|
||||
|
||||
/**
|
||||
* A map of completed advancement criteria to when it was completed
|
||||
*/
|
||||
@SerializedName("completed_criteria")
|
||||
public Map<String, Date> completedCriteria;
|
||||
|
||||
public AdvancementData(@NotNull String key, @NotNull Map<String, Date> awardedCriteria) {
|
||||
this.key = key;
|
||||
this.completedCriteria = awardedCriteria;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected AdvancementData() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Represents the type of a {@link PersistentDataTag}
|
||||
*/
|
||||
public enum BukkitPersistentDataTagType {
|
||||
|
||||
BYTE,
|
||||
SHORT,
|
||||
INTEGER,
|
||||
LONG,
|
||||
FLOAT,
|
||||
DOUBLE,
|
||||
STRING,
|
||||
BYTE_ARRAY,
|
||||
INTEGER_ARRAY,
|
||||
LONG_ARRAY,
|
||||
TAG_CONTAINER_ARRAY,
|
||||
TAG_CONTAINER;
|
||||
|
||||
|
||||
public static Optional<BukkitPersistentDataTagType> getDataType(@NotNull String typeName) {
|
||||
for (BukkitPersistentDataTagType type : values()) {
|
||||
if (type.name().equalsIgnoreCase(typeName)) {
|
||||
return Optional.of(type);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.xerial.snappy.Snappy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CompressedDataAdapter extends JsonDataAdapter {
|
||||
|
||||
@Override
|
||||
public byte[] toBytes(@NotNull UserData data) throws DataAdaptionException {
|
||||
try {
|
||||
return Snappy.compress(super.toBytes(data));
|
||||
} catch (IOException e) {
|
||||
throw new DataAdaptionException("Failed to compress data", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull UserData fromBytes(byte[] data) throws DataAdaptionException {
|
||||
try {
|
||||
return super.fromBytes(Snappy.uncompress(data));
|
||||
} catch (IOException e) {
|
||||
throw new DataAdaptionException("Failed to decompress data", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* An adapter that adapts {@link UserData} to and from a portable byte array.
|
||||
*/
|
||||
public interface DataAdapter {
|
||||
|
||||
/**
|
||||
* Converts {@link UserData} to a byte array
|
||||
*
|
||||
* @param data The {@link UserData} to adapt
|
||||
* @return The byte array.
|
||||
* @throws DataAdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
byte[] toBytes(@NotNull UserData data) throws DataAdaptionException;
|
||||
|
||||
/**
|
||||
* Serializes {@link UserData} to a JSON string.
|
||||
*
|
||||
* @param data The {@link UserData} to serialize
|
||||
* @param pretty Whether to pretty print the JSON.
|
||||
* @return The output json string.
|
||||
* @throws DataAdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
@NotNull
|
||||
String toJson(@NotNull UserData data, boolean pretty) throws DataAdaptionException;
|
||||
|
||||
/**
|
||||
* Converts a byte array to {@link UserData}.
|
||||
*
|
||||
* @param data The byte array to adapt.
|
||||
* @return The {@link UserData}.
|
||||
* @throws DataAdaptionException If an error occurred during adaptation, such as if the byte array is invalid.
|
||||
*/
|
||||
@NotNull
|
||||
UserData fromBytes(final byte[] data) throws DataAdaptionException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
/**
|
||||
* Indicates an error occurred during {@link UserData} adaptation to and from (compressed) json.
|
||||
*/
|
||||
public class DataAdaptionException extends RuntimeException {
|
||||
protected DataAdaptionException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.api.BaseHuskSyncAPI;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Identifies the cause of a player data save.
|
||||
*
|
||||
* @implNote This enum is saved in the database.
|
||||
* </p>
|
||||
* Cause names have a max length of 32 characters.
|
||||
*/
|
||||
public enum DataSaveCause {
|
||||
|
||||
/**
|
||||
* Indicates data saved when a player disconnected from the server (either to change servers, or to log off)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
DISCONNECT,
|
||||
/**
|
||||
* Indicates data saved when the world saved
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
WORLD_SAVE,
|
||||
/**
|
||||
* Indicates data saved when the server shut down
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
SERVER_SHUTDOWN,
|
||||
/**
|
||||
* Indicates data was saved by editing inventory contents via the {@code /inventory} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
INVENTORY_COMMAND,
|
||||
/**
|
||||
* Indicates data was saved by editing Ender Chest contents via the {@code /enderchest} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
ENDERCHEST_COMMAND,
|
||||
/**
|
||||
* Indicates data was saved by restoring it from a previous version
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
BACKUP_RESTORE,
|
||||
/**
|
||||
* Indicates data was saved by an API call
|
||||
*
|
||||
* @see BaseHuskSyncAPI#saveUserData(OnlineUser)
|
||||
* @see BaseHuskSyncAPI#setUserData(User, UserData)
|
||||
* @since 2.0
|
||||
*/
|
||||
API,
|
||||
/**
|
||||
* Indicates data was saved from being imported from MySQLPlayerDataBridge
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
MPDB_MIGRATION,
|
||||
/**
|
||||
* Indicates data was saved from being imported from a legacy version (v1.x)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
LEGACY_MIGRATION,
|
||||
/**
|
||||
* Indicates data was saved by an unknown cause.
|
||||
* </p>
|
||||
* This should not be used and is only used for error handling purposes.
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
UNKNOWN;
|
||||
|
||||
/**
|
||||
* Returns a {@link DataSaveCause} by name.
|
||||
*
|
||||
* @return the {@link DataSaveCause} or {@link #UNKNOWN} if the name is not valid.
|
||||
*/
|
||||
@NotNull
|
||||
public static DataSaveCause getCauseByName(@NotNull String name) {
|
||||
for (DataSaveCause cause : values()) {
|
||||
if (cause.name().equalsIgnoreCase(name)) {
|
||||
return cause;
|
||||
}
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Indicates an error occurred during Base-64 serialization and deserialization of data.
|
||||
* </p>
|
||||
* For example, an exception deserializing {@link ItemData} item stack or {@link PotionEffectData} potion effect arrays
|
||||
*/
|
||||
public class DataSerializationException extends RuntimeException {
|
||||
protected DataSerializationException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Stores information about the contents of a player's inventory or Ender Chest.
|
||||
*/
|
||||
public class ItemData {
|
||||
|
||||
/**
|
||||
* A Base-64 string of platform-serialized items
|
||||
*/
|
||||
@SerializedName("serialized_items")
|
||||
public String serializedItems;
|
||||
|
||||
public ItemData(@NotNull final String serializedItems) {
|
||||
this.serializedItems = serializedItems;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected ItemData() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class JsonDataAdapter implements DataAdapter {
|
||||
|
||||
@Override
|
||||
public byte[] toBytes(@NotNull UserData data) throws DataAdaptionException {
|
||||
return toJson(data, false).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toJson(@NotNull UserData data, boolean pretty) throws DataAdaptionException {
|
||||
return (pretty ? new GsonBuilder().setPrettyPrinting() : new GsonBuilder()).create().toJson(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull UserData fromBytes(byte[] data) throws DataAdaptionException {
|
||||
try {
|
||||
return new GsonBuilder().create().fromJson(new String(data, StandardCharsets.UTF_8), UserData.class);
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new DataAdaptionException("Failed to parse JSON data", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Stores information about a player's location
|
||||
*/
|
||||
public class LocationData {
|
||||
|
||||
/**
|
||||
* Name of the world on the server
|
||||
*/
|
||||
@SerializedName("world_name")
|
||||
public String worldName;
|
||||
/**
|
||||
* Unique id of the world
|
||||
*/
|
||||
@SerializedName("world_uuid")
|
||||
public UUID worldUuid;
|
||||
/**
|
||||
* The environment type of the world (one of "NORMAL", "NETHER", "THE_END")
|
||||
*/
|
||||
@SerializedName("world_environment")
|
||||
public String worldEnvironment;
|
||||
|
||||
/**
|
||||
* The x coordinate of the location
|
||||
*/
|
||||
@SerializedName("x")
|
||||
public double x;
|
||||
/**
|
||||
* The y coordinate of the location
|
||||
*/
|
||||
@SerializedName("y")
|
||||
public double y;
|
||||
/**
|
||||
* The z coordinate of the location
|
||||
*/
|
||||
@SerializedName("z")
|
||||
public double z;
|
||||
|
||||
/**
|
||||
* The location's facing yaw angle
|
||||
*/
|
||||
@SerializedName("yaw")
|
||||
public float yaw;
|
||||
/**
|
||||
* The location's facing pitch angle
|
||||
*/
|
||||
@SerializedName("pitch")
|
||||
public float pitch;
|
||||
|
||||
public LocationData(@NotNull String worldName, @NotNull UUID worldUuid,
|
||||
@NotNull String worldEnvironment,
|
||||
double x, double y, double z,
|
||||
float yaw, float pitch) {
|
||||
this.worldName = worldName;
|
||||
this.worldUuid = worldUuid;
|
||||
this.worldEnvironment = worldEnvironment;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.yaw = yaw;
|
||||
this.pitch = pitch;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected LocationData() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Store's a user's persistent data container, holding a map of plugin-set persistent values
|
||||
*/
|
||||
public class PersistentDataContainerData {
|
||||
|
||||
/**
|
||||
* Map of namespaced key strings to a byte array representing the persistent data
|
||||
*/
|
||||
@SerializedName("persistent_data_map")
|
||||
protected Map<String, PersistentDataTag<?>> persistentDataMap;
|
||||
|
||||
public PersistentDataContainerData(@NotNull final Map<String, PersistentDataTag<?>> persistentDataMap) {
|
||||
this.persistentDataMap = persistentDataMap;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected PersistentDataContainerData() {
|
||||
}
|
||||
|
||||
|
||||
public <T> Optional<T> getTagValue(@NotNull final String tagName, @NotNull Class<T> tagClass) {
|
||||
if (persistentDataMap.containsKey(tagName)) {
|
||||
return Optional.of(tagClass.cast(persistentDataMap.get(tagName).value));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Optional<BukkitPersistentDataTagType> getTagType(@NotNull final String tagType) {
|
||||
if (persistentDataMap.containsKey(tagType)) {
|
||||
return BukkitPersistentDataTagType.getDataType(persistentDataMap.get(tagType).type);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Set<String> getTags() {
|
||||
return persistentDataMap.keySet();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Represents a persistent data tag set by a plugin.
|
||||
*/
|
||||
public class PersistentDataTag<T> {
|
||||
|
||||
/**
|
||||
* The enumerated primitive data type name value of the tag
|
||||
*/
|
||||
protected String type;
|
||||
|
||||
/**
|
||||
* The value of the tag
|
||||
*/
|
||||
public T value;
|
||||
|
||||
public PersistentDataTag(@NotNull BukkitPersistentDataTagType type, @NotNull T value) {
|
||||
this.type = type.name();
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
private PersistentDataTag() {
|
||||
}
|
||||
|
||||
public Optional<BukkitPersistentDataTagType> getType() {
|
||||
return BukkitPersistentDataTagType.getDataType(type);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Stores potion effect data
|
||||
*/
|
||||
public class PotionEffectData {
|
||||
|
||||
@SerializedName("serialized_potion_effects")
|
||||
public String serializedPotionEffects;
|
||||
|
||||
public PotionEffectData(@NotNull final String serializedPotionEffects) {
|
||||
this.serializedPotionEffects = serializedPotionEffects;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected PotionEffectData() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Stores information about a player's statistics
|
||||
*/
|
||||
public class StatisticsData {
|
||||
|
||||
/**
|
||||
* Map of untyped statistic names to their values
|
||||
*/
|
||||
@SerializedName("untyped_statistics")
|
||||
public Map<String, Integer> untypedStatistics;
|
||||
|
||||
/**
|
||||
* Map of block type statistics to a map of material types to values
|
||||
*/
|
||||
@SerializedName("block_statistics")
|
||||
public Map<String, Map<String, Integer>> blockStatistics;
|
||||
|
||||
/**
|
||||
* Map of item type statistics to a map of material types to values
|
||||
*/
|
||||
@SerializedName("item_statistics")
|
||||
public Map<String, Map<String, Integer>> itemStatistics;
|
||||
|
||||
/**
|
||||
* Map of entity type statistics to a map of entity types to values
|
||||
*/
|
||||
@SerializedName("entity_statistics")
|
||||
public Map<String, Map<String, Integer>> entityStatistics;
|
||||
|
||||
public StatisticsData(@NotNull Map<String, Integer> untypedStatistics,
|
||||
@NotNull Map<String, Map<String, Integer>> blockStatistics,
|
||||
@NotNull Map<String, Map<String, Integer>> itemStatistics,
|
||||
@NotNull Map<String, Map<String, Integer>> entityStatistics) {
|
||||
this.untypedStatistics = untypedStatistics;
|
||||
this.blockStatistics = blockStatistics;
|
||||
this.itemStatistics = itemStatistics;
|
||||
this.entityStatistics = entityStatistics;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected StatisticsData() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Stores status information about a player
|
||||
*/
|
||||
public class StatusData {
|
||||
|
||||
/**
|
||||
* The player's health points
|
||||
*/
|
||||
@SerializedName("health")
|
||||
public double health;
|
||||
|
||||
/**
|
||||
* The player's maximum health points
|
||||
*/
|
||||
@SerializedName("max_health")
|
||||
public double maxHealth;
|
||||
|
||||
/**
|
||||
* The player's health scaling factor
|
||||
*/
|
||||
@SerializedName("health_scale")
|
||||
public double healthScale;
|
||||
|
||||
/**
|
||||
* The player's hunger points
|
||||
*/
|
||||
@SerializedName("hunger")
|
||||
public int hunger;
|
||||
|
||||
/**
|
||||
* The player's saturation points
|
||||
*/
|
||||
@SerializedName("saturation")
|
||||
public float saturation;
|
||||
|
||||
/**
|
||||
* The player's saturation exhaustion points
|
||||
*/
|
||||
@SerializedName("saturation_exhaustion")
|
||||
public float saturationExhaustion;
|
||||
|
||||
/**
|
||||
* The player's currently selected item slot
|
||||
*/
|
||||
@SerializedName("selected_item_slot")
|
||||
public int selectedItemSlot;
|
||||
|
||||
/**
|
||||
* The player's total experience points<p>
|
||||
* (not to be confused with <i>experience level</i> - this is the "points" value shown on the death screen)
|
||||
*/
|
||||
@SerializedName("total_experience")
|
||||
public int totalExperience;
|
||||
|
||||
/**
|
||||
* The player's experience level (shown on the exp bar)
|
||||
*/
|
||||
@SerializedName("experience_level")
|
||||
public int expLevel;
|
||||
|
||||
/**
|
||||
* The player's progress to their next experience level
|
||||
*/
|
||||
@SerializedName("experience_progress")
|
||||
public float expProgress;
|
||||
|
||||
/**
|
||||
* The player's game mode string (one of "SURVIVAL", "CREATIVE", "ADVENTURE", "SPECTATOR")
|
||||
*/
|
||||
@SerializedName("game_mode")
|
||||
public String gameMode;
|
||||
|
||||
/**
|
||||
* If the player is currently flying
|
||||
*/
|
||||
@SerializedName("is_flying")
|
||||
public boolean isFlying;
|
||||
|
||||
public StatusData(final double health, final double maxHealth, final double healthScale,
|
||||
final int hunger, final float saturation, final float saturationExhaustion,
|
||||
final int selectedItemSlot, final int totalExperience, final int expLevel,
|
||||
final float expProgress, final String gameMode, final boolean isFlying) {
|
||||
this.health = health;
|
||||
this.maxHealth = maxHealth;
|
||||
this.healthScale = healthScale;
|
||||
this.hunger = hunger;
|
||||
this.saturation = saturation;
|
||||
this.saturationExhaustion = saturationExhaustion;
|
||||
this.selectedItemSlot = selectedItemSlot;
|
||||
this.totalExperience = totalExperience;
|
||||
this.expLevel = expLevel;
|
||||
this.expProgress = expProgress;
|
||||
this.gameMode = gameMode;
|
||||
this.isFlying = isFlying;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected StatusData() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.config.Settings;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Flags for setting {@link StatusData}, indicating which elements should be synced
|
||||
*/
|
||||
public enum StatusDataFlag {
|
||||
|
||||
SET_HEALTH(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HEALTH),
|
||||
SET_MAX_HEALTH(Settings.ConfigOption.SYNCHRONIZATION_SYNC_MAX_HEALTH),
|
||||
SET_HUNGER(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HUNGER),
|
||||
SET_EXPERIENCE(Settings.ConfigOption.SYNCHRONIZATION_SYNC_EXPERIENCE),
|
||||
SET_GAME_MODE(Settings.ConfigOption.SYNCHRONIZATION_SYNC_GAME_MODE),
|
||||
SET_FLYING(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION),
|
||||
SET_SELECTED_ITEM_SLOT(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES);
|
||||
|
||||
private final Settings.ConfigOption configOption;
|
||||
|
||||
StatusDataFlag(@NotNull Settings.ConfigOption configOption) {
|
||||
this.configOption = configOption;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all status data flags
|
||||
*
|
||||
* @return all status data flags as a list
|
||||
*/
|
||||
@NotNull
|
||||
@SuppressWarnings("unused")
|
||||
public static List<StatusDataFlag> getAll() {
|
||||
return Arrays.stream(StatusDataFlag.values()).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all status data flags that are enabled for setting as per the {@link Settings}
|
||||
*
|
||||
* @param settings the settings to use for determining which flags are enabled
|
||||
* @return all status data flags that are enabled for setting
|
||||
*/
|
||||
@NotNull
|
||||
public static List<StatusDataFlag> getFromSettings(@NotNull Settings settings) {
|
||||
return Arrays.stream(StatusDataFlag.values()).filter(
|
||||
flag -> settings.getBooleanValue(flag.configOption)).toList();
|
||||
}
|
||||
|
||||
}
|
||||
143
common/src/main/java/net/william278/husksync/data/UserData.java
Normal file
143
common/src/main/java/net/william278/husksync/data/UserData.java
Normal file
@@ -0,0 +1,143 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/***
|
||||
* Stores data about a user
|
||||
*/
|
||||
public class UserData {
|
||||
|
||||
/**
|
||||
* Indicates the version of the {@link UserData} format being used.
|
||||
* </p>
|
||||
* This value is to be incremented whenever the format changes.
|
||||
*/
|
||||
public static final int CURRENT_FORMAT_VERSION = 2;
|
||||
|
||||
/**
|
||||
* Stores the user's status data, including health, food, etc.
|
||||
*/
|
||||
@SerializedName("status")
|
||||
protected StatusData statusData;
|
||||
|
||||
/**
|
||||
* Stores the user's inventory contents
|
||||
*/
|
||||
@SerializedName("inventory")
|
||||
protected ItemData inventoryData;
|
||||
|
||||
/**
|
||||
* Stores the user's ender chest contents
|
||||
*/
|
||||
@SerializedName("ender_chest")
|
||||
protected ItemData enderChestData;
|
||||
|
||||
/**
|
||||
* Store's the user's potion effects
|
||||
*/
|
||||
@SerializedName("potion_effects")
|
||||
protected PotionEffectData potionEffectData;
|
||||
|
||||
/**
|
||||
* Stores the set of this user's advancements
|
||||
*/
|
||||
@SerializedName("advancements")
|
||||
protected List<AdvancementData> advancementData;
|
||||
|
||||
/**
|
||||
* Stores the user's set of statistics
|
||||
*/
|
||||
@SerializedName("statistics")
|
||||
protected StatisticsData statisticData;
|
||||
|
||||
/**
|
||||
* Store's the user's world location and coordinates
|
||||
*/
|
||||
@SerializedName("location")
|
||||
protected LocationData locationData;
|
||||
|
||||
/**
|
||||
* Stores the user's serialized persistent data container, which contains metadata keys applied by other plugins
|
||||
*/
|
||||
@SerializedName("persistent_data_container")
|
||||
protected PersistentDataContainerData persistentDataContainerData;
|
||||
|
||||
/**
|
||||
* Stores the version of Minecraft this data was generated in
|
||||
*/
|
||||
@SerializedName("minecraft_version")
|
||||
protected String minecraftVersion;
|
||||
|
||||
/**
|
||||
* Stores the version of the data format being used
|
||||
*/
|
||||
@SerializedName("format_version")
|
||||
protected int formatVersion;
|
||||
|
||||
public UserData(@NotNull StatusData statusData, @NotNull ItemData inventoryData,
|
||||
@NotNull ItemData enderChestData, @NotNull PotionEffectData potionEffectData,
|
||||
@NotNull List<AdvancementData> advancementData, @NotNull StatisticsData statisticData,
|
||||
@NotNull LocationData locationData, @NotNull PersistentDataContainerData persistentDataContainerData,
|
||||
@NotNull String minecraftVersion) {
|
||||
this.statusData = statusData;
|
||||
this.inventoryData = inventoryData;
|
||||
this.enderChestData = enderChestData;
|
||||
this.potionEffectData = potionEffectData;
|
||||
this.advancementData = advancementData;
|
||||
this.statisticData = statisticData;
|
||||
this.locationData = locationData;
|
||||
this.persistentDataContainerData = persistentDataContainerData;
|
||||
this.minecraftVersion = minecraftVersion;
|
||||
this.formatVersion = CURRENT_FORMAT_VERSION;
|
||||
}
|
||||
|
||||
// Empty constructor to facilitate json serialization
|
||||
@SuppressWarnings("unused")
|
||||
protected UserData() {
|
||||
}
|
||||
|
||||
public StatusData getStatusData() {
|
||||
return statusData;
|
||||
}
|
||||
|
||||
public ItemData getInventoryData() {
|
||||
return inventoryData;
|
||||
}
|
||||
|
||||
public ItemData getEnderChestData() {
|
||||
return enderChestData;
|
||||
}
|
||||
|
||||
public PotionEffectData getPotionEffectsData() {
|
||||
return potionEffectData;
|
||||
}
|
||||
|
||||
public List<AdvancementData> getAdvancementData() {
|
||||
return advancementData;
|
||||
}
|
||||
|
||||
public StatisticsData getStatisticsData() {
|
||||
return statisticData;
|
||||
}
|
||||
|
||||
public LocationData getLocationData() {
|
||||
return locationData;
|
||||
}
|
||||
|
||||
public PersistentDataContainerData getPersistentDataContainerData() {
|
||||
return persistentDataContainerData;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getMinecraftVersion() {
|
||||
return minecraftVersion;
|
||||
}
|
||||
|
||||
public int getFormatVersion() {
|
||||
return formatVersion;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents a uniquely versioned and timestamped snapshot of a user's data, including why it was saved.
|
||||
*
|
||||
* @param versionUUID The unique identifier for this user data version
|
||||
* @param versionTimestamp An epoch milliseconds timestamp of when this data was created
|
||||
* @param userData The {@link UserData} that has been versioned
|
||||
* @param cause The {@link DataSaveCause} that caused this data to be saved
|
||||
*/
|
||||
public record UserDataSnapshot(@NotNull UUID versionUUID, @NotNull Date versionTimestamp,
|
||||
@NotNull DataSaveCause cause, boolean pinned,
|
||||
@NotNull UserData userData) implements Comparable<UserDataSnapshot> {
|
||||
|
||||
/**
|
||||
* Version {@link UserData} into a {@link UserDataSnapshot}, assigning it a random {@link UUID} and the current timestamp {@link Date}
|
||||
* </p>
|
||||
* Note that this method will set {@code cause} to {@link DataSaveCause#API}
|
||||
*
|
||||
* @param userData The {@link UserData} to version
|
||||
* @return A new {@link UserDataSnapshot}
|
||||
* @implNote This isn't used to version data that is going to be set to a database to prevent UUID collisions.<p>
|
||||
* Database implementations should instead use their own UUID generation functions.
|
||||
*/
|
||||
public static UserDataSnapshot create(@NotNull UserData userData) {
|
||||
return new UserDataSnapshot(UUID.randomUUID(), new Date(),
|
||||
DataSaveCause.API, false, userData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare UserData by creation timestamp
|
||||
*
|
||||
* @param other the other UserData to be compared
|
||||
* @return the comparison result; the more recent UserData is greater than the less recent UserData
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(@NotNull UserDataSnapshot other) {
|
||||
return Long.compare(this.versionTimestamp.getTime(), other.versionTimestamp.getTime());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package net.william278.husksync.database;
|
||||
|
||||
import net.william278.husksync.data.DataAdapter;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.event.EventCannon;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.util.Logger;
|
||||
import net.william278.husksync.util.ResourceReader;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* An abstract representation of the plugin database, storing player data.
|
||||
* <p>
|
||||
* Implemented by different database platforms - MySQL, SQLite, etc. - as configured by the administrator.
|
||||
*/
|
||||
public abstract class Database {
|
||||
|
||||
/**
|
||||
* Name of the table that stores player information
|
||||
*/
|
||||
protected final String playerTableName;
|
||||
|
||||
/**
|
||||
* Name of the table that stores data
|
||||
*/
|
||||
protected final String dataTableName;
|
||||
|
||||
/**
|
||||
* The maximum number of user records to store in the database at once per user
|
||||
*/
|
||||
protected final int maxUserDataRecords;
|
||||
|
||||
/**
|
||||
* {@link DataAdapter} implementation used for adapting {@link UserData} to and from JSON
|
||||
*/
|
||||
private final DataAdapter dataAdapter;
|
||||
|
||||
/**
|
||||
* Returns the {@link DataAdapter} used to adapt {@link UserData} to and from JSON
|
||||
*
|
||||
* @return instance of the {@link DataAdapter} implementation
|
||||
*/
|
||||
protected DataAdapter getDataAdapter() {
|
||||
return dataAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link EventCannon} implementation used for firing events
|
||||
*/
|
||||
private final EventCannon eventCannon;
|
||||
|
||||
/**
|
||||
* Returns the {@link EventCannon} used to fire events
|
||||
*
|
||||
* @return instance of the {@link EventCannon} implementation
|
||||
*/
|
||||
protected EventCannon getEventCannon() {
|
||||
return eventCannon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger instance used for database error logging
|
||||
*/
|
||||
private final Logger logger;
|
||||
|
||||
/**
|
||||
* Returns the {@link Logger} used to log database errors
|
||||
*
|
||||
* @return the {@link Logger} instance
|
||||
*/
|
||||
protected Logger getLogger() {
|
||||
return logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link ResourceReader} used to read internal resource files by name
|
||||
*/
|
||||
private final ResourceReader resourceReader;
|
||||
|
||||
protected Database(@NotNull String playerTableName, @NotNull String dataTableName, final int maxUserDataRecords,
|
||||
@NotNull ResourceReader resourceReader, @NotNull DataAdapter dataAdapter,
|
||||
@NotNull EventCannon eventCannon, @NotNull Logger logger) {
|
||||
this.playerTableName = playerTableName;
|
||||
this.dataTableName = dataTableName;
|
||||
this.maxUserDataRecords = maxUserDataRecords;
|
||||
this.resourceReader = resourceReader;
|
||||
this.dataAdapter = dataAdapter;
|
||||
this.eventCannon = eventCannon;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads SQL table creation schema statements from a resource file as a string array
|
||||
*
|
||||
* @param schemaFileName database script resource file to load from
|
||||
* @return Array of string-formatted table creation schema statements
|
||||
* @throws IOException if the resource could not be read
|
||||
*/
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException {
|
||||
return formatStatementTables(new String(Objects.requireNonNull(resourceReader.getResource(schemaFileName))
|
||||
.readAllBytes(), StandardCharsets.UTF_8)).split(";");
|
||||
}
|
||||
|
||||
/**
|
||||
* Format all table name placeholder strings in a SQL statement
|
||||
*
|
||||
* @param sql the SQL statement with un-formatted table name placeholders
|
||||
* @return the formatted statement, with table placeholders replaced with the correct names
|
||||
*/
|
||||
protected final String formatStatementTables(@NotNull String sql) {
|
||||
return sql.replaceAll("%users_table%", playerTableName)
|
||||
.replaceAll("%user_data_table%", dataTableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the database and ensure tables are present; create tables if they do not exist.
|
||||
*
|
||||
* @return A future returning boolean - if the connection could be established.
|
||||
*/
|
||||
public abstract boolean initialize();
|
||||
|
||||
/**
|
||||
* Ensure a {@link User} has an entry in the database and that their username is up-to-date
|
||||
*
|
||||
* @param user The {@link User} to ensure
|
||||
* @return A future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> ensureUser(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Get a player by their Minecraft account {@link UUID}
|
||||
*
|
||||
* @param uuid Minecraft account {@link UUID} of the {@link User} to get
|
||||
* @return A future returning an optional with the {@link User} present if they exist
|
||||
*/
|
||||
public abstract CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid);
|
||||
|
||||
/**
|
||||
* Get a user by their username (<i>case-insensitive</i>)
|
||||
*
|
||||
* @param username Username of the {@link User} to get (<i>case-insensitive</i>)
|
||||
* @return A future returning an optional with the {@link User} present if they exist
|
||||
*/
|
||||
public abstract CompletableFuture<Optional<User>> getUserByName(@NotNull String username);
|
||||
|
||||
/**
|
||||
* Get the current uniquely versioned user data for a given user, if it exists.
|
||||
*
|
||||
* @param user the user to get data for
|
||||
* @return an optional containing the {@link UserDataSnapshot}, if it exists, or an empty optional if it does not
|
||||
*/
|
||||
public abstract CompletableFuture<Optional<UserDataSnapshot>> getCurrentUserData(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Get all {@link UserDataSnapshot} entries for a user from the database.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @return A future returning a list of a user's {@link UserDataSnapshot} entries
|
||||
*/
|
||||
public abstract CompletableFuture<List<UserDataSnapshot>> getUserData(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Gets a specific {@link UserDataSnapshot} entry for a user from the database, by its UUID.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @param versionUuid The UUID of the {@link UserDataSnapshot} entry to get
|
||||
* @return A future returning an optional containing the {@link UserDataSnapshot}, if it exists, or an empty optional if it does not
|
||||
*/
|
||||
public abstract CompletableFuture<Optional<UserDataSnapshot>> getUserData(@NotNull User user, @NotNull UUID versionUuid);
|
||||
|
||||
/**
|
||||
* <b>(Internal)</b> Prune user data for a given user to the maximum value as configured.
|
||||
*
|
||||
* @param user The user to prune data for
|
||||
* @return A future returning void when complete
|
||||
* @implNote Data snapshots marked as {@code pinned} are exempt from rotation
|
||||
*/
|
||||
protected abstract CompletableFuture<Void> rotateUserData(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Deletes a specific {@link UserDataSnapshot} entry for a user from the database, by its UUID.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @param versionUuid The UUID of the {@link UserDataSnapshot} entry to delete
|
||||
* @return A future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Boolean> deleteUserData(@NotNull User user, @NotNull UUID versionUuid);
|
||||
|
||||
/**
|
||||
* Save user data to the database<p>
|
||||
* This will remove the oldest data for the user if the amount of data exceeds the limit as configured
|
||||
*
|
||||
* @param user The user to add data for
|
||||
* @param userData The {@link UserData} to set. The implementation should version it with a random UUID and the current timestamp during insertion.
|
||||
* @return A future returning void when complete
|
||||
* @see UserDataSnapshot#create(UserData)
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData, @NotNull DataSaveCause dataSaveCause);
|
||||
|
||||
/**
|
||||
* Pin a saved {@link UserDataSnapshot} by given version UUID, setting it's {@code pinned} state to {@code true}.
|
||||
*
|
||||
* @param user The user to pin the data for
|
||||
* @param versionUuid The UUID of the user's {@link UserDataSnapshot} entry to pin
|
||||
* @return A future returning a boolean; {@code true} if the operation completed successfully, {@code false} if it failed
|
||||
* @see UserDataSnapshot#pinned()
|
||||
*/
|
||||
public abstract CompletableFuture<Void> pinUserData(@NotNull User user, @NotNull UUID versionUuid);
|
||||
|
||||
/**
|
||||
* Unpin a saved {@link UserDataSnapshot} by given version UUID, setting it's {@code pinned} state to {@code false}.
|
||||
*
|
||||
* @param user The user to unpin the data for
|
||||
* @param versionUuid The UUID of the user's {@link UserDataSnapshot} entry to unpin
|
||||
* @return A future returning a boolean; {@code true} if the operation completed successfully, {@code false} if it failed
|
||||
* @see UserDataSnapshot#pinned()
|
||||
*/
|
||||
public abstract CompletableFuture<Void> unpinUserData(@NotNull User user, @NotNull UUID versionUuid);
|
||||
|
||||
/**
|
||||
* Wipes <b>all</b> {@link UserData} entries from the database.
|
||||
* <b>This should never be used</b>, except when preparing tables for migration.
|
||||
*
|
||||
* @return A future returning void when complete
|
||||
* @see Migrator#start()
|
||||
*/
|
||||
public abstract CompletableFuture<Void> wipeDatabase();
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
public abstract void close();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
package net.william278.husksync.database;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.*;
|
||||
import net.william278.husksync.event.DataSaveEvent;
|
||||
import net.william278.husksync.event.EventCannon;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.util.Logger;
|
||||
import net.william278.husksync.util.ResourceReader;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.sql.*;
|
||||
import java.util.Date;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class MySqlDatabase extends Database {
|
||||
|
||||
/**
|
||||
* MySQL server hostname
|
||||
*/
|
||||
private final String mySqlHost;
|
||||
|
||||
/**
|
||||
* MySQL server port
|
||||
*/
|
||||
private final int mySqlPort;
|
||||
|
||||
/**
|
||||
* Database to use on the MySQL server
|
||||
*/
|
||||
private final String mySqlDatabaseName;
|
||||
private final String mySqlUsername;
|
||||
private final String mySqlPassword;
|
||||
private final String mySqlConnectionParameters;
|
||||
|
||||
private final int hikariMaximumPoolSize;
|
||||
private final int hikariMinimumIdle;
|
||||
private final int hikariMaximumLifetime;
|
||||
private final int hikariKeepAliveTime;
|
||||
private final int hikariConnectionTimeOut;
|
||||
|
||||
private static final String DATA_POOL_NAME = "HuskSyncHikariPool";
|
||||
|
||||
/**
|
||||
* The Hikari data source - a pool of database connections that can be fetched on-demand
|
||||
*/
|
||||
private HikariDataSource connectionPool;
|
||||
|
||||
public MySqlDatabase(@NotNull Settings settings, @NotNull ResourceReader resourceReader, @NotNull Logger logger,
|
||||
@NotNull DataAdapter dataAdapter, @NotNull EventCannon eventCannon) {
|
||||
super(settings.getStringValue(Settings.ConfigOption.DATABASE_USERS_TABLE_NAME),
|
||||
settings.getStringValue(Settings.ConfigOption.DATABASE_USER_DATA_TABLE_NAME),
|
||||
Math.max(1, Math.min(20, settings.getIntegerValue(Settings.ConfigOption.SYNCHRONIZATION_MAX_USER_DATA_SNAPSHOTS))),
|
||||
resourceReader, dataAdapter, eventCannon, logger);
|
||||
this.mySqlHost = settings.getStringValue(Settings.ConfigOption.DATABASE_HOST);
|
||||
this.mySqlPort = settings.getIntegerValue(Settings.ConfigOption.DATABASE_PORT);
|
||||
this.mySqlDatabaseName = settings.getStringValue(Settings.ConfigOption.DATABASE_NAME);
|
||||
this.mySqlUsername = settings.getStringValue(Settings.ConfigOption.DATABASE_USERNAME);
|
||||
this.mySqlPassword = settings.getStringValue(Settings.ConfigOption.DATABASE_PASSWORD);
|
||||
this.mySqlConnectionParameters = settings.getStringValue(Settings.ConfigOption.DATABASE_CONNECTION_PARAMS);
|
||||
this.hikariMaximumPoolSize = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MAX_SIZE);
|
||||
this.hikariMinimumIdle = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MIN_IDLE);
|
||||
this.hikariMaximumLifetime = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MAX_LIFETIME);
|
||||
this.hikariKeepAliveTime = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_KEEPALIVE);
|
||||
this.hikariConnectionTimeOut = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the auto-closeable connection from the hikariDataSource
|
||||
*
|
||||
* @return The {@link Connection} to the MySQL database
|
||||
* @throws SQLException if the connection fails for some reason
|
||||
*/
|
||||
private Connection getConnection() throws SQLException {
|
||||
return connectionPool.getConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initialize() {
|
||||
try {
|
||||
// Create jdbc driver connection url
|
||||
final String jdbcUrl = "jdbc:mysql://" + mySqlHost + ":" + mySqlPort + "/" + mySqlDatabaseName + mySqlConnectionParameters;
|
||||
connectionPool = new HikariDataSource();
|
||||
connectionPool.setJdbcUrl(jdbcUrl);
|
||||
|
||||
// Authenticate
|
||||
connectionPool.setUsername(mySqlUsername);
|
||||
connectionPool.setPassword(mySqlPassword);
|
||||
|
||||
// Set various additional parameters
|
||||
connectionPool.setMaximumPoolSize(hikariMaximumPoolSize);
|
||||
connectionPool.setMinimumIdle(hikariMinimumIdle);
|
||||
connectionPool.setMaxLifetime(hikariMaximumLifetime);
|
||||
connectionPool.setKeepaliveTime(hikariKeepAliveTime);
|
||||
connectionPool.setConnectionTimeout(hikariConnectionTimeOut);
|
||||
connectionPool.setPoolName(DATA_POOL_NAME);
|
||||
|
||||
// Prepare database schema; make tables if they don't exist
|
||||
try (Connection connection = connectionPool.getConnection()) {
|
||||
// Load database schema CREATE statements from schema file
|
||||
final String[] databaseSchema = getSchemaStatements("database/mysql_schema.sql");
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
for (String tableCreationStatement : databaseSchema) {
|
||||
statement.execute(tableCreationStatement);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (SQLException | IOException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to perform database setup: " + e.getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().log(Level.SEVERE, "An unhandled exception occurred during database setup!", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> ensureUser(@NotNull User user) {
|
||||
return getUser(user.uuid).thenAccept(optionalUser ->
|
||||
optionalUser.ifPresentOrElse(existingUser -> {
|
||||
if (!existingUser.username.equals(user.username)) {
|
||||
// Update a user's name if it has changed in the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE `%users_table%`
|
||||
SET `username`=?
|
||||
WHERE `uuid`=?"""))) {
|
||||
|
||||
statement.setString(1, user.username);
|
||||
statement.setString(2, existingUser.uuid.toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
getLogger().log(Level.INFO, "Updated " + user.username + "'s name in the database (" + existingUser.username + " -> " + user.username + ")");
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to update a user's name on the database", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
// Insert new player data into the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO `%users_table%` (`uuid`,`username`)
|
||||
VALUES (?,?);"""))) {
|
||||
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, user.username);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `uuid`, `username`
|
||||
FROM `%users_table%`
|
||||
WHERE `uuid`=?"""))) {
|
||||
|
||||
statement.setString(1, uuid.toString());
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return Optional.of(new User(UUID.fromString(resultSet.getString("uuid")),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to fetch a user from uuid from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<User>> getUserByName(@NotNull String username) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `uuid`, `username`
|
||||
FROM `%users_table%`
|
||||
WHERE `username`=?"""))) {
|
||||
statement.setString(1, username);
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return Optional.of(new User(UUID.fromString(resultSet.getString("uuid")),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to fetch a user by name from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<UserDataSnapshot>> getCurrentUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
|
||||
FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=?
|
||||
ORDER BY `timestamp` DESC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final Blob blob = resultSet.getBlob("data");
|
||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
return Optional.of(new UserDataSnapshot(
|
||||
UUID.fromString(resultSet.getString("version_uuid")),
|
||||
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
|
||||
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
|
||||
resultSet.getBoolean("pinned"),
|
||||
getDataAdapter().fromBytes(dataByteArray)));
|
||||
}
|
||||
}
|
||||
} catch (SQLException | DataAdaptionException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<UserDataSnapshot>> getUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final List<UserDataSnapshot> retrievedData = new ArrayList<>();
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
|
||||
FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=?
|
||||
ORDER BY `timestamp` DESC;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
final Blob blob = resultSet.getBlob("data");
|
||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
final UserDataSnapshot data = new UserDataSnapshot(
|
||||
UUID.fromString(resultSet.getString("version_uuid")),
|
||||
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
|
||||
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
|
||||
resultSet.getBoolean("pinned"),
|
||||
getDataAdapter().fromBytes(dataByteArray));
|
||||
retrievedData.add(data);
|
||||
}
|
||||
return retrievedData;
|
||||
}
|
||||
} catch (SQLException | DataAdaptionException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
}
|
||||
return retrievedData;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<UserDataSnapshot>> getUserData(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
|
||||
FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
ORDER BY `timestamp` DESC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final Blob blob = resultSet.getBlob("data");
|
||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
return Optional.of(new UserDataSnapshot(
|
||||
UUID.fromString(resultSet.getString("version_uuid")),
|
||||
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
|
||||
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
|
||||
resultSet.getBoolean("pinned"),
|
||||
getDataAdapter().fromBytes(dataByteArray)));
|
||||
}
|
||||
}
|
||||
} catch (SQLException | DataAdaptionException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to fetch specific user data by UUID from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<Void> rotateUserData(@NotNull User user) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
final List<UserDataSnapshot> unpinnedUserData = getUserData(user).join().stream()
|
||||
.filter(dataSnapshot -> !dataSnapshot.pinned()).toList();
|
||||
if (unpinnedUserData.size() > maxUserDataRecords) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=?
|
||||
AND `pinned` IS FALSE
|
||||
ORDER BY `timestamp` ASC
|
||||
LIMIT %entry_count%;""".replace("%entry_count%",
|
||||
Integer.toString(unpinnedUserData.size() - maxUserDataRecords))))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to prune user data from the database", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> deleteUserData(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
return statement.executeUpdate() > 0;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to delete specific user data from the database", e);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData,
|
||||
@NotNull DataSaveCause saveCause) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
final DataSaveEvent dataSaveEvent = (DataSaveEvent) getEventCannon().fireDataSaveEvent(user,
|
||||
userData, saveCause).join();
|
||||
if (!dataSaveEvent.isCancelled()) {
|
||||
final UserData finalData = dataSaveEvent.getUserData();
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO `%user_data_table%`
|
||||
(`player_uuid`,`version_uuid`,`timestamp`,`save_cause`,`data`)
|
||||
VALUES (?,UUID(),NOW(),?,?);"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, saveCause.name());
|
||||
statement.setBlob(3, new ByteArrayInputStream(
|
||||
getDataAdapter().toBytes(finalData)));
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException | DataAdaptionException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to set user data in the database", e);
|
||||
}
|
||||
}
|
||||
}).thenRun(() -> rotateUserData(user).join());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> pinUserData(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE `%user_data_table%`
|
||||
SET `pinned`=TRUE
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to pin user data in the database", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> unpinUserData(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE `%user_data_table%`
|
||||
SET `pinned`=FALSE
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to unpin user data in the database", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> wipeDatabase() {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
statement.executeUpdate(formatStatementTables("DELETE FROM `%user_data_table%`;"));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to wipe the database", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (connectionPool != null) {
|
||||
if (!connectionPool.isClosed()) {
|
||||
connectionPool.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package net.william278.husksync.editor;
|
||||
|
||||
import net.william278.husksync.command.Permission;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.data.AdvancementData;
|
||||
import net.william278.husksync.data.ItemData;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Provides methods for displaying and editing user data
|
||||
*/
|
||||
public class DataEditor {
|
||||
|
||||
/**
|
||||
* Map of currently open inventory and ender chest data editors
|
||||
*/
|
||||
@NotNull
|
||||
protected final HashMap<UUID, ItemEditorMenu> openInventoryMenus;
|
||||
|
||||
private final Locales locales;
|
||||
|
||||
public DataEditor(@NotNull Locales locales) {
|
||||
this.openInventoryMenus = new HashMap<>();
|
||||
this.locales = locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open an inventory or ender chest editor menu
|
||||
*
|
||||
* @param user The online user to open the editor for
|
||||
* @param itemEditorMenu The {@link ItemEditorMenu} to open
|
||||
* @see ItemEditorMenu#createInventoryMenu(ItemData, User, OnlineUser, Locales, boolean)
|
||||
* @see ItemEditorMenu#createEnderChestMenu(ItemData, User, OnlineUser, Locales, boolean)
|
||||
*/
|
||||
public CompletableFuture<ItemData> openItemEditorMenu(@NotNull OnlineUser user,
|
||||
@NotNull ItemEditorMenu itemEditorMenu) {
|
||||
this.openInventoryMenus.put(user.uuid, itemEditorMenu);
|
||||
return itemEditorMenu.showInventory(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close an inventory or ender chest editor menu
|
||||
*
|
||||
* @param user The online user to close the editor for
|
||||
* @param itemData the {@link ItemData} contained within the menu at the time of closing
|
||||
*/
|
||||
public void closeInventoryMenu(@NotNull OnlineUser user, @NotNull ItemData itemData) {
|
||||
if (this.openInventoryMenus.containsKey(user.uuid)) {
|
||||
this.openInventoryMenus.get(user.uuid).closeInventory(itemData);
|
||||
}
|
||||
this.openInventoryMenus.remove(user.uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether edits to the inventory or ender chest menu are allowed
|
||||
*
|
||||
* @param user The online user with an inventory open to check
|
||||
* @return {@code true} if edits to the inventory or ender chest menu are allowed; {@code false} otherwise, including if they don't have an inventory open
|
||||
*/
|
||||
public boolean cancelMenuEdit(@NotNull OnlineUser user) {
|
||||
if (this.openInventoryMenus.containsKey(user.uuid)) {
|
||||
return !this.openInventoryMenus.get(user.uuid).canEdit;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a chat menu detailing information about {@link UserDataSnapshot}
|
||||
*
|
||||
* @param user The online user to display the message to
|
||||
* @param userData The {@link UserDataSnapshot} to display information about
|
||||
* @param dataOwner The {@link User} who owns the {@link UserDataSnapshot}
|
||||
*/
|
||||
public void displayDataOverview(@NotNull OnlineUser user, @NotNull UserDataSnapshot userData,
|
||||
@NotNull User dataOwner) {
|
||||
locales.getLocale("data_manager_title",
|
||||
userData.versionUUID().toString().split("-")[0],
|
||||
userData.versionUUID().toString(),
|
||||
dataOwner.username,
|
||||
dataOwner.uuid.toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
locales.getLocale("data_manager_timestamp",
|
||||
new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss").format(userData.versionTimestamp()))
|
||||
.ifPresent(user::sendMessage);
|
||||
if (userData.pinned()) {
|
||||
locales.getLocale("data_manager_pinned").ifPresent(user::sendMessage);
|
||||
}
|
||||
locales.getLocale("data_manager_cause",
|
||||
userData.cause().name().toLowerCase().replaceAll("_", " "))
|
||||
.ifPresent(user::sendMessage);
|
||||
locales.getLocale("data_manager_status",
|
||||
Integer.toString((int) userData.userData().getStatusData().health),
|
||||
Integer.toString((int) userData.userData().getStatusData().maxHealth),
|
||||
Integer.toString(userData.userData().getStatusData().hunger),
|
||||
Integer.toString(userData.userData().getStatusData().expLevel),
|
||||
userData.userData().getStatusData().gameMode.toLowerCase())
|
||||
.ifPresent(user::sendMessage);
|
||||
locales.getLocale("data_manager_advancements_statistics",
|
||||
Integer.toString(userData.userData().getAdvancementData().size()),
|
||||
generateAdvancementPreview(userData.userData().getAdvancementData()),
|
||||
String.format("%.2f", (((userData.userData().getStatisticsData().untypedStatistics.getOrDefault(
|
||||
"PLAY_ONE_MINUTE", 0)) / 20d) / 60d) / 60d))
|
||||
.ifPresent(user::sendMessage);
|
||||
if (user.hasPermission(Permission.COMMAND_INVENTORY.node)
|
||||
&& user.hasPermission(Permission.COMMAND_ENDER_CHEST.node)) {
|
||||
locales.getLocale("data_manager_item_buttons",
|
||||
dataOwner.username, userData.versionUUID().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
if (user.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
locales.getLocale("data_manager_management_buttons",
|
||||
dataOwner.username, userData.versionUUID().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String generateAdvancementPreview(@NotNull List<AdvancementData> advancementData) {
|
||||
final StringJoiner joiner = new StringJoiner("\n");
|
||||
final List<AdvancementData> advancementsToPreview = advancementData.stream().filter(dataItem ->
|
||||
!dataItem.key.startsWith("minecraft:recipes/")).toList();
|
||||
final int PREVIEW_SIZE = 8;
|
||||
for (int i = 0; i < advancementsToPreview.size(); i++) {
|
||||
joiner.add(advancementsToPreview.get(i).key);
|
||||
if (i >= PREVIEW_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
final int remainingAdvancements = advancementsToPreview.size() - PREVIEW_SIZE;
|
||||
if (remainingAdvancements > 0) {
|
||||
joiner.add(locales.getRawLocale("data_manager_advancements_preview_remaining",
|
||||
Integer.toString(remainingAdvancements)).orElse("+" + remainingAdvancements + "…"));
|
||||
}
|
||||
return joiner.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a chat list detailing a player's saved list of {@link UserDataSnapshot}
|
||||
*
|
||||
* @param user The online user to display the message to
|
||||
* @param userDataList The list of {@link UserDataSnapshot} to display
|
||||
* @param dataOwner The {@link User} who owns the {@link UserDataSnapshot}
|
||||
*/
|
||||
public void displayDataList(@NotNull OnlineUser user, @NotNull List<UserDataSnapshot> userDataList,
|
||||
@NotNull User dataOwner) {
|
||||
locales.getLocale("data_list_title",
|
||||
dataOwner.username, dataOwner.uuid.toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
final String[] numberedIcons = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳".split("");
|
||||
for (int i = 0; i < Math.min(20, userDataList.size()); i++) {
|
||||
final UserDataSnapshot userData = userDataList.get(i);
|
||||
locales.getLocale("data_list_item",
|
||||
numberedIcons[i],
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
||||
.format(userData.versionTimestamp()),
|
||||
userData.versionUUID().toString().split("-")[0],
|
||||
userData.versionUUID().toString(),
|
||||
userData.cause().name().toLowerCase().replaceAll("_", " "),
|
||||
dataOwner.username,
|
||||
userData.pinned() ? "※" : " ")
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the user has an inventory editor menu open
|
||||
*
|
||||
* @param user {@link OnlineUser} to check
|
||||
* @return {@code true} if the user has an inventory editor open; {@code false} otherwise
|
||||
*/
|
||||
public Optional<ItemEditorMenu> getEditingInventoryData(@NotNull OnlineUser user) {
|
||||
return this.openInventoryMenus.containsKey(user.uuid) ? Optional.of(this.openInventoryMenus.get(user.uuid))
|
||||
: Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package net.william278.husksync.editor;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.william278.husksync.command.Permission;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.data.ItemData;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class ItemEditorMenu {
|
||||
|
||||
public final ItemData itemData;
|
||||
public final ItemEditorMenuType itemEditorMenuType;
|
||||
public final MineDown menuTitle;
|
||||
public final boolean canEdit;
|
||||
|
||||
private CompletableFuture<ItemData> inventoryDataCompletableFuture;
|
||||
|
||||
private ItemEditorMenu(@NotNull ItemData itemData, ItemEditorMenuType itemEditorMenuType,
|
||||
@NotNull MineDown menuTitle, boolean canEdit) {
|
||||
this.itemData = itemData;
|
||||
this.menuTitle = menuTitle;
|
||||
this.itemEditorMenuType = itemEditorMenuType;
|
||||
this.canEdit = canEdit;
|
||||
}
|
||||
|
||||
public CompletableFuture<ItemData> showInventory(@NotNull OnlineUser user) {
|
||||
inventoryDataCompletableFuture = new CompletableFuture<>();
|
||||
user.showMenu(this);
|
||||
return inventoryDataCompletableFuture;
|
||||
}
|
||||
|
||||
public void closeInventory(@NotNull ItemData itemData) {
|
||||
inventoryDataCompletableFuture.complete(itemData);
|
||||
}
|
||||
|
||||
public static ItemEditorMenu createInventoryMenu(@NotNull ItemData itemData, @NotNull User dataOwner,
|
||||
@NotNull OnlineUser viewer, @NotNull Locales locales,
|
||||
boolean canEdit) {
|
||||
return new ItemEditorMenu(itemData, ItemEditorMenuType.INVENTORY_VIEWER,
|
||||
locales.getLocale(ItemEditorMenuType.INVENTORY_VIEWER.localeKey, dataOwner.username).orElse(new MineDown("")),
|
||||
viewer.hasPermission(Permission.COMMAND_INVENTORY_EDIT.node) && canEdit);
|
||||
}
|
||||
|
||||
public static ItemEditorMenu createEnderChestMenu(@NotNull ItemData itemData, @NotNull User dataOwner,
|
||||
@NotNull OnlineUser viewer, @NotNull Locales locales,
|
||||
boolean canEdit) {
|
||||
return new ItemEditorMenu(itemData, ItemEditorMenuType.ENDER_CHEST_VIEWER,
|
||||
locales.getLocale(ItemEditorMenuType.ENDER_CHEST_VIEWER.localeKey, dataOwner.username).orElse(new MineDown("")),
|
||||
viewer.hasPermission(Permission.COMMAND_ENDER_CHEST_EDIT.node) && canEdit);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package net.william278.husksync.editor;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public enum ItemEditorMenuType {
|
||||
INVENTORY_VIEWER(45, "inventory_viewer_menu_title"),
|
||||
ENDER_CHEST_VIEWER(27, "ender_chest_viewer_menu_title");
|
||||
|
||||
public final int slotCount;
|
||||
final String localeKey;
|
||||
|
||||
ItemEditorMenuType(int slotCount, @NotNull String localeKey) {
|
||||
this.slotCount = slotCount;
|
||||
this.localeKey = localeKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package net.william278.husksync.event;
|
||||
|
||||
public interface CancellableEvent extends Event {
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
default boolean isCancelled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
void setCancelled(boolean cancelled);
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user