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

Compare commits

...

85 Commits

Author SHA1 Message Date
William
2b6e2a1ddc Prep 1.4 release 2022-06-08 01:15:54 +01:00
William
aed68b3045 follow redirects in curl 2022-06-08 00:35:52 +01:00
William
6ee5141c8f Missing -Dfile= 2022-06-08 00:19:57 +01:00
William
3ff3c1207c change curl 2022-06-08 00:15:58 +01:00
William
5947bac82c fix param passing 2022-06-08 00:11:41 +01:00
William
baab59d17a fix maven command 2022-06-08 00:03:36 +01:00
William
e98f272dfd another typo 2022-06-08 00:00:21 +01:00
William
f4b680bee7 Typo in script 2022-06-07 23:57:21 +01:00
William
73fe184e70 add mpdbdataconverter install script 2022-06-07 23:55:35 +01:00
William
bdc07f064d Update jitpack.yml 2022-06-07 23:39:19 +01:00
William
01aa28f28c Update jitpack.yml 2022-06-07 23:35:46 +01:00
William
5de81f06d6 Remove MPDB dependency requirement. Now uses a library I made. 2022-06-07 23:16:05 +01:00
William
d5da516f17 or not 2022-06-07 22:38:29 +01:00
William
5e0e3fd27d exclude mpdb from build 2022-06-07 22:36:15 +01:00
William
5cee7cca84 Update build script 2022-06-07 22:05:29 +01:00
William
330627553e Update jitpack.yml 2022-06-07 21:59:27 +01:00
William
359f0d6f18 Refactor API 2022-06-07 21:59:02 +01:00
William
b531196d8a Small typo fix 2022-06-07 21:00:51 +01:00
William
951fc27a67 It's 2022 now 2022-06-07 21:00:33 +01:00
William
a5f7b37fac Fix incompatibility with custom health scalars, close #25 2022-06-07 20:59:59 +01:00
William
da7a85dde9 Save player data on WorldSaveEvent, add config option, close #21 2022-06-07 20:55:47 +01:00
William
0f215c80ea Update jitpack.yml automation 2022-06-07 20:45:43 +01:00
William
a76aecdd23 Refactor package to net.william278; update dependencies & stop shading internal modules 2022-06-07 20:44:53 +01:00
William
2f3b0f37e8 Fix wrong locale being displayed 2022-06-07 20:27:09 +01:00
William
330476ee23 Credit for Ukrainian locales 2022-05-13 21:59:48 +01:00
Thourgard
6bf36bcbb1 Create uk-ua.yml 2022-05-13 22:21:38 +03:00
William
a25b7a2c89 Add command usage details to README 2022-05-01 16:00:54 +01:00
William
ab3271c0ec Add simplified chinese credit 2022-03-15 12:37:46 +00:00
William
0fd5e4eb36 Merge pull request #24 from Ghost-chu/master
Simplified Chinese (China Mainland) translation
2022-03-15 12:37:09 +00:00
William
996b9bc63d Fix zh-tw file formatting, add credit 2022-03-15 12:36:48 +00:00
Ghost_chu
bedb903215 Simplified Chinese (China Mainland) 2022-03-15 14:20:04 +08:00
William
d8b80388fd Merge pull request #23 from davgo0103/master
Add Traditional Chinese (Taiwan) language file
2022-03-14 17:06:04 +00:00
小蔡
3f77b8b5f6 Delete .vs directory 2022-03-14 00:52:02 +08:00
小蔡
bd599081e5 Delete ProjectSettings.json 2022-03-13 00:54:35 +08:00
davgo0103
4d5902132c Add Traditional Chinese (Taiwan) language file 2022-03-13 00:49:10 +08:00
davgo0103
bd3c080b4f Add Traditional Chinese (Taiwan) language file 2022-03-12 21:20:48 +08:00
William
dd46a6cdd5 Disable native_advancement_synchronization by default 2022-03-04 13:18:10 +00:00
William
7d453b7438 Merge remote-tracking branch 'origin/master' 2022-02-26 21:52:39 +00:00
William
c5e0640f83 Async proxy data setting and fetching; add timestamp API to player data; add option for bounceBackSyncrhonization 2022-02-26 21:52:34 +00:00
William
fb3b2bd66e Update README.md 2022-02-19 19:31:03 +00:00
William
cfe3879010 Update README.md 2022-02-19 19:29:38 +00:00
William
aedb517662 Update README.md 2022-02-19 19:27:19 +00:00
William
b7e6861f03 Add credit for ceddix 2022-02-18 12:36:41 +00:00
William
61072bfa51 Merge remote-tracking branch 'origin/master' 2022-02-18 12:35:32 +00:00
William
ae439595ea Async data serialization and add synchronization_timeout_retry_delay config option 2022-02-18 12:35:27 +00:00
William
e1f6e40624 Merge pull request #19 from Ceddix/master
Create de-de.yml
2022-02-16 14:36:54 +00:00
Ceddix
ff17b58473 Create de-de.yml 2022-02-16 15:15:01 +01:00
William
2d8f139940 Encode Javadocs with UTF-8 2022-02-16 00:23:48 +00:00
William
07452083cb Merge remote-tracking branch 'origin/master' 2022-02-12 15:36:32 +00:00
William
55ea6bc391 Add spanish locale credit 2022-02-12 15:36:27 +00:00
William
742a033cf7 Add spanish (es-es) localisation by anchelthe 2022-02-12 15:20:12 +00:00
William
a3d745898e Add localisation credit 2022-02-06 14:59:44 +00:00
William
25c4553dd8 Merge branch 'ja-jp' 2022-02-06 14:57:34 +00:00
William
049dcbe589 Fix rare EndOfStream exception with Jedis listener 2022-02-06 14:56:48 +00:00
Namiu/うにたろう
3ffa2dc0ca Create ja-jp.yml
Add japanese language file
2022-02-06 17:18:00 +09:00
William
9bf0fe7bb9 Use a continuous connection for pub/sub to avoid EndOfStreamException and increase exception logging verbosity 2022-02-05 15:15:04 +00:00
William
4ab5070043 Comment typo 2022-02-03 19:36:25 +00:00
William
1664f1bf66 Upgrade bStats to 3.0.0 2022-02-03 19:31:22 +00:00
Harvels X
1a45100907 Add Gradle parallel execution of tasks; 2022-02-02 18:10:41 +03:00
Harvels X
50b07721d9 Resolve conversations https://github.com/WiIIiam278/HuskSync/pull/16; 2022-02-02 17:50:11 +03:00
Harvels X
b450910b5a Fix meta separator in VersionUtils.toString; 2022-01-31 20:14:36 +03:00
Harvels X
28c14ed393 Edit: fix UpdateChecker version check;
Add: version utils;
2022-01-31 20:03:24 +03:00
Harvels X
3c50245540 Edit: relocations & excludes;
Refactoring modules configuration;
2022-01-31 18:23:01 +03:00
Harvels X
6ea8cdb75c Edit: move shadow plugin to global;
Add: shading main modules;
2022-01-31 18:21:31 +03:00
Harvels X
fd42bc99be Edit: move plugin manifests;
Edit: retrieve a version of velocity;
2022-01-31 18:18:10 +03:00
Harvels X
545c0896f0 Add: static project properties; 2022-01-31 16:02:25 +03:00
Harvels X
d67d5b64da Edit: remove use shadowing for apis; 2022-01-31 16:01:32 +03:00
Harvels X
83ddc76075 Edit: cleanup & move to submodule;
Add: filter resource for all projects & use maven style;
Edit: add revision or build number in project version;
2022-01-31 16:00:08 +03:00
William
06e72f0831 Merge remote-tracking branch 'origin/master' 2022-01-27 16:20:18 +00:00
William
c439ad59ac Fixed current timestamp being generated being incorrect 2022-01-27 16:19:56 +00:00
William
aa3e73ea33 Add velocity metrics to README.md 2022-01-24 13:49:40 +00:00
William
37520991e5 Conformity to Bukkit API conventions 2022-01-22 22:41:39 +00:00
William
804f156027 Bump version number 2022-01-22 22:38:37 +00:00
William
56ecb7f76a Use lowest event priority so HuskSync fires first 2022-01-22 22:37:16 +00:00
William
7a89ffdf35 Make event priority LOWEST 2022-01-22 22:32:44 +00:00
William
6719858de1 Add SSL connection option for Redis 2022-01-21 00:57:16 +00:00
William
920d2582f5 Update Redis initialization handling now that it is multithreaded 2022-01-20 18:36:33 +00:00
William
7d46ce076b Authenticate when creating the ConnectionPool 2022-01-20 18:17:25 +00:00
William
027ee0dbbb Escape version string 2022-01-19 17:51:36 +00:00
William
93be26a946 Use JedisPool instead of single Jedis connection 2022-01-19 17:29:25 +00:00
William
4ec4ba9a1e Fix description 2022-01-19 13:56:29 +00:00
William
6d31d28f47 Bump Jedis, fix status message missing a newline 2022-01-19 13:55:03 +00:00
William
de3838873e Merge remote-tracking branch 'origin/master' 2022-01-13 18:01:28 +00:00
William
f01bb7c082 Update author URL 2022-01-13 18:01:23 +00:00
William
051e2c5b72 Update README.md 2022-01-07 20:52:01 +00:00
75 changed files with 971 additions and 706 deletions

2
.gitignore vendored
View File

@@ -106,7 +106,7 @@ build/
# Ignore Gradle GUI config
gradle-app.setting
# me.william278.crossserversync.bungeecord.data.DataManager.PlayerDataCache of project
# net.william278.crossserversync.bungeecord.data.DataManager.PlayerDataCache of project
.gradletasknamecache
**/build/

View File

@@ -0,0 +1,5 @@
#!/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"

View File

@@ -1,4 +1,4 @@
Copyright © William278 2021. All rights reserved
Copyright © William278 2022. All rights reserved
LICENSE
This source code is provided as reference to licensed individuals that have purchased the HuskSync
@@ -8,7 +8,7 @@ not grant you the rights to modify, re-distribute, compile or redistribute this
parties that are utilised in the plugin.
CONTRIBUTOR AGREEMENT
By contributing code to this repository, contributors agree that they forefeit their contributions
By contributing code to this repository, contributors agree that they forfeit their contributions
to the copyright holder and only the copyright holder.
In exchange for contributing, the copyright holder may give, at their discretion, permission to use
the plugin in commercial contexts

View File

@@ -37,10 +37,15 @@ To migrate from MySQLPLayerDataBridge, you need a Proxy server with HuskSync ins
#### 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.)
#### SQL errors in proxy console / data not synchronising
#### 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
![Flow chart showing different processes of how the plugin works](images/flow-chart.png)
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.
@@ -69,15 +74,15 @@ Everything except player locations are synchronised by default. You can enable o
### 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` | View an offline player's inventory | `husksync.command.inventory` |
| `/husksync echest` | View an offline player's ender chest | `husksync.command.ender_chest` |
| `/husksync migrate` | Migrate data from MPDB | _Console-only_ |
| 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?
@@ -131,7 +136,7 @@ With Maven, add the repository to your pom.xml:
Then, add the dependency. Replace `version` with the latest version of HuskSync: [![](https://jitpack.io/v/WiIIiam278/HuskSync.svg)](https://jitpack.io/#WiIIiam278/HuskSync)
```xml
<dependency>
<groupId>com.github.WiIIiam278</groupId>
<groupId>net.william278</groupId>
<artifactId>HuskSync</artifactId>
<version>version</version>
<scope>provided</scope>
@@ -147,10 +152,10 @@ Or, with Gradle, add the dependency like so to your build.gradle:
}
}
```
Then add the dependency as follows. Replace `version` with the latest version of HuskSync: [![](https://jitpack.io/v/WiIIiam278/HuskSync.svg)](https://jitpack.io/#WiIIiam278/HuskSync)
Then add the dependency as follows. Replace `version` with the latest version of HuskSync: [![](https://jitpack.io/v/net.william278/HuskSync.svg)](https://jitpack.io/#net.william278/HuskSync)
```
dependencies {
compileOnly 'com.github.WiIIiam278:HuskSync:version'
compileOnly 'net.william278:HuskSync:version'
}
```
@@ -160,7 +165,7 @@ Then add the dependency as follows. Replace `version` with the latest version of
#### 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);
@@ -174,15 +179,15 @@ try {
```
#### Getting ItemStacks and usable data from PlayerData
Use the static methods provided in the [DataSerializer class](https://javadoc.jitpack.io/com/github/WiIIiam278/HuskSync/latest/javadoc/me/william278/husksync/bukkit/data/DataSerializer.html). For instance, to get a player's inventory as an `ItemStack[]` from a `PlayerData` object.
```
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 {
@@ -201,10 +206,9 @@ A code bounty program is in place for HuskSync, where developers making signific
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
To build HuskSync you will first need to download MySqlPlayerDataBridge and `mvn install:install-file` the jar file to your local maven repository.
```
mvn install:install-file -Dfile=MysqlPlayerDataBridge-v4.0.1.jar -DgroupId=net.craftersland.data -DartifactId=bridge -Dversion=4.0.1 -Dpackaging=jar
```
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:
```
@@ -215,10 +219,11 @@ Then, to build the plugin, run the following in the root of the repository:
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)
You can turn metric collection off by navigating to `plugins/bStats/config.yml` and editing the config to disable plugin metrics.
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.
* Proof of purchase is required for support.

View File

@@ -1,44 +1,20 @@
dependencies {
implementation project(':common')
compileOnly project(path: ':common')
implementation project(path: ':bukkit')
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
compileOnly 'org.jetbrains:annotations:22.0.0'
}
publishing {
publications {
mavenJava(MavenPublication) {
shadow.component(it)
afterEvaluate {
artifact javadocsJar
}
}
}
repositories {
mavenLocal()
}
compileOnly 'org.jetbrains:annotations:23.0.0'
}
shadowJar {
classifier = null
relocate ':common', 'me.william278.husksync'
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'
}
repositories {
mavenCentral()
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
}
task javadocs(type: Javadoc) {
options.addStringOption('Xdoclint:none', '-quiet')
source = project(':common').sourceSets.main.allJava
source += project(':api').sourceSets.main.allJava
classpath = files(project(':common').sourceSets.main.compileClasspath)
classpath += files(project(':api').sourceSets.main.compileClasspath)
destinationDir = file("${buildDir}/docs/javadoc")
}
task javadocsJar(type: Jar, dependsOn: javadocs) {
classifier = 'javadoc'
from javadocs.destinationDir
java {
withSourcesJar()
withJavadocJar()
}

View File

@@ -1,16 +1,18 @@
package me.william278.husksync.bukkit.api;
package net.william278.husksync.bukkit.api;
import me.william278.husksync.PlayerData;
import me.william278.husksync.Settings;
import me.william278.husksync.redis.RedisMessage;
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.HashMap;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
* API method class for HuskSync. To access methods, use the {@link #getInstance()} entrypoint.
* HuskSync's API. To access methods, use the {@link #getInstance()} entrypoint.
*
* @author William
*/
public class HuskSyncAPI {
@@ -20,7 +22,7 @@ public class HuskSyncAPI {
private static HuskSyncAPI instance;
/**
* API entry point. Returns an instance of the {@link HuskSyncAPI}
* The API entry point. Returns an instance of the {@link HuskSyncAPI}
*
* @return instance of the {@link HuskSyncAPI}
*/
@@ -32,35 +34,38 @@ public class HuskSyncAPI {
}
/**
* (INTERNAL) Map of API requests that are processed by the bukkit plugin that implements the API.
*/
public static HashMap<UUID, CompletableFuture<PlayerData>> apiRequests = new HashMap<>();
/**
* Returns a {@link CompletableFuture} that will fetch the {@link PlayerData} for a user given their {@link UUID}, which contains synchronised data that can then be deserialized into ItemStacks and other usable values using the {@link me.william278.husksync.bukkit.data.DataSerializer} class. If no data could be returned, such as if an invalid UUID is specified, the CompletableFuture will be cancelled. Note that this only returns the last cached data of the user; not necessarily the current state of their inventory if they are online.
* 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();
apiRequests.put(requestUUID, new CompletableFuture<>());
BukkitRedisListener.apiRequests.put(requestUUID, new CompletableFuture<>());
// Remove the request from the map on completion
apiRequests.get(requestUUID).whenComplete((playerData, throwable) -> apiRequests.remove(requestUUID));
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 apiRequests.get(requestUUID);
return BukkitRedisListener.apiRequests.get(requestUUID);
}
/**
* Updates a player's {@link PlayerData} to the central cache and database. If the player is online on the Proxy network, they will be updated and overwritten with this data.
* 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
@@ -70,7 +75,7 @@ public class HuskSyncAPI {
final String serializedPlayerData = RedisMessage.serialize(playerData);
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
serializedPlayerData).send();
serializedPlayerData, Boolean.toString(true)).send();
}
}

View File

@@ -1,33 +1,27 @@
buildscript {
repositories {
mavenCentral()
}
}
plugins {
id 'com.github.johnrengelman.shadow' version '7.1.0' apply false
id 'com.github.johnrengelman.shadow' version '7.1.0'
id 'org.ajoberstar.grgit' version '4.1.1'
id 'java'
}
allprojects {
group 'me.William278'
version '1.3'
group 'net.william278'
version "$ext.plugin_version+${versionMetadata()}"
compileJava { options.encoding = 'UTF-8' }
tasks.withType(JavaCompile) { options.encoding = 'UTF-8' }
javadoc { options.encoding = 'UTF-8' }
ext {
set 'version', version.toString()
}
logger.lifecycle('Building HuskSync v' + version.toString())
import org.apache.tools.ant.filters.ReplaceTokens
subprojects {
allprojects {
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'java'
apply plugin: 'maven-publish'
compileJava {
options.release = 16
}
compileJava.options.encoding = 'UTF-8'
javadoc.options.encoding = 'UTF-8'
javadoc.options.addStringOption('Xdoclint:none', '-quiet')
compileJava.options.release.set 16
repositories {
mavenLocal()
@@ -39,4 +33,40 @@ subprojects {
maven { url 'https://repo.alessiodp.com/releases/' }
maven { url 'https://jitpack.io' }
}
dependencies {
implementation('redis.clients:jedis:4.2.3') {
//noinspection GroovyAssignabilityCheck
exclude module: 'slf4j-api'
}
}
processResources {
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
tokens: rootProject.ext.properties
}
}
subprojects {
version rootProject.version
archivesBaseName = "${rootProject.name}-${project.name.capitalize()}"
if (['bukkit', 'api', 'bungeecord', 'velocity', 'plugin'].contains(project.name)) {
shadowJar {
destinationDirectory.set(file("$rootDir/target"))
archiveClassifier.set('')
}
jar.dependsOn shadowJar
clean.delete "$rootDir/target"
}
}
logger.lifecycle("Building HuskSync ${version} by William278")
@SuppressWarnings('GrMethodMayBeStatic')
def versionMetadata() {
if (grgit == null) {
return System.getenv("GITHUB_RUN_NUMBER") ? 'build.' + System.getenv("GITHUB_RUN_NUMBER") : 'unknown'
}
return 'rev.' + grgit.head().abbreviatedId + (grgit.status().clean ? '' : '-indev')
}

View File

@@ -1,20 +1,18 @@
dependencies {
compileOnly project(':common')
compileOnly project(':api')
implementation project(path: ':common', configuration: 'shadow')
implementation project(path: ':common')
compileOnly 'redis.clients:jedis:3.7.1'
implementation 'org.bstats:bstats-bukkit:2.2.1'
implementation 'org.bstats:bstats-bukkit:3.0.0'
implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
implementation 'net.william278:mpdbdataconverter:1.0'
compileOnly 'net.craftersland.data:bridge:4.0.1'
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
compileOnly 'org.jetbrains:annotations:22.0.0'
compileOnly 'org.jetbrains:annotations:23.0.0'
}
shadowJar {
relocate 'org.bstats', 'me.William278.husksync.libraries.bstats.bukkit'
relocate 'de.themoep', 'me.William278.husksync.libraries.minedown.standard'
}
tasks.register('prepareKotlinBuildScriptModel'){}
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'
}

View File

@@ -1,41 +0,0 @@
package me.william278.husksync.bukkit.util.nms;
import me.william278.husksync.util.ThrowSupplier;
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 String MINECRAFT_PACKAGE = compare("1.17") < 0 ?
"net.minecraft.server.".concat(PACKAGE_VERSION) : "net.minecraft.server";
public final static String SERVER_VERSION = Bukkit.getBukkitVersion().split("-")[0];
public static int compare(String version) {
if (version == null || SERVER_VERSION == null) return 1;
String[] as = SERVER_VERSION.split("\\.");
String[] bs = version.split("\\.");
int length = Math.max(as.length, bs.length);
for (int i = 0; i < length; i++) {
int a = i < as.length ? Integer.parseInt(as[i]) : 0;
int b = i < bs.length ? Integer.parseInt(bs[i]) : 0;
if (a < b) return -1;
if (a > b) return 1;
}
return 0;
}
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)));
}
}

View File

@@ -1,13 +1,14 @@
package me.william278.husksync;
package net.william278.husksync;
import me.william278.husksync.bukkit.util.BukkitUpdateChecker;
import me.william278.husksync.bukkit.util.PlayerSetter;
import me.william278.husksync.bukkit.config.ConfigLoader;
import me.william278.husksync.bukkit.data.BukkitDataCache;
import me.william278.husksync.bukkit.listener.BukkitRedisListener;
import me.william278.husksync.bukkit.listener.BukkitEventListener;
import me.william278.husksync.bukkit.migrator.MPDBDeserializer;
import me.william278.husksync.redis.RedisMessage;
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;
@@ -17,7 +18,6 @@ import org.bukkit.scheduler.BukkitTask;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
public final class HuskSyncBukkit extends JavaPlugin {
@@ -32,6 +32,8 @@ public final class HuskSyncBukkit extends JavaPlugin {
public static BukkitDataCache bukkitCache;
public static BukkitRedisListener redisListener;
// Used for establishing a handshake with redis
public static UUID serverUUID;
@@ -120,11 +122,7 @@ public final class HuskSyncBukkit extends JavaPlugin {
getServer().getPluginManager().registerEvents(new BukkitEventListener(), this);
// Initialize the redis listener
if (!new BukkitRedisListener().isActiveAndEnabled) {
getPluginLoader().disablePlugin(this);
getLogger().severe("Failed to initialize Redis; disabling HuskSync (" + getServer().getName() + ") v" + getDescription().getVersion());
return;
}
redisListener = new BukkitRedisListener();
// Ensure redis is connected; establish a handshake
establishRedisHandshake();
@@ -146,7 +144,7 @@ public final class HuskSyncBukkit extends JavaPlugin {
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);
PlayerSetter.updatePlayerData(player, false);
}
getLogger().info("Data save complete!");
}

View File

@@ -1,6 +1,6 @@
package me.william278.husksync.bukkit.config;
package net.william278.husksync.bukkit.config;
import me.william278.husksync.Settings;
import net.william278.husksync.Settings;
import org.bukkit.configuration.file.FileConfiguration;
public class ConfigLoader {
@@ -12,6 +12,7 @@ public class ConfigLoader {
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);
@@ -26,6 +27,8 @@ public class ConfigLoader {
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);
}
}

View File

@@ -1,4 +1,4 @@
package me.william278.husksync.bukkit.data;
package net.william278.husksync.bukkit.data;
import java.util.HashMap;
import java.util.HashSet;

View File

@@ -1,6 +1,6 @@
package me.william278.husksync.bukkit.data;
package net.william278.husksync.bukkit.data;
import me.william278.husksync.redis.RedisMessage;
import net.william278.husksync.redis.RedisMessage;
import org.bukkit.*;
import org.bukkit.advancement.Advancement;
import org.bukkit.advancement.AdvancementProgress;
@@ -20,7 +20,7 @@ import java.time.Instant;
import java.util.*;
/**
* Class that contains static methods for serializing and deserializing data from {@link me.william278.husksync.PlayerData}
* Class that contains static methods for serializing and deserializing data from {@link net.william278.husksync.PlayerData}
*/
public class DataSerializer {

View File

@@ -1,10 +1,10 @@
package me.william278.husksync.bukkit.data;
package net.william278.husksync.bukkit.data;
import me.william278.husksync.HuskSyncBukkit;
import me.william278.husksync.PlayerData;
import me.william278.husksync.Settings;
import me.william278.husksync.bukkit.util.PlayerSetter;
import me.william278.husksync.redis.RedisMessage;
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;
@@ -58,7 +58,7 @@ public class DataViewer {
// 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))
RedisMessage.serialize(playerData), Boolean.toString(true))
.send();
}

View File

@@ -1,6 +1,6 @@
package me.william278.husksync.bukkit.api.events;
package net.william278.husksync.bukkit.events;
import me.william278.husksync.PlayerData;
import net.william278.husksync.PlayerData;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;

View File

@@ -1,6 +1,6 @@
package me.william278.husksync.bukkit.api.events;
package net.william278.husksync.bukkit.events;
import me.william278.husksync.PlayerData;
import net.william278.husksync.PlayerData;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;

View File

@@ -1,8 +1,9 @@
package me.william278.husksync.bukkit.listener;
package net.william278.husksync.bukkit.listener;
import me.william278.husksync.HuskSyncBukkit;
import me.william278.husksync.bukkit.data.DataViewer;
import me.william278.husksync.bukkit.util.PlayerSetter;
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;
@@ -14,6 +15,7 @@ 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;
@@ -22,7 +24,7 @@ public class BukkitEventListener implements Listener {
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
@EventHandler
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerQuit(PlayerQuitEvent event) {
// When a player leaves a Bukkit server
final Player player = event.getPlayer();
@@ -33,13 +35,14 @@ public class BukkitEventListener implements Listener {
return;
}
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled) return; // If the plugin has not been initialized correctly
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled)
return; // If the plugin has not been initialized correctly
// Update the player's data
PlayerSetter.updatePlayerData(player);
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> PlayerSetter.updatePlayerData(player, true));
}
@EventHandler
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerJoin(PlayerJoinEvent event) {
if (!plugin.isEnabled()) return; // If the plugin has not been initialized correctly
@@ -49,18 +52,22 @@ public class BukkitEventListener implements Listener {
// 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)
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())) {
try {
PlayerSetter.requestPlayerData(player.getUniqueId());
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData fetch request", e);
}
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 10 ticks, ensure it will be
Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, () -> {
// 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())) {
@@ -70,13 +77,14 @@ public class BukkitEventListener implements Listener {
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData fetch request", e);
}
}
}, 5);
}, 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
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();
@@ -95,14 +103,14 @@ public class BukkitEventListener implements Listener {
* Events to cancel if the player has not been set yet
*/
@EventHandler(priority = EventPriority.MONITOR)
@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.MONITOR)
@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())) {
@@ -111,31 +119,41 @@ public class BukkitEventListener implements Listener {
}
}
@EventHandler(priority = EventPriority.MONITOR)
@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.MONITOR)
@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.MONITOR)
@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.MONITOR)
@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);
}
}
}

View File

@@ -1,31 +1,35 @@
package me.william278.husksync.bukkit.listener;
package net.william278.husksync.bukkit.listener;
import de.themoep.minedown.MineDown;
import me.william278.husksync.HuskSyncBukkit;
import me.william278.husksync.PlayerData;
import me.william278.husksync.Settings;
import me.william278.husksync.bukkit.api.HuskSyncAPI;
import me.william278.husksync.bukkit.config.ConfigLoader;
import me.william278.husksync.bukkit.data.DataViewer;
import me.william278.husksync.bukkit.migrator.MPDBDeserializer;
import me.william278.husksync.bukkit.util.PlayerSetter;
import me.william278.husksync.migrator.MPDBPlayerData;
import me.william278.husksync.redis.RedisListener;
import me.william278.husksync.redis.RedisMessage;
import me.william278.husksync.util.MessageManager;
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();
}
@@ -110,10 +114,10 @@ public class BukkitRedisListener extends RedisListener {
}
case API_DATA_RETURN -> {
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[0]);
if (HuskSyncAPI.apiRequests.containsKey(requestUUID)) {
if (apiRequests.containsKey(requestUUID)) {
try {
final PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageDataElements()[1]);
HuskSyncAPI.apiRequests.get(requestUUID).complete(data);
apiRequests.get(requestUUID).complete(data);
} catch (IOException | ClassNotFoundException e) {
log(Level.SEVERE, "Failed to serialize returned API-requested player data");
}
@@ -123,8 +127,8 @@ public class BukkitRedisListener extends RedisListener {
case API_DATA_CANCEL -> {
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[0]);
// Cancel requests if no data could be found on the proxy
if (HuskSyncAPI.apiRequests.containsKey(requestUUID)) {
HuskSyncAPI.apiRequests.get(requestUUID).cancel(true);
if (apiRequests.containsKey(requestUUID)) {
apiRequests.get(requestUUID).cancel(true);
}
}
case RELOAD_CONFIG -> {

View File

@@ -1,17 +1,17 @@
package me.william278.husksync.bukkit.migrator;
package net.william278.husksync.bukkit.migrator;
import me.william278.husksync.HuskSyncBukkit;
import me.william278.husksync.PlayerData;
import me.william278.husksync.bukkit.util.PlayerSetter;
import me.william278.husksync.bukkit.data.DataSerializer;
import me.william278.husksync.migrator.MPDBPlayerData;
import net.craftersland.data.bridge.PD;
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.lang.reflect.InvocationTargetException;
import java.util.logging.Level;
public class MPDBDeserializer {
@@ -19,10 +19,12 @@ public class MPDBDeserializer {
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
// Instance of MySqlPlayerDataBridge
private static PD mySqlPlayerDataBridge;
private static MPDBConverter mpdbConverter;
public static void setMySqlPlayerDataBridge() {
mySqlPlayerDataBridge = (PD) Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
Plugin mpdbPlugin = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
assert mpdbPlugin != null;
mpdbConverter = MPDBConverter.getInstance(mpdbPlugin);
}
/**
@@ -44,13 +46,13 @@ public class MPDBDeserializer {
// Set inventory contents
Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
if (!mpdbPlayerData.inventoryData.isEmpty() && !mpdbPlayerData.inventoryData.equalsIgnoreCase("none")) {
PlayerSetter.setInventory(inventory, getItemStackArrayFromMPDBBase64String(mpdbPlayerData.inventoryData));
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 = getItemStackArrayFromMPDBBase64String(mpdbPlayerData.armorData);
ItemStack[] armorItems = mpdbConverter.getItemStackFromSerializedData(mpdbPlayerData.armorData);
for (ItemStack armorPiece : armorItems) {
if (armorPiece != null) {
inventory.setItem(armorSlot, armorPiece);
@@ -66,7 +68,7 @@ public class MPDBDeserializer {
// Set ender chest (again, if there is data)
ItemStack[] enderChestData;
if (!mpdbPlayerData.enderChestData.isEmpty() && !mpdbPlayerData.enderChestData.equalsIgnoreCase("none")) {
enderChestData = getItemStackArrayFromMPDBBase64String(mpdbPlayerData.enderChestData);
enderChestData = mpdbConverter.getItemStackFromSerializedData(mpdbPlayerData.enderChestData);
} else {
enderChestData = new ItemStack[0];
}
@@ -82,19 +84,4 @@ public class MPDBDeserializer {
}
return playerData;
}
/**
* Returns an ItemStack array from a decoded base 64 string in MySQLPlayerDataBridge's format
*
* @param data The encoded ItemStack[] string from MySQLPlayerDataBridge
* @return The {@link ItemStack[]} array
* @throws InvocationTargetException If an error occurs during decoding
* @throws IllegalAccessException If an error occurs during decoding
*/
public static ItemStack[] getItemStackArrayFromMPDBBase64String(String data) throws InvocationTargetException, IllegalAccessException {
if (data.isEmpty()) {
return new ItemStack[0];
}
return mySqlPlayerDataBridge.getItemStackSerializer().fromBase64(data);
}
}

View File

@@ -1,7 +1,7 @@
package me.william278.husksync.bukkit.util;
package net.william278.husksync.bukkit.util;
import me.william278.husksync.HuskSyncBukkit;
import me.william278.husksync.util.UpdateChecker;
import net.william278.husksync.HuskSyncBukkit;
import net.william278.husksync.util.UpdateChecker;
import java.util.logging.Level;

View File

@@ -1,13 +1,13 @@
package me.william278.husksync.bukkit.util;
package net.william278.husksync.bukkit.util;
import me.william278.husksync.HuskSyncBukkit;
import me.william278.husksync.PlayerData;
import me.william278.husksync.Settings;
import me.william278.husksync.bukkit.api.events.SyncCompleteEvent;
import me.william278.husksync.bukkit.api.events.SyncEvent;
import me.william278.husksync.bukkit.data.DataSerializer;
import me.william278.husksync.bukkit.util.nms.AdvancementUtils;
import me.william278.husksync.redis.RedisMessage;
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;
@@ -66,7 +66,7 @@ public class PlayerSetter {
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 seperately
// 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;
@@ -95,15 +95,16 @@ public class PlayerSetter {
/**
* Update a {@link Player}'s data, sending it to the proxy
*
* @param player {@link Player} to send data to 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) {
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).send();
serializedPlayerData, Boolean.toString(bounceBack)).send();
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData update to the proxy", e);
}
@@ -280,7 +281,7 @@ public class PlayerSetter {
final Object playerAdvancements = AdvancementUtils.getPlayerAdvancements(player);
// Clear
AdvancementUtils.clearPlayerAdvancements(playerAdvancements);
AdvancementUtils.clearPlayerAdvancements(playerAdvancements);
AdvancementUtils.clearVisibleAdvancements(playerAdvancements);
advancementRecords.forEach(advancementRecord -> {
@@ -330,7 +331,7 @@ public class PlayerSetter {
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
// Apply the advancements to the player
Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
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();
@@ -463,12 +464,13 @@ public class PlayerSetter {
*/
private static void setPlayerHealth(Player player, double health, double maxHealth, double healthScale) {
// Set max health
if (maxHealth != 0.0D) {
if (maxHealth != 0D) {
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).setBaseValue(maxHealth);
}
// Set health
player.setHealth(player.getHealth() > maxHealth ? maxHealth : health);
double currentHealth = player.getHealth();
if (health != currentHealth) player.setHealth(currentHealth > maxHealth ? maxHealth : health);
// Set health scaling if needed
if (healthScale != 0D) {

View File

@@ -1,6 +1,6 @@
package me.william278.husksync.bukkit.util.nms;
package net.william278.husksync.bukkit.util.nms;
import me.william278.husksync.util.ThrowSupplier;
import net.william278.husksync.util.ThrowSupplier;
import org.bukkit.advancement.Advancement;
import org.bukkit.entity.Player;

View File

@@ -1,6 +1,6 @@
package me.william278.husksync.bukkit.util.nms;
package net.william278.husksync.bukkit.util.nms;
import me.william278.husksync.util.ThrowSupplier;
import net.william278.husksync.util.ThrowSupplier;
import org.bukkit.entity.LivingEntity;
import java.lang.reflect.InvocationTargetException;

View File

@@ -0,0 +1,25 @@
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)));
}
}

View File

@@ -2,6 +2,7 @@ redis_settings:
host: 'localhost'
port: 6379
password: ''
use_ssl: false
synchronisation_settings:
inventories: true
ender_chests: true
@@ -16,4 +17,6 @@ synchronisation_settings:
flight: false
cluster_id: 'main'
check_for_updates: true
native_advancement_synchronization: true
synchronization_timeout_retry_delay: 15
save_on_world_save: true
native_advancement_synchronization: false

View File

@@ -1,7 +1,8 @@
name: HuskSync
version: @version@
main: me.william278.husksync.HuskSyncBukkit
version: ${version}
main: net.william278.husksync.HuskSyncBukkit
api-version: 1.16
author: William278
description: 'A modern, cross-server player data synchronization system'
website: 'https://william278.net'
softdepend: [MysqlPlayerDataBridge]

View File

@@ -1,9 +1,8 @@
dependencies {
compileOnly project(':common')
implementation project(path: ':common', configuration: 'shadow')
implementation project(path: ':common')
compileOnly 'redis.clients:jedis:3.7.1'
implementation 'org.bstats:bstats-bungeecord:2.2.1'
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'
@@ -11,10 +10,15 @@ dependencies {
}
shadowJar {
relocate 'com.zaxxer', 'me.William278.husksync.libraries.hikari'
relocate 'org.bstats', 'me.William278.husksync.libraries.bstats.bungee'
relocate 'de.themoep', 'me.William278.husksync.libraries.minedown.standard'
relocate 'net.byteflux', 'me.William278.husksync.libraries.libby.bungee'
}
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'
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
tasks.register('prepareKotlinBuildScriptModel'){}
dependencies {
//noinspection GroovyAssignabilityCheck
exclude dependency(':slf4j-api')
}
}

View File

@@ -1,16 +1,18 @@
package me.william278.husksync;
package net.william278.husksync;
import me.william278.husksync.bungeecord.command.BungeeCommand;
import me.william278.husksync.bungeecord.config.ConfigLoader;
import me.william278.husksync.bungeecord.config.ConfigManager;
import me.william278.husksync.proxy.data.DataManager;
import me.william278.husksync.bungeecord.listener.BungeeEventListener;
import me.william278.husksync.bungeecord.listener.BungeeRedisListener;
import me.william278.husksync.migrator.MPDBMigrator;
import me.william278.husksync.bungeecord.util.BungeeLogger;
import me.william278.husksync.bungeecord.util.BungeeUpdateChecker;
import me.william278.husksync.redis.RedisMessage;
import me.william278.husksync.util.Logger;
import net.william278.husksync.Server;
import net.william278.husksync.Settings;
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 net.byteflux.libby.BungeeLibraryManager;
import net.byteflux.libby.Library;
import net.md_5.bungee.api.ProxyServer;
@@ -48,6 +50,8 @@ public final class HuskSyncBungeeCord extends Plugin {
public static MPDBMigrator mpdbMigrator;
public static BungeeRedisListener redisListener;
private Logger logger;
public Logger getBungeeLogger() {
@@ -98,10 +102,7 @@ public final class HuskSyncBungeeCord extends Plugin {
}
// Initialize the redis listener
if (!new BungeeRedisListener().isActiveAndEnabled) {
getBungeeLogger().severe("Failed to initialize Redis; HuskSync will now abort loading itself (" + getProxy().getName() + ") v" + getDescription().getVersion());
return;
}
redisListener = new BungeeRedisListener();
// Register listener
getProxy().getPluginManager().registerListener(this, new BungeeEventListener());
@@ -156,7 +157,7 @@ public final class HuskSyncBungeeCord extends Plugin {
Library mySqlLib = Library.builder()
.groupId("mysql")
.artifactId("mysql-connector-java")
.version("8.0.27")
.version("8.0.29")
.build();
Library sqLiteLib = Library.builder()

View File

@@ -1,17 +1,17 @@
package me.william278.husksync.bungeecord.command;
package net.william278.husksync.bungeecord.command;
import de.themoep.minedown.MineDown;
import me.william278.husksync.HuskSyncBungeeCord;
import me.william278.husksync.Server;
import me.william278.husksync.bungeecord.util.BungeeUpdateChecker;
import me.william278.husksync.proxy.command.HuskSyncCommand;
import me.william278.husksync.util.MessageManager;
import me.william278.husksync.PlayerData;
import me.william278.husksync.Settings;
import me.william278.husksync.bungeecord.config.ConfigLoader;
import me.william278.husksync.bungeecord.config.ConfigManager;
import me.william278.husksync.migrator.MPDBMigrator;
import me.william278.husksync.redis.RedisMessage;
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;
@@ -301,7 +301,7 @@ public class BungeeCommand extends Command implements TabExecutor, HuskSyncComma
HuskSyncBungeeCord.synchronisedServers)) {
ProxyServer.getInstance().getScheduler().runAsync(plugin, () ->
HuskSyncBungeeCord.mpdbMigrator.executeMigrationOperations(HuskSyncBungeeCord.dataManager,
HuskSyncBungeeCord.synchronisedServers));
HuskSyncBungeeCord.synchronisedServers, HuskSyncBungeeCord.redisListener));
}
}
default -> sender.sendMessage(new MineDown("Error: Invalid argument for migration. Use \"husksync migrate\" to start the process").toComponent());
@@ -317,7 +317,7 @@ public class BungeeCommand extends Command implements TabExecutor, HuskSyncComma
// 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_ender_chest")).toComponent());
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_inventory")).toComponent());
return;
}
if (ProxyServer.getInstance().getPlayer(targetPlayerName) != null) {

View File

@@ -1,8 +1,8 @@
package me.william278.husksync.bungeecord.config;
package net.william278.husksync.bungeecord.config;
import me.william278.husksync.HuskSyncBungeeCord;
import me.william278.husksync.Settings;
import me.william278.husksync.util.MessageManager;
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;
@@ -42,6 +42,7 @@ public class ConfigLoader {
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) {
@@ -59,6 +60,8 @@ public class ConfigLoader {
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";

View File

@@ -1,7 +1,7 @@
package me.william278.husksync.bungeecord.config;
package net.william278.husksync.bungeecord.config;
import me.william278.husksync.HuskSyncBungeeCord;
import me.william278.husksync.Settings;
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;

View File

@@ -1,14 +1,15 @@
package me.william278.husksync.bungeecord.listener;
package net.william278.husksync.bungeecord.listener;
import me.william278.husksync.HuskSyncBungeeCord;
import me.william278.husksync.PlayerData;
import me.william278.husksync.Settings;
import me.william278.husksync.redis.RedisMessage;
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;
@@ -18,7 +19,7 @@ public class BungeeEventListener implements Listener {
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
@EventHandler
@EventHandler(priority = EventPriority.LOWEST)
public void onPostLogin(PostLoginEvent event) {
final ProxiedPlayer player = event.getPlayer();
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
@@ -26,7 +27,7 @@ public class BungeeEventListener implements Listener {
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());
final Map<Settings.SynchronisationCluster, PlayerData> data = HuskSyncBungeeCord.dataManager.getPlayerData(player.getUniqueId());
// Update the player's data from SQL onto the cache
assert data != null;

View File

@@ -1,14 +1,14 @@
package me.william278.husksync.bungeecord.listener;
package net.william278.husksync.bungeecord.listener;
import de.themoep.minedown.MineDown;
import me.william278.husksync.HuskSyncBungeeCord;
import me.william278.husksync.Server;
import me.william278.husksync.util.MessageManager;
import me.william278.husksync.PlayerData;
import me.william278.husksync.Settings;
import me.william278.husksync.migrator.MPDBMigrator;
import me.william278.husksync.redis.RedisListener;
import me.william278.husksync.redis.RedisMessage;
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;
@@ -24,6 +24,7 @@ public class BungeeRedisListener extends RedisListener {
// Initialize the listener on the bungee
public BungeeRedisListener() {
super();
listen();
}
@@ -62,40 +63,37 @@ public class BungeeRedisListener extends RedisListener {
}
switch (message.getMessageType()) {
case PLAYER_DATA_REQUEST -> {
case PLAYER_DATA_REQUEST -> ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
// Get the UUID of the requesting player
final UUID requestingPlayerUUID = UUID.fromString(message.getMessageData());
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
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();
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 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) {
if (player.isConnected()) {
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();
// 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());
}
});
}
case PLAYER_DATA_UPDATE -> {
} 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.getMessageData();
final String serializedPlayerData = message.getMessageDataElements()[0];
final boolean bounceBack = Boolean.parseBoolean(message.getMessageDataElements()[1]);
try {
playerData = (PlayerData) RedisMessage.deserialize(serializedPlayerData);
} catch (IOException | ClassNotFoundException e) {
@@ -113,10 +111,10 @@ public class BungeeRedisListener extends RedisListener {
}
// Reply with the player data if they are still online (switching server)
try {
ProxiedPlayer player = ProxyServer.getInstance().getPlayer(playerData.getPlayerUUID());
if (player != null) {
if (player.isConnected()) {
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)
@@ -125,12 +123,12 @@ public class BungeeRedisListener extends RedisListener {
// 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();
}
} 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
@@ -197,10 +195,9 @@ public class BungeeRedisListener extends RedisListener {
HuskSyncBungeeCord.dataManager));
}
}
case API_DATA_REQUEST -> {
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());
@@ -220,7 +217,7 @@ public class BungeeRedisListener extends RedisListener {
} catch (IOException e) {
plugin.getBungeeLogger().log(Level.SEVERE, "Failed to serialize PlayerData requested via the API");
}
}
});
}
}

View File

@@ -1,6 +1,6 @@
package me.william278.husksync.bungeecord.util;
package net.william278.husksync.bungeecord.util;
import me.william278.husksync.util.Logger;
import net.william278.husksync.util.Logger;
import java.util.logging.Level;

View File

@@ -1,7 +1,7 @@
package me.william278.husksync.bungeecord.util;
package net.william278.husksync.bungeecord.util;
import me.william278.husksync.HuskSyncBungeeCord;
import me.william278.husksync.util.UpdateChecker;
import net.william278.husksync.HuskSyncBungeeCord;
import net.william278.husksync.util.UpdateChecker;
import java.util.logging.Level;

View File

@@ -1,5 +1,5 @@
name: HuskSync
version: @version@
main: me.william278.husksync.HuskSyncBungeeCord
version: ${version}
main: net.william278.husksync.HuskSyncBungeeCord
author: William278
description: 'A modern, cross-server player data synchronization system'

View File

@@ -1,28 +1,11 @@
plugins {
id 'java'
}
dependencies {
implementation 'redis.clients:jedis:3.7.1'
implementation 'com.zaxxer:HikariCP:5.0.0'
}
import org.apache.tools.ant.filters.ReplaceTokens
task updateVersion(type: Copy) {
from('src/main/resources') {
include 'plugin.yml'
include 'bungee.yml'
}
into 'build/sources/resources/'
filter(ReplaceTokens, tokens: [version: '' + project.version])
}
processResources {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
dependsOn updateVersion
from 'build/sources/resources'
compileOnly 'com.zaxxer:HikariCP:5.0.1'
}
shadowJar {
dependsOn processResources
// Exclude some unnecessary files
exclude "**/module-info.class"
exclude "module-info.class"
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
}

View File

@@ -1,71 +0,0 @@
package me.william278.husksync.redis;
import me.william278.husksync.Settings;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisClientConfig;
import redis.clients.jedis.JedisPubSub;
import redis.clients.jedis.exceptions.JedisException;
import java.io.IOException;
import java.util.logging.Level;
public abstract class RedisListener {
/**
* Determines if the RedisListener is working properly
*/
public boolean isActiveAndEnabled;
/**
* Handle an incoming {@link RedisMessage}
*
* @param message The {@link RedisMessage} to handle
*/
public abstract void handleMessage(RedisMessage message);
/**
* Log to console
*
* @param level The {@link Level} to log
* @param message Message to log
*/
public abstract void log(Level level, String message);
/**
* Start the Redis listener
*/
public final void listen() {
try (Jedis jedis = new Jedis(Settings.redisHost, Settings.redisPort)) {
final String jedisPassword = Settings.redisPassword;
jedis.connect();
if (jedis.isConnected()) {
if (!jedisPassword.equals("")) {
jedis.auth(jedisPassword);
}
isActiveAndEnabled = true;
log(Level.INFO, "Enabled Redis listener successfully!");
new Thread(() -> jedis.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
// Only accept messages to the HuskSync channel
if (!channel.equals(RedisMessage.REDIS_CHANNEL)) {
return;
}
// Handle the message
try {
handleMessage(new RedisMessage(message));
} catch (IOException | ClassNotFoundException e) {
log(Level.SEVERE, "Failed to deserialize message target");
}
}
}, RedisMessage.REDIS_CHANNEL), "Redis Subscriber").start();
} else {
isActiveAndEnabled = false;
log(Level.SEVERE, "Failed to initialize the redis listener!");
}
} catch (JedisException e) {
log(Level.SEVERE, "Failed to establish a connection to the Redis server!");
}
}
}

View File

@@ -1,6 +1,7 @@
package me.william278.husksync;
package net.william278.husksync;
import java.io.*;
import java.time.Instant;
import java.util.UUID;
/**
@@ -18,6 +19,11 @@ public class PlayerData implements Serializable {
*/
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
*/
@@ -70,6 +76,7 @@ public class PlayerData implements Serializable {
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;
@@ -109,16 +116,17 @@ public class PlayerData implements Serializable {
* @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 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, String serializedInventory, String serializedEnderChest,
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;
@@ -172,6 +180,15 @@ public class PlayerData implements Serializable {
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
*
@@ -341,6 +358,7 @@ public class PlayerData implements Serializable {
*/
public void setSerializedInventory(String serializedInventory) {
this.serializedInventory = serializedInventory;
this.timestamp = Instant.now().getEpochSecond();
}
/**
@@ -350,6 +368,7 @@ public class PlayerData implements Serializable {
*/
public void setSerializedEnderChest(String serializedEnderChest) {
this.serializedEnderChest = serializedEnderChest;
this.timestamp = Instant.now().getEpochSecond();
}
/**
@@ -359,6 +378,7 @@ public class PlayerData implements Serializable {
*/
public void setHealth(double health) {
this.health = health;
this.timestamp = Instant.now().getEpochSecond();
}
/**
@@ -368,6 +388,7 @@ public class PlayerData implements Serializable {
*/
public void setMaxHealth(double maxHealth) {
this.maxHealth = maxHealth;
this.timestamp = Instant.now().getEpochSecond();
}
/**
@@ -377,6 +398,7 @@ public class PlayerData implements Serializable {
*/
public void setHealthScale(double healthScale) {
this.healthScale = healthScale;
this.timestamp = Instant.now().getEpochSecond();
}
/**
@@ -386,6 +408,7 @@ public class PlayerData implements Serializable {
*/
public void setHunger(int hunger) {
this.hunger = hunger;
this.timestamp = Instant.now().getEpochSecond();
}
/**
@@ -395,6 +418,7 @@ public class PlayerData implements Serializable {
*/
public void setSaturation(float saturation) {
this.saturation = saturation;
this.timestamp = Instant.now().getEpochSecond();
}
/**
@@ -404,6 +428,7 @@ public class PlayerData implements Serializable {
*/
public void setSaturationExhaustion(float saturationExhaustion) {
this.saturationExhaustion = saturationExhaustion;
this.timestamp = Instant.now().getEpochSecond();
}
/**
@@ -413,6 +438,7 @@ public class PlayerData implements Serializable {
*/
public void setSelectedSlot(int selectedSlot) {
this.selectedSlot = selectedSlot;
this.timestamp = Instant.now().getEpochSecond();
}
/**
@@ -422,6 +448,7 @@ public class PlayerData implements Serializable {
*/
public void setSerializedEffectData(String serializedEffectData) {
this.serializedEffectData = serializedEffectData;
this.timestamp = Instant.now().getEpochSecond();
}
/**
@@ -431,6 +458,7 @@ public class PlayerData implements Serializable {
*/
public void setTotalExperience(int totalExperience) {
this.totalExperience = totalExperience;
this.timestamp = Instant.now().getEpochSecond();
}
/**
@@ -440,6 +468,7 @@ public class PlayerData implements Serializable {
*/
public void setExpLevel(int expLevel) {
this.expLevel = expLevel;
this.timestamp = Instant.now().getEpochSecond();
}
/**
@@ -449,6 +478,7 @@ public class PlayerData implements Serializable {
*/
public void setExpProgress(float expProgress) {
this.expProgress = expProgress;
this.timestamp = Instant.now().getEpochSecond();
}
/**
@@ -458,6 +488,7 @@ public class PlayerData implements Serializable {
*/
public void setGameMode(String gameMode) {
this.gameMode = gameMode;
this.timestamp = Instant.now().getEpochSecond();
}
/**
@@ -467,6 +498,7 @@ public class PlayerData implements Serializable {
*/
public void setSerializedStatistics(String serializedStatistics) {
this.serializedStatistics = serializedStatistics;
this.timestamp = Instant.now().getEpochSecond();
}
/**
@@ -476,6 +508,7 @@ public class PlayerData implements Serializable {
*/
public void setFlying(boolean flying) {
isFlying = flying;
this.timestamp = Instant.now().getEpochSecond();
}
/**
@@ -485,6 +518,7 @@ public class PlayerData implements Serializable {
*/
public void setSerializedAdvancements(String serializedAdvancements) {
this.serializedAdvancements = serializedAdvancements;
this.timestamp = Instant.now().getEpochSecond();
}
/**
@@ -494,5 +528,6 @@ public class PlayerData implements Serializable {
*/
public void setSerializedLocation(String serializedLocation) {
this.serializedLocation = serializedLocation;
this.timestamp = Instant.now().getEpochSecond();
}
}

View File

@@ -1,4 +1,4 @@
package me.william278.husksync;
package net.william278.husksync;
import java.util.UUID;

View File

@@ -1,4 +1,4 @@
package me.william278.husksync;
package net.william278.husksync;
import java.util.ArrayList;
@@ -21,6 +21,7 @@ public class Settings {
public static String redisHost;
public static int redisPort;
public static String redisPassword;
public static boolean redisSSL;
/*
* Bungee / Proxy server-only settings
@@ -35,6 +36,9 @@ public class Settings {
// 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;
@@ -66,8 +70,8 @@ public class Settings {
public static boolean syncAdvancements;
public static boolean syncLocation;
public static boolean syncFlight;
// Future
public static long synchronizationTimeoutRetryDelay;
public static boolean saveOnWorldSave;
public static boolean useNativeImplementation;
// This Cluster ID

View File

@@ -1,13 +1,14 @@
package me.william278.husksync.migrator;
package net.william278.husksync.migrator;
import me.william278.husksync.PlayerData;
import me.william278.husksync.Server;
import me.william278.husksync.Settings;
import me.william278.husksync.proxy.data.DataManager;
import me.william278.husksync.proxy.data.sql.Database;
import me.william278.husksync.proxy.data.sql.MySQL;
import me.william278.husksync.redis.RedisMessage;
import me.william278.husksync.util.Logger;
import net.william278.husksync.PlayerData;
import net.william278.husksync.Server;
import net.william278.husksync.Settings;
import net.william278.husksync.proxy.data.DataManager;
import net.william278.husksync.proxy.data.sql.Database;
import net.william278.husksync.proxy.data.sql.MySQL;
import net.william278.husksync.redis.RedisListener;
import net.william278.husksync.redis.RedisMessage;
import net.william278.husksync.util.Logger;
import java.io.IOException;
import java.sql.Connection;
@@ -95,7 +96,7 @@ public class MPDBMigrator {
}
// Carry out the migration
public void executeMigrationOperations(DataManager dataManager, HashSet<Server> synchronisedServers) {
public void executeMigrationOperations(DataManager dataManager, HashSet<Server> synchronisedServers, RedisListener redisListener) {
// Prepare the target database for insertion
prepareTargetDatabase(dataManager);
@@ -109,7 +110,7 @@ public class MPDBMigrator {
getExperienceData();
// Send the encoded data to the Bukkit servers for conversion
sendEncodedData(synchronisedServers);
sendEncodedData(synchronisedServers, redisListener);
}
// Clear the new database out of current data
@@ -200,7 +201,7 @@ public class MPDBMigrator {
}
}
private void sendEncodedData(HashSet<Server> synchronisedServers) {
private void sendEncodedData(HashSet<Server> synchronisedServers, RedisListener redisListener) {
for (Server processingServer : synchronisedServers) {
if (processingServer.hasMySqlPlayerDataBridge()) {
for (MPDBPlayerData data : mpdbPlayerData) {

View File

@@ -1,4 +1,4 @@
package me.william278.husksync.migrator;
package net.william278.husksync.migrator;
import java.io.Serializable;
import java.util.UUID;

View File

@@ -1,4 +1,4 @@
package me.william278.husksync.proxy.command;
package net.william278.husksync.proxy.command;
public interface HuskSyncCommand {

View File

@@ -1,15 +1,14 @@
package me.william278.husksync.proxy.data;
package net.william278.husksync.proxy.data;
import me.william278.husksync.PlayerData;
import me.william278.husksync.Settings;
import me.william278.husksync.proxy.data.sql.Database;
import me.william278.husksync.proxy.data.sql.MySQL;
import me.william278.husksync.proxy.data.sql.SQLite;
import me.william278.husksync.util.Logger;
import net.william278.husksync.PlayerData;
import net.william278.husksync.Settings;
import net.william278.husksync.proxy.data.sql.Database;
import net.william278.husksync.proxy.data.sql.MySQL;
import net.william278.husksync.proxy.data.sql.SQLite;
import net.william278.husksync.util.Logger;
import java.io.File;
import java.sql.*;
import java.time.Instant;
import java.util.*;
import java.util.logging.Level;
@@ -185,7 +184,7 @@ public class DataManager {
ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
final UUID dataVersionUUID = UUID.fromString(resultSet.getString("version_uuid"));
//final Timestamp dataSaveTimestamp = resultSet.getTimestamp("timestamp");
final Timestamp dataSaveTimestamp = resultSet.getTimestamp("timestamp");
final String serializedInventory = resultSet.getString("inventory");
final String serializedEnderChest = resultSet.getString("ender_chest");
final double health = resultSet.getDouble("health");
@@ -205,10 +204,10 @@ public class DataManager {
final String serializedLocationData = resultSet.getString("location");
final String serializedStatisticData = resultSet.getString("statistics");
data.put(cluster, new PlayerData(playerUUID, dataVersionUUID, serializedInventory, serializedEnderChest,
health, maxHealth, healthScale, hunger, saturation, saturationExhaustion, selectedSlot, serializedStatusEffects,
totalExperience, expLevel, expProgress, gameMode, serializedStatisticData, isFlying,
serializedAdvancementData, serializedLocationData));
data.put(cluster, new PlayerData(playerUUID, dataVersionUUID, dataSaveTimestamp.toInstant().getEpochSecond(),
serializedInventory, serializedEnderChest, health, maxHealth, healthScale, hunger, saturation,
saturationExhaustion, selectedSlot, serializedStatusEffects, totalExperience, expLevel, expProgress,
gameMode, serializedStatisticData, isFlying, serializedAdvancementData, serializedLocationData));
} else {
data.put(cluster, PlayerData.DEFAULT_PLAYER_DATA(playerUUID));
}
@@ -240,7 +239,7 @@ public class DataManager {
try (PreparedStatement statement = connection.prepareStatement(
"UPDATE " + cluster.dataTableName() + " SET `version_uuid`=?, `timestamp`=?, `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`=? WHERE `player_id`=(SELECT `id` FROM " + cluster.playerTableName() + " WHERE `uuid`=?);")) {
statement.setString(1, playerData.getDataVersionUUID().toString());
statement.setTimestamp(2, new Timestamp(Instant.now().getEpochSecond()));
statement.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
statement.setString(3, playerData.getSerializedInventory());
statement.setString(4, playerData.getSerializedEnderChest());
statement.setDouble(5, playerData.getHealth()); // Health
@@ -274,7 +273,7 @@ public class DataManager {
"INSERT INTO " + cluster.dataTableName() + " (`player_id`,`version_uuid`,`timestamp`,`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`) VALUES((SELECT `id` FROM " + cluster.playerTableName() + " WHERE `uuid`=?),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);")) {
statement.setString(1, playerData.getPlayerUUID().toString());
statement.setString(2, playerData.getDataVersionUUID().toString());
statement.setTimestamp(3, new Timestamp(Instant.now().getEpochSecond()));
statement.setTimestamp(3, new Timestamp(System.currentTimeMillis()));
statement.setString(4, playerData.getSerializedInventory());
statement.setString(5, playerData.getSerializedEnderChest());
statement.setDouble(6, playerData.getHealth()); // Health

View File

@@ -1,7 +1,7 @@
package me.william278.husksync.proxy.data.sql;
package net.william278.husksync.proxy.data.sql;
import me.william278.husksync.Settings;
import me.william278.husksync.util.Logger;
import net.william278.husksync.Settings;
import net.william278.husksync.util.Logger;
import java.sql.Connection;
import java.sql.SQLException;

View File

@@ -1,8 +1,8 @@
package me.william278.husksync.proxy.data.sql;
package net.william278.husksync.proxy.data.sql;
import com.zaxxer.hikari.HikariDataSource;
import me.william278.husksync.Settings;
import me.william278.husksync.util.Logger;
import net.william278.husksync.Settings;
import net.william278.husksync.util.Logger;
import java.sql.Connection;
import java.sql.SQLException;

View File

@@ -1,8 +1,8 @@
package me.william278.husksync.proxy.data.sql;
package net.william278.husksync.proxy.data.sql;
import com.zaxxer.hikari.HikariDataSource;
import me.william278.husksync.Settings;
import me.william278.husksync.util.Logger;
import net.william278.husksync.Settings;
import net.william278.husksync.util.Logger;
import java.io.File;
import java.io.IOException;

View File

@@ -0,0 +1,126 @@
package net.william278.husksync.redis;
import net.william278.husksync.Settings;
import redis.clients.jedis.*;
import redis.clients.jedis.exceptions.JedisConnectionException;
import redis.clients.jedis.exceptions.JedisException;
import java.io.IOException;
import java.util.logging.Level;
public abstract class RedisListener {
/**
* Determines if the RedisListener is working properly
*/
public boolean isActiveAndEnabled;
/**
* Pool of connections to the Redis server
*/
private static JedisPool jedisPool;
/**
* Creates a new RedisListener and initialises the Redis connection
*/
public RedisListener() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(0);
config.setTestOnBorrow(true);
config.setTestOnReturn(true);
if (Settings.redisPassword.isEmpty()) {
jedisPool = new JedisPool(config,
Settings.redisHost,
Settings.redisPort,
0,
Settings.redisSSL);
} else {
jedisPool = new JedisPool(config,
Settings.redisHost,
Settings.redisPort,
0,
Settings.redisPassword,
Settings.redisSSL);
}
}
/**
* Handle an incoming {@link RedisMessage}
*
* @param message The {@link RedisMessage} to handle
*/
public abstract void handleMessage(RedisMessage message);
/**
* Log to console
*
* @param level The {@link Level} to log
* @param message Message to log
*/
public abstract void log(Level level, String message);
/**
* Fetch a connection to the Redis server from the JedisPool
*
* @return Jedis instance from the pool
*/
public static Jedis getJedisConnection() {
return jedisPool.getResource();
}
/**
* Start the Redis listener
*/
public final void listen() {
new Thread(() -> {
isActiveAndEnabled = true;
while (isActiveAndEnabled) {
Jedis subscriber;
if (Settings.redisPassword.isEmpty()) {
subscriber = new Jedis(Settings.redisHost,
Settings.redisPort,
0);
} else {
final JedisClientConfig config = DefaultJedisClientConfig.builder()
.password(Settings.redisPassword)
.timeoutMillis(0).build();
subscriber = new Jedis(Settings.redisHost,
Settings.redisPort,
config);
}
subscriber.connect();
log(Level.INFO, "Enabled Redis listener successfully!");
try {
subscriber.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
// Only accept messages to the HuskSync channel
if (!channel.equals(RedisMessage.REDIS_CHANNEL)) {
return;
}
// Handle the message
try {
handleMessage(new RedisMessage(message));
} catch (IOException | ClassNotFoundException e) {
log(Level.SEVERE, "Failed to deserialize message target");
}
}
}, RedisMessage.REDIS_CHANNEL);
} catch (JedisConnectionException connectionException) {
log(Level.SEVERE, "A connection exception occurred with the Jedis listener");
connectionException.printStackTrace();
} catch (JedisException jedisException) {
isActiveAndEnabled = false;
log(Level.SEVERE, "An exception occurred with the Jedis listener");
jedisException.printStackTrace();
} finally {
subscriber.close();
}
}
}, "Redis Subscriber").start();
}
}

View File

@@ -1,7 +1,7 @@
package me.william278.husksync.redis;
package net.william278.husksync.redis;
import me.william278.husksync.PlayerData;
import me.william278.husksync.Settings;
import net.william278.husksync.PlayerData;
import net.william278.husksync.Settings;
import redis.clients.jedis.Jedis;
import java.io.*;
@@ -22,8 +22,9 @@ public class RedisMessage {
/**
* Create a new RedisMessage
* @param type The type of the message
* @param target Who will receive this message
*
* @param type The type of the message
* @param target Who will receive this message
* @param messageData The message data elements
*/
public RedisMessage(MessageType type, MessageTarget target, String... messageData) {
@@ -38,6 +39,7 @@ public class RedisMessage {
/**
* Get a new RedisMessage from an incoming message string
*
* @param messageString The message string to parse
*/
public RedisMessage(String messageString) throws IOException, ClassNotFoundException {
@@ -49,6 +51,7 @@ public class RedisMessage {
/**
* Returns the full, formatted message string with type, target & data
*
* @return The fully formatted message
*/
private String getFullMessage() throws IOException {
@@ -61,21 +64,18 @@ public class RedisMessage {
* Send the redis message
*/
public void send() throws IOException {
try (Jedis publisher = new Jedis(Settings.redisHost, Settings.redisPort)) {
final String jedisPassword = Settings.redisPassword;
publisher.connect();
if (!jedisPassword.equals("")) {
publisher.auth(jedisPassword);
}
publisher.publish(REDIS_CHANNEL, getFullMessage());
}
try (Jedis publisher = RedisListener.getJedisConnection()) {
publisher.publish(REDIS_CHANNEL, getFullMessage());
}
}
public String getMessageData() {
return messageData;
}
public String[] getMessageDataElements() { return messageData.split(MESSAGE_DATA_SEPARATOR); }
public String[] getMessageDataElements() {
return messageData.split(MESSAGE_DATA_SEPARATOR);
}
public MessageType getMessageType() {
return messageType;
@@ -173,7 +173,9 @@ public class RedisMessage {
/**
* A record that defines the target of a plugin message; a spigot server or the proxy server(s). For Bukkit servers, the name of the server must also be specified
*/
public record MessageTarget(Settings.ServerType targetServerType, UUID targetPlayerUUID, String targetClusterId) implements Serializable { }
public record MessageTarget(Settings.ServerType targetServerType, UUID targetPlayerUUID,
String targetClusterId) implements Serializable {
}
/**
* Deserialize an object from a Base64 string

View File

@@ -1,4 +1,4 @@
package me.william278.husksync.util;
package net.william278.husksync.util;
import java.util.logging.Level;

View File

@@ -1,4 +1,4 @@
package me.william278.husksync.util;
package net.william278.husksync.util;
import java.util.HashMap;
@@ -16,14 +16,15 @@ public class MessageManager {
public static StringBuilder PLUGIN_INFORMATION = new StringBuilder().append("[HuskSync](#00fb9a bold) [| %proxy_brand% Version %proxy_version% (%bukkit_brand% v%bukkit_version%)](#00fb9a)\n")
.append("[%plugin_description%](gray)\n")
.append("[• Author:](white) [William278](gray show_text=&7Click to pay a visit open_url=https://youtube.com/William27528)\n")
.append("[• Author:](white) [William278](gray show_text=&7Click to visit website open_url=https://william278.net)\n")
.append("[• Contributors:](white) [HarvelsX](gray show_text=&7Code)\n")
.append("[• Translators:](white) [Namiu/うにたろう](gray show_text=&7Japanese, ja-jp), [anchelthe](gray show_text=&7Spanish, es-es), [Ceddix](gray show_text=&7German, de-de), [小蔡](gray show_text=&7Traditional Chinese, zh-tw), [Ghost-chu](gray show_text=&7Simplified Chinese, zh-cn), [Thourgard](gray show_text=&7Ukrainian, uk-ua)\n")
.append("[• Plugin Info:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/)\n")
.append("[• Report Issues:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/issues)\n")
.append("[• Support Discord:](white) [[Link]](#00fb9a show_text=&7Click to join open_url=https://discord.gg/tVYhJfyDWG)");
public static StringBuilder PLUGIN_STATUS = new StringBuilder().append("[HuskSync](#00fb9a bold) [| Current system status:](#00fb9a)\n")
.append("[• Connected servers:](white) [%1%](#00fb9a)")
.append("[• Connected servers:](white) [%1%](#00fb9a)\n")
.append("[• Cached player data:](white) [%2%](#00fb9a)");
}

View File

@@ -1,4 +1,4 @@
package me.william278.husksync.util;
package net.william278.husksync.util;
public interface ThrowSupplier<T> {
T get() throws Exception;

View File

@@ -1,4 +1,4 @@
package me.william278.husksync.util;
package net.william278.husksync.util;
import java.io.BufferedReader;
import java.io.IOException;
@@ -11,38 +11,35 @@ public abstract class UpdateChecker {
private final static int SPIGOT_PROJECT_ID = 97144;
private final String currentVersion;
private String latestVersion;
private final VersionUtils.Version currentVersion;
private VersionUtils.Version latestVersion;
public UpdateChecker(String currentVersion) {
this.currentVersion = currentVersion;
this.currentVersion = VersionUtils.Version.of(currentVersion);
try {
final URL url = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + SPIGOT_PROJECT_ID);
URLConnection urlConnection = url.openConnection();
this.latestVersion = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())).readLine();
this.latestVersion = VersionUtils.Version.of(new BufferedReader(new InputStreamReader(urlConnection.getInputStream())).readLine());
} catch (IOException e) {
log(Level.WARNING, "Failed to check for updates: An IOException occurred.");
this.latestVersion = "Unknown";
this.latestVersion = new VersionUtils.Version();
} catch (Exception e) {
log(Level.WARNING, "Failed to check for updates: An exception occurred.");
this.latestVersion = "Unknown";
this.latestVersion = new VersionUtils.Version();
}
}
public boolean isUpToDate() {
if (latestVersion.equalsIgnoreCase("Unknown")) {
return true;
}
return latestVersion.equals(currentVersion);
return this.currentVersion.compareTo(latestVersion) >= 0;
}
public String getLatestVersion() {
return latestVersion;
return latestVersion.toString();
}
public String getCurrentVersion() {
return currentVersion;
return currentVersion.toString();
}
public abstract void log(Level level, String message);

View File

@@ -0,0 +1,61 @@
package net.william278.husksync.util;
import java.util.Arrays;
public class VersionUtils {
private final static char META_SEPARATOR = '+';
private final static String VERSION_SEPARATOR = "\\.";
public static class Version implements Comparable<Version> {
public int[] versions = new int[]{};
public String metadata = "";
public Version() {
}
public Version(String version) {
this.parse(version);
}
public static Version of(String version) {
return new Version(version);
}
private void parse(String version) {
int metaIndex = version.indexOf(META_SEPARATOR);
if (metaIndex > 0) {
this.metadata = version.substring(metaIndex + 1);
version = version.substring(0, metaIndex);
}
String[] versions = version.split(VERSION_SEPARATOR);
this.versions = Arrays.stream(versions).mapToInt(Integer::parseInt).toArray();
}
@Override
public int compareTo(Version version) {
int length = Math.max(this.versions.length, version.versions.length);
for (int i = 0; i < length; i++) {
int a = i < this.versions.length ? this.versions[i] : 0;
int b = i < version.versions.length ? version.versions[i] : 0;
if (a < b) return -1;
if (a > b) return 1;
}
return 0;
}
@Override
public String toString() {
StringBuilder stringBuffer = new StringBuilder();
for (int version : this.versions) {
stringBuffer.append(version).append('.');
}
stringBuffer.deleteCharAt(stringBuffer.length() - 1);
return stringBuffer.append('+').append(this.metadata).toString();
}
}
}

View File

@@ -0,0 +1,14 @@
synchronisation_complete: '[Daten synchronisiert!](#00fb9a)'
viewing_inventory_of: '[Einsicht in das Inventar von](#00fb9a) [%1%](#00fb9a bold)'
viewing_ender_chest_of: '[Einsicht in die Endertruhe von](#00fb9a) [%1%](#00fb9a bold)'
reload_complete: '[HuskSync](#00fb9a bold) [| Die Konfigurations- und Meldungsdateien wurden aktualisiert.](#00fb9a)'
error_invalid_syntax: '[Fehler:](#ff3300) [Falsche Syntax. Nutze: %1%](#ff7e5e)'
error_invalid_player: '[Fehler:](#ff3300) [Dieser Spieler konnte nicht gefunden werden](#ff7e5e)'
error_no_permission: '[Fehler:](#ff3300) [Du hast nicht die benötigten Berechtigungen um diesen Befehl auszuführen](#ff7e5e)'
error_cannot_view_inventory_online: '[Fehler:](#ff3300) [Du kannst nicht über HuskSync auf das Inventar eines Online-Spielers zugreifen](#ff7e5e)'
error_cannot_view_ender_chest_online: '[Fehler:](#ff3300) [Du kannst nicht über HuskSync auf die Endertruhe eines Online-Spielers zugreifen](#ff7e5e)'
error_cannot_view_own_inventory: '[Fehler:](#ff3300) [Du kannst nicht auf dein eigenes Inventar zugreifen!](#ff7e5e)'
error_cannot_view_own_ender_chest: '[Fehler:](#ff3300) [Du kannst nicht auf deine eigene Endertruhe zugreifen!](#ff7e5e)'
error_console_command_only: '[Fehler:](#ff3300) [Dieser Befehl kann nur über die %1% Konsole ausgeführt werden](#ff7e5e)'
error_no_servers_proxied: '[Fehler:](#ff3300) [Vorgang konnte nicht verarbeitet werden; Es sind keine Server online, auf denen HuskSync installiert ist. Bitte stelle sicher, dass HuskSync sowohl auf dem Proxy-Server als auch auf allen Servern installiert ist, zwischen denen du Daten synchronisieren möchtest.](#ff7e5e)'
error_invalid_cluster: '[Fehler:](#ff3300) [Bitte gib die ID eines gültigen Clusters an.](#ff7e5e)'

View File

@@ -0,0 +1,14 @@
synchronisation_complete: '[Datos sincronizados!](#00fb9a)'
viewing_inventory_of: '[Viendo el inventario de](#00fb9a) [%1%](#00fb9a bold)'
viewing_ender_chest_of: '[Viendo el Ender Chest de](#00fb9a) [%1%](#00fb9a bold)'
reload_complete: '[HuskSync](#00fb9a bold) [| Se ha reiniciado la configuración y los archivos de los mensajes.](#00fb9a)'
error_invalid_syntax: '[Error:](#ff3300) [Sintaxis incorrecta. Uso: %1%](#ff7e5e)'
error_invalid_player: '[Error:](#ff3300) [No se ha podido encontrar a ese jugador](#ff7e5e)'
error_no_permission: '[Error:](#ff3300) [No tienes permiso para ejecutar este comando](#ff7e5e)'
error_cannot_view_inventory_online: '[Error:](#ff3300) [A traves de HuskSync no puedes acceder al inventario de un jugador conectado](#ff7e5e)'
error_cannot_view_ender_chest_online: '[Error:](#ff3300) [A traves de HuskSync no puedes acceder al Ender Chest de un jugador conectado.](#ff7e5e)'
error_cannot_view_own_inventory: '[Error:](#ff3300) [No puedes acceder a tu inventario!](#ff7e5e)'
error_cannot_view_own_ender_chest: '[Error:](#ff3300) [No puedes acceder a tu Ender Chest!](#ff7e5e)'
error_console_command_only: '[Error:](#ff3300) [Ese comando solo puede ser ejecutado desde la %1% consola](#ff7e5e)'
error_no_servers_proxied: '[Error:](#ff3300) [Ha ocurrido un error mientras se procesaba la acción; no hay servidores online con HusckSync instalado. Por favor, asegúrate que HuskSync está instalado tanto en el proxy como en todos los servidores entre los que quieres sincronizar datos.](#ff7e5e)'
error_invalid_cluster: '[Error:](#ff3300) [Por favor, especifica la ID de un cluster válido.](#ff7e5e)'

View File

@@ -0,0 +1,14 @@
synchronisation_complete: '[データが同期されました!](#00fb9a)'
viewing_inventory_of: '[%1%](#00fb9a bold) [のインベントリを表示します](#00fb9a) '
viewing_ender_chest_of: '[%1%](#00fb9a bold) [のエンダーチェストを表示します](#00fb9a) '
reload_complete: '[HuskSync](#00fb9a bold) [| 設定ファイルとメッセージファイルを再読み込みしました。](#00fb9a)'
error_invalid_syntax: '[Error:](#ff3300) [構文が正しくありません。使用法: %1%](#ff7e5e)'
error_invalid_player: '[Error:](#ff3300) [そのプレイヤーは見つかりませんでした](#ff7e5e)'
error_no_permission: '[Error:](#ff3300) [このコマンドを実行する権限がありません](#ff7e5e)'
error_cannot_view_inventory_online: '[Error:](#ff3300) [HuskSyncからオンラインプレイヤーのインベントリにはアクセスできません](#ff7e5e)'
error_cannot_view_ender_chest_online: '[Error:](#ff3300) [HuskSyncからオンラインプレイヤーのエンダーチェストにはアクセスできません](#ff7e5e)'
error_cannot_view_own_inventory: '[Error:](#ff3300) [自分のインベントリにはアクセスできません!](#ff7e5e)'
error_cannot_view_own_ender_chest: '[Error:](#ff3300) [自分のエンダーチェストにはアクセスできません!](#ff7e5e)'
error_console_command_only: '[Error:](#ff3300) [そのコマンドは%1%コンソールからのみ実行できます](#ff7e5e)'
error_no_servers_proxied: '[Error:](#ff3300) [操作の処理に失敗; HuskSyncがインストールされているサーバーがオンラインになっていません。プロキシサーバーとデータを同期させたいすべてのサーバーにHuskSyncがインストールされていることを確認してください。](#ff7e5e)'
error_invalid_cluster: '[Error:](#ff3300) [有効なクラスターのIDを指定してください。](#ff7e5e)'

View File

@@ -0,0 +1,14 @@
synchronisation_complete: '[Дані синхронізовано!](#00fb9a)'
viewing_inventory_of: '[Переглядання інвентару](#00fb9a) [%1%](#00fb9a bold)'
viewing_ender_chest_of: '[Переглядання скрині енду](#00fb9a) [%1%](#00fb9a bold)'
reload_complete: '[HuskSync](#00fb9a bold) [| Перезавантажено конфіґ та файли повідомлень.](#00fb9a)'
error_invalid_syntax: '[Помилка:](#ff3300) [Неправильний синтакс. Використання: %1%](#ff7e5e)'
error_invalid_player: '[Помилка:](#ff3300) [Гравця не знайдено](#ff7e5e)'
error_no_permission: '[Помилка:](#ff3300) [Ввас немає дозволу на використання цієї команди](#ff7e5e)'
error_cannot_view_inventory_online: '[Помилка:](#ff3300) [Ви не можете переглядати інвентар гравців, що знаходяться онлайн, з допомогую HuskSync](#ff7e5e)'
error_cannot_view_ender_chest_online: '[Помилка:](#ff3300) [Ви не можете переглядати скриню енду гравців, що знаходяться онлайн, з допомогую HuskSync](#ff7e5e)'
error_cannot_view_own_inventory: '[Помилка:](#ff3300) [Ви не можете переглядати власний інвентар!](#ff7e5e)'
error_cannot_view_own_ender_chest: '[Помилка:](#ff3300) [Ви не можете переглядати власну скриню енду!](#ff7e5e)'
error_console_command_only: '[Помилка:](#ff3300) [Ця команда може бути використана лише з допомогою %1% консолі](#ff7e5e)'
error_no_servers_proxied: '[Помилка:](#ff3300) [Не вдалося опрацювати операцію; не знайдено жодного сервера із встановленим HuskSync. Запевніться, будьласка, що HuskSync встановлено на Проксі та усіх серверах між якими ви хочете синхроніхувати дані.](#ff7e5e)'
error_invalid_cluster: '[Помилка:](#ff3300) [Зазнчте будь ласка ID слушного кластеру.](#ff7e5e)'

View File

@@ -0,0 +1,14 @@
synchronisation_complete: '[数据同步完成](#00fb9a)'
viewing_inventory_of: '[查看玩家背包:](#00fb9a) [%1%](#00fb9a bold)'
viewing_ender_chest_of: '[查看玩家末影箱:](#00fb9a) [%1%](#00fb9a bold)'
reload_complete: '[HuskSync](#00fb9a bold) [| 配置与语言文件重载完成.](#00fb9a)'
error_invalid_syntax: '[错误:](#ff3300) [格式错误. 使用方法: %1%](#ff7e5e)'
error_invalid_player: '[错误:](#ff3300) [未找到目标玩家](#ff7e5e)'
error_no_permission: '[错误:](#ff3300) [你没有权限执行此命令](#ff7e5e)'
error_cannot_view_inventory_online: '[错误:](#ff3300) [你不能在玩家在线时通过 HuskSync 查看与编辑玩家物品栏](#ff7e5e)'
error_cannot_view_ender_chest_online: '[错误:](#ff3300) [你不能在玩家在线时通过 HuskSync 查看与编辑玩家末影箱](#ff7e5e)'
error_cannot_view_own_inventory: '[错误:](#ff3300) [你不能查看和编辑自己的物品栏!](#ff7e5e)'
error_cannot_view_own_ender_chest: '[错误:](#ff3300) [你不能查看和编辑自己的末影箱!](#ff7e5e)'
error_console_command_only: '[错误:](#ff3300) [该命令只能由 %1% 控制台执行](#ff7e5e)'
error_no_servers_proxied: '[错误:](#ff3300) [操作处理失败; 没有任何安装了 HuskSync 的后端服务器在线. 请确认 HuskSync 已在 BungeeCord/Velocity 等代理服务器和所有你希望互相同步数据的后端服务器间安装.](#ff7e5e)'
error_invalid_cluster: '[错误:](#ff3300) [请指定一个有效的集群(cluster) ID.](#ff7e5e)'

View File

@@ -0,0 +1,14 @@
synchronisation_complete: '[資料已同步!!](#00fb9a)'
viewing_inventory_of: '[查看](#00fb9a) [%1%](#00fb9a bold) [的背包](#00fb9a)'
viewing_ender_chest_of: '[查看](#00fb9a) [%1%](#00fb9a bold) [的終界箱](#00fb9a)'
reload_complete: '[HuskSync](#00fb9a bold) [| 已重新載入配置和訊息文件](#00fb9a)'
error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法: %1%](#ff7e5e)'
error_invalid_player: '[錯誤:](#ff3300) [找不到這位玩家](#ff7e5e)'
error_no_permission: '[錯誤:](#ff3300) [您沒有權限執行這個指令](#ff7e5e)'
error_cannot_view_inventory_online: '[錯誤:](#ff3300) [您無法通過 HuskSync 查看在線玩家的背包](#ff7e5e)'
error_cannot_view_ender_chest_online: '[錯誤:](#ff3300) [您無法通過 HuskSync 查看在線玩家的終界箱](#ff7e5e)'
error_cannot_view_own_inventory: '[錯誤:](#ff3300) [您無法查看自己的背包!](#ff7e5e)'
error_cannot_view_own_ender_chest: '[錯誤:](#ff3300) [你無法查看自己的終界箱!](#ff7e5e)'
error_console_command_only: '[錯誤:](#ff3300) [該指令只能通過 %1% 控制台運行](#ff7e5e)'
error_no_servers_proxied: '[錯誤:](#ff3300) [處理操作失敗: 沒有安裝 HuskSync 的伺服器在線。 請確保在 Proxy 伺服器和您希望在其他同步數據的所有伺服器上都安裝了 HuskSync。](#ff7e5e)'
error_invalid_cluster: '[錯誤:](#ff3300) [請提供有效的 Cluster ID](#ff7e5e)'

View File

@@ -3,6 +3,7 @@ redis_settings:
host: 'localhost'
port: 6379
password: ''
use_ssl: false
data_storage_settings:
database_type: 'sqlite'
mysql_settings:
@@ -18,6 +19,7 @@ data_storage_settings:
maximum_lifetime: 1800000
keepalive_time: 0
connection_timeout: 5000
bounce_back_synchronization: true
clusters:
main:
player_table: 'husksync_players'

View File

@@ -1 +1,3 @@
javaVersion=16
javaVersion=16
plugin_version=1.4
plugin_archive=husksync

View File

@@ -6,7 +6,8 @@ before_install:
- 'chmod +x gradlew'
- 'chmod +x ./.jitpack/ensure-java-16'
- 'bash ./.jitpack/ensure-java-16 install'
- 'bash ./.jitpack/install-mpdb-converter'
install:
- 'if ! ./.jitpack/ensure-java-16 use; then source ~/.sdkman/bin/sdkman-init.sh; fi'
- 'java -version'
- './gradlew api:clean api:publishToMavenLocal'
- './gradlew clean publishToMavenLocal'

View File

@@ -1,30 +1,28 @@
//file:noinspection GroovyAssignabilityCheck
plugins {
id 'maven-publish'
}
dependencies {
implementation project(path: ":common", configuration: 'shadow')
implementation project(path: ":api", configuration: 'shadow')
implementation project(path: ":bukkit", configuration: 'shadow')
implementation project(path: ":bungeecord", configuration: 'shadow')
implementation project(path: ":velocity", configuration: 'shadow')
implementation project(path: ':bukkit', configuration: 'shadow')
implementation project(path: ':api', configuration: 'shadow')
implementation project(path: ':bungeecord', configuration: 'shadow')
implementation project(path: ':velocity', configuration: 'shadow')
}
shadowJar {
// Relocations
relocate 'redis.clients', 'me.William278.husksync.libraries.jedis'
destinationDirectory.set(file("$rootDir/target/"))
archiveBaseName.set('HuskSync')
archiveClassifier.set('')
build {
dependsOn tasks.named("shadowJar")
dependencies {
exclude dependency(':jedis')
exclude dependency(':commons-pool2')
}
}
publishing {
publications {
mavenJava(MavenPublication) {
groupId = 'me.William278'
artifactId = 'HuskSync-plugin'
version = "$project.version"
groupId = 'net.william278'
artifactId = 'husksync-plugin'
version = "$rootProject.version"
artifact shadowJar
}

View File

@@ -1,21 +1,24 @@
dependencies {
compileOnly project(':common')
implementation project(path: ':common', configuration: 'shadow')
implementation project(path: ':common')
compileOnly 'redis.clients:jedis:3.7.1'
implementation 'org.bstats:bstats-velocity:2.2.1'
implementation 'com.zaxxer:HikariCP:5.0.1'
implementation 'org.bstats:bstats-velocity:3.0.0'
implementation 'de.themoep:minedown-adventure:1.7.1-SNAPSHOT'
implementation 'net.byteflux:libby-velocity:1.1.5'
compileOnly 'com.velocitypowered:velocity-api:3.1.0'
annotationProcessor 'com.velocitypowered:velocity-api:3.1.0'
}
shadowJar {
relocate 'com.zaxxer', 'me.William278.husksync.libraries.hikari'
relocate 'org.bstats', 'me.William278.husksync.libraries.bstats.velocity'
relocate 'de.themoep', 'me.William278.husksync.libraries.minedown.adventure'
relocate 'net.byteflux', 'me.William278.husksync.libraries.libby.velocity'
}
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'
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
tasks.register('prepareKotlinBuildScriptModel'){}
dependencies {
//noinspection GroovyAssignabilityCheck
exclude dependency(':slf4j-api')
}
}

View File

@@ -1,4 +1,4 @@
package me.william278.husksync;
package net.william278.husksync;
import com.google.inject.Inject;
import com.velocitypowered.api.command.CommandManager;
@@ -7,18 +7,21 @@ import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
import me.william278.husksync.migrator.MPDBMigrator;
import me.william278.husksync.proxy.data.DataManager;
import me.william278.husksync.redis.RedisMessage;
import me.william278.husksync.velocity.command.VelocityCommand;
import me.william278.husksync.velocity.config.ConfigLoader;
import me.william278.husksync.velocity.config.ConfigManager;
import me.william278.husksync.velocity.listener.VelocityEventListener;
import me.william278.husksync.velocity.listener.VelocityRedisListener;
import me.william278.husksync.velocity.util.VelocityLogger;
import me.william278.husksync.velocity.util.VelocityUpdateChecker;
import net.william278.husksync.Server;
import net.william278.husksync.Settings;
import net.william278.husksync.migrator.MPDBMigrator;
import net.william278.husksync.proxy.data.DataManager;
import net.william278.husksync.redis.RedisMessage;
import net.william278.husksync.velocity.command.VelocityCommand;
import net.william278.husksync.velocity.config.ConfigLoader;
import net.william278.husksync.velocity.config.ConfigManager;
import net.william278.husksync.velocity.listener.VelocityEventListener;
import net.william278.husksync.velocity.listener.VelocityRedisListener;
import net.william278.husksync.velocity.util.VelocityLogger;
import net.william278.husksync.velocity.util.VelocityUpdateChecker;
import net.byteflux.libby.Library;
import net.byteflux.libby.VelocityLibraryManager;
import org.bstats.velocity.Metrics;
@@ -31,19 +34,11 @@ import java.util.HashSet;
import java.util.Objects;
import java.util.logging.Level;
import static me.william278.husksync.HuskSyncVelocity.VERSION;
@Plugin(
id = "husksync",
name = "HuskSync",
version = VERSION,
description = "HuskSync for velocity",
authors = {"William278"}
)
@Plugin(id = "husksync")
public class HuskSyncVelocity {
// Plugin version
public static final String VERSION = "1.3";
public static String VERSION = null;
// Velocity bStats ID (different from Bukkit and BungeeCord)
private static final int METRICS_ID = 13489;
@@ -68,6 +63,8 @@ public class HuskSyncVelocity {
public static DataManager dataManager;
public static VelocityRedisListener redisListener;
public static MPDBMigrator mpdbMigrator;
private final Logger logger;
@@ -92,11 +89,13 @@ public class HuskSyncVelocity {
}
@Inject
public HuskSyncVelocity(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory, Metrics.Factory metricsFactory) {
public HuskSyncVelocity(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory, Metrics.Factory metricsFactory, PluginContainer pluginContainer) {
this.server = server;
this.logger = logger;
this.dataDirectory = dataDirectory;
this.metricsFactory = metricsFactory;
pluginContainer.getDescription().getVersion().ifPresent(s -> VERSION = s);
}
@Subscribe
@@ -145,10 +144,7 @@ public class HuskSyncVelocity {
}
// Initialize the redis listener
if (!new VelocityRedisListener().isActiveAndEnabled) {
getVelocityLogger().severe("Failed to initialize Redis; HuskSync will now abort loading itself (Velocity) v" + VERSION);
return;
}
redisListener = new VelocityRedisListener();
// Register listener
server.getEventManager().register(this, new VelocityEventListener());
@@ -208,7 +204,7 @@ public class HuskSyncVelocity {
Library mySqlLib = Library.builder()
.groupId("mysql")
.artifactId("mysql-connector-java")
.version("8.0.27")
.version("8.0.29")
.build();
Library sqLiteLib = Library.builder()

View File

@@ -1,20 +1,20 @@
package me.william278.husksync.velocity.command;
package net.william278.husksync.velocity.command;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.command.SimpleCommand;
import com.velocitypowered.api.proxy.Player;
import de.themoep.minedown.adventure.MineDown;
import me.william278.husksync.HuskSyncVelocity;
import me.william278.husksync.PlayerData;
import me.william278.husksync.Server;
import me.william278.husksync.Settings;
import me.william278.husksync.migrator.MPDBMigrator;
import me.william278.husksync.proxy.command.HuskSyncCommand;
import me.william278.husksync.redis.RedisMessage;
import me.william278.husksync.util.MessageManager;
import me.william278.husksync.velocity.util.VelocityUpdateChecker;
import me.william278.husksync.velocity.config.ConfigLoader;
import me.william278.husksync.velocity.config.ConfigManager;
import net.william278.husksync.HuskSyncVelocity;
import net.william278.husksync.PlayerData;
import net.william278.husksync.Server;
import net.william278.husksync.Settings;
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.william278.husksync.velocity.util.VelocityUpdateChecker;
import net.william278.husksync.velocity.config.ConfigLoader;
import net.william278.husksync.velocity.config.ConfigManager;
import java.io.IOException;
import java.util.*;
@@ -294,7 +294,7 @@ public class VelocityCommand implements SimpleCommand, HuskSyncCommand {
HuskSyncVelocity.synchronisedServers)) {
plugin.getProxyServer().getScheduler().buildTask(plugin, () ->
HuskSyncVelocity.mpdbMigrator.executeMigrationOperations(HuskSyncVelocity.dataManager,
HuskSyncVelocity.synchronisedServers)).schedule();
HuskSyncVelocity.synchronisedServers, HuskSyncVelocity.redisListener)).schedule();
}
}
default -> sender.sendMessage(new MineDown("Error: Invalid argument for migration. Use \"husksync migrate\" to start the process").toComponent());
@@ -310,7 +310,7 @@ public class VelocityCommand implements SimpleCommand, HuskSyncCommand {
// View the inventory of a player specified by their name
private void openInventory(Player viewer, String targetPlayerName, String clusterId) {
if (viewer.getUsername().equalsIgnoreCase(targetPlayerName)) {
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_ender_chest")).toComponent());
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_inventory")).toComponent());
return;
}
if (plugin.getProxyServer().getPlayer(targetPlayerName).isPresent()) {

View File

@@ -1,8 +1,8 @@
package me.william278.husksync.velocity.config;
package net.william278.husksync.velocity.config;
import me.william278.husksync.HuskSyncVelocity;
import me.william278.husksync.Settings;
import me.william278.husksync.util.MessageManager;
import net.william278.husksync.HuskSyncVelocity;
import net.william278.husksync.Settings;
import net.william278.husksync.util.MessageManager;
import ninja.leaping.configurate.ConfigurationNode;
import java.util.HashMap;
@@ -31,25 +31,24 @@ public class ConfigLoader {
}
private static String getConfigString(ConfigurationNode rootNode, String defaultValue, String... nodePath) {
return !rootNode.getNode((Object[]) nodePath).isVirtual() ? rootNode.getNode((Object[])nodePath).getString() : defaultValue;
return !rootNode.getNode((Object[]) nodePath).isVirtual() ? rootNode.getNode((Object[]) nodePath).getString() : defaultValue;
}
@SuppressWarnings("SameParameterValue")
private static boolean getConfigBoolean(ConfigurationNode rootNode, boolean defaultValue, String... nodePath) {
return !rootNode.getNode((Object[]) nodePath).isVirtual() ? rootNode.getNode((Object[])nodePath).getBoolean() : defaultValue;
return !rootNode.getNode((Object[]) nodePath).isVirtual() ? rootNode.getNode((Object[]) nodePath).getBoolean() : defaultValue;
}
private static int getConfigInt(ConfigurationNode rootNode, int defaultValue, String... nodePath) {
return !rootNode.getNode((Object[]) nodePath).isVirtual() ? rootNode.getNode((Object[])nodePath).getInt() : defaultValue;
return !rootNode.getNode((Object[]) nodePath).isVirtual() ? rootNode.getNode((Object[]) nodePath).getInt() : defaultValue;
}
private static long getConfigLong(ConfigurationNode rootNode, long defaultValue, String... nodePath) {
return !rootNode.getNode((Object[])nodePath).isVirtual() ? rootNode.getNode((Object[])nodePath).getLong() : defaultValue;
return !rootNode.getNode((Object[]) nodePath).isVirtual() ? rootNode.getNode((Object[]) nodePath).getLong() : defaultValue;
}
public static void loadSettings(ConfigurationNode loadedConfig) throws IllegalArgumentException {
ConfigurationNode config = copyDefaults(loadedConfig);
//ConfigurationNode config = copyDefaults(loadedConfig);
Settings.language = getConfigString(config, "en-gb", "language");
@@ -58,6 +57,7 @@ public class ConfigLoader {
Settings.redisHost = getConfigString(config, "localhost", "redis_settings", "host");
Settings.redisPort = getConfigInt(config, 6379, "redis_settings", "port");
Settings.redisPassword = getConfigString(config, "", "redis_settings", "password");
Settings.redisSSL = getConfigBoolean(config, false, "redis_settings", "use_ssl");
Settings.dataStorageType = Settings.DataStorageType.valueOf(getConfigString(config, "sqlite", "data_storage_settings", "database_type").toUpperCase());
if (Settings.dataStorageType == Settings.DataStorageType.MYSQL) {
@@ -75,6 +75,8 @@ public class ConfigLoader {
Settings.hikariKeepAliveTime = getConfigLong(config, 0, "data_storage_settings", "hikari_pool_settings", "keepalive_time");
Settings.hikariConnectionTimeOut = getConfigLong(config, 5000, "data_storage_settings", "hikari_pool_settings", "connection_timeout");
Settings.bounceBackSynchronisation = getConfigBoolean(config, true,"bounce_back_synchronization");
// Read cluster data
ConfigurationNode clusterSection = config.getNode("clusters");
final String settingDatabaseName = Settings.mySQLDatabase != null ? Settings.mySQLDatabase : "HuskSync";

View File

@@ -1,7 +1,7 @@
package me.william278.husksync.velocity.config;
package net.william278.husksync.velocity.config;
import me.william278.husksync.HuskSyncVelocity;
import me.william278.husksync.Settings;
import net.william278.husksync.HuskSyncVelocity;
import net.william278.husksync.Settings;
import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.yaml.YAMLConfigurationLoader;
import org.yaml.snakeyaml.DumperOptions;

View File

@@ -1,12 +1,13 @@
package me.william278.husksync.velocity.listener;
package net.william278.husksync.velocity.listener;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.PostLoginEvent;
import com.velocitypowered.api.proxy.Player;
import me.william278.husksync.HuskSyncVelocity;
import me.william278.husksync.PlayerData;
import me.william278.husksync.Settings;
import me.william278.husksync.redis.RedisMessage;
import net.william278.husksync.HuskSyncVelocity;
import net.william278.husksync.PlayerData;
import net.william278.husksync.Settings;
import net.william278.husksync.redis.RedisMessage;
import java.io.IOException;
import java.util.Map;
@@ -16,7 +17,7 @@ public class VelocityEventListener {
private static final HuskSyncVelocity plugin = HuskSyncVelocity.getInstance();
@Subscribe
@Subscribe(order = PostOrder.FIRST)
public void onPostLogin(PostLoginEvent event) {
final Player player = event.getPlayer();
plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {

View File

@@ -1,15 +1,15 @@
package me.william278.husksync.velocity.listener;
package net.william278.husksync.velocity.listener;
import com.velocitypowered.api.proxy.Player;
import de.themoep.minedown.adventure.MineDown;
import me.william278.husksync.HuskSyncVelocity;
import me.william278.husksync.PlayerData;
import me.william278.husksync.Server;
import me.william278.husksync.Settings;
import me.william278.husksync.migrator.MPDBMigrator;
import me.william278.husksync.redis.RedisListener;
import me.william278.husksync.redis.RedisMessage;
import me.william278.husksync.util.MessageManager;
import net.william278.husksync.HuskSyncVelocity;
import net.william278.husksync.PlayerData;
import net.william278.husksync.Server;
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.william278.husksync.util.MessageManager;
import java.io.IOException;
import java.util.Objects;
@@ -23,6 +23,7 @@ public class VelocityRedisListener extends RedisListener {
// Initialize the listener on the bungee
public VelocityRedisListener() {
super();
listen();
}
@@ -61,36 +62,35 @@ public class VelocityRedisListener extends RedisListener {
}
switch (message.getMessageType()) {
case PLAYER_DATA_REQUEST -> {
case PLAYER_DATA_REQUEST -> plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
// Get the UUID of the requesting player
final UUID requestingPlayerUUID = UUID.fromString(message.getMessageData());
plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
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();
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 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
Optional<Player> player = plugin.getProxyServer().getPlayer(requestingPlayerUUID);
player.ifPresent(value -> value.sendActionBar(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();
}
}).schedule();
}
case PLAYER_DATA_UPDATE -> {
// Send synchronisation complete message
Optional<Player> player = plugin.getProxyServer().getPlayer(requestingPlayerUUID);
player.ifPresent(value -> value.sendActionBar(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();
}
}).schedule();
case PLAYER_DATA_UPDATE -> plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
// Deserialize the PlayerData received
PlayerData playerData;
final String serializedPlayerData = message.getMessageData();
final String serializedPlayerData = message.getMessageDataElements()[0];
final boolean bounceBack = Boolean.parseBoolean(message.getMessageDataElements()[1]);
try {
playerData = (PlayerData) RedisMessage.deserialize(serializedPlayerData);
} catch (IOException | ClassNotFoundException e) {
@@ -108,23 +108,24 @@ public class VelocityRedisListener extends RedisListener {
}
// Reply with the player data if they are still online (switching server)
Optional<Player> updatingPlayer = plugin.getProxyServer().getPlayer(playerData.getPlayerUUID());
updatingPlayer.ifPresent(player -> {
try {
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, playerData.getPlayerUUID(), message.getMessageTarget().targetClusterId()),
RedisMessage.serialize(playerData))
.send();
if (Settings.bounceBackSynchronisation && bounceBack) {
Optional<Player> updatingPlayer = plugin.getProxyServer().getPlayer(playerData.getPlayerUUID());
updatingPlayer.ifPresent(player -> {
try {
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, playerData.getPlayerUUID(), message.getMessageTarget().targetClusterId()),
RedisMessage.serialize(playerData))
.send();
// Send synchronisation complete message
player.sendActionBar(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();
}
});
}
// Send synchronisation complete message
player.sendActionBar(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();
}
});
}
}).schedule();
case CONNECTION_HANDSHAKE -> {
// Reply to a Bukkit server's connection handshake to complete the process
if (HuskSyncVelocity.isDisabling) return; // Return if the Proxy is disabling
@@ -190,7 +191,7 @@ public class VelocityRedisListener extends RedisListener {
migrator.loadIncomingData(migrator.incomingPlayerData, HuskSyncVelocity.dataManager);
}
}
case API_DATA_REQUEST -> {
case API_DATA_REQUEST -> plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
final UUID playerUUID = UUID.fromString(message.getMessageDataElements()[0]);
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[1]);
@@ -213,7 +214,7 @@ public class VelocityRedisListener extends RedisListener {
} catch (IOException e) {
plugin.getVelocityLogger().log(Level.SEVERE, "Failed to serialize PlayerData requested via the API");
}
}
}).schedule();
}
}

View File

@@ -1,6 +1,6 @@
package me.william278.husksync.velocity.util;
package net.william278.husksync.velocity.util;
import me.william278.husksync.util.Logger;
import net.william278.husksync.util.Logger;
import java.util.logging.Level;

View File

@@ -1,7 +1,7 @@
package me.william278.husksync.velocity.util;
package net.william278.husksync.velocity.util;
import me.william278.husksync.HuskSyncVelocity;
import me.william278.husksync.util.UpdateChecker;
import net.william278.husksync.HuskSyncVelocity;
import net.william278.husksync.util.UpdateChecker;
import java.util.logging.Level;

View File

@@ -0,0 +1,12 @@
{
"id": "husksync",
"name": "HuskSync",
"version": "${version}",
"description": "A modern, cross-server player data synchronization system",
"url": "https://william278.net",
"authors": [
"William278"
],
"dependencies": [],
"main": "net.william278.husksync.HuskSyncVelocity"
}