Initial project setup, building, setup planning, readme & redis

feat/data-edit-commands
William 3 years ago
parent 53bdad288a
commit 2a7371be31

3
.gitignore vendored

@ -106,7 +106,7 @@ build/
# Ignore Gradle GUI config
gradle-app.setting
# Cache of project
# me.william278.crossserversync.bungeecord.PlayerDataCache of project
.gradletasknamecache
**/build/
@ -117,3 +117,4 @@ run/
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
/build-output-final/
/target/

@ -0,0 +1,20 @@
# CrossServerSync
**CrossServerSync** is a robust solution for synchronising player data (inventories, health, hunger & status effects) between servers. It was designed as a lightweight alternative to MySQLPlayerDataBridge,
### Installation
Install CrossServerSync in the `/plugins/` folder of your Spigot (and derivatives) servers and Proxy (BungeeCord and derivatives) server.
Start your servers, then stop them again to allow the configuration files to generate.
Navigate to the generated config.yml files on your Spigot server and Proxy (located in `/plugins/CrossServerSync/`) and fill in the credentials of your redis server. On the Proxy server, you can additionally configure a MySQL database to save player data in, as by default the plugin will create a SQLite database for this.
If you have multiple proxy servers (i.e. via RedisBungee), you need to install the plugin on all of them and make use of the MySQL option and ensure the proxies are using the same database.
### How it works
![Flow chart showing different processes of how the plugin works](images/flow-chart.png)
CrossServerSync synchronises player data between servers using Redis to transfer cached data, loaded from a central database as necessary.
### Building
To build CrossServerSync, run the following in the root of the repository:
```
./gradlew clean build
```

@ -1,28 +1,40 @@
defaultTasks ':bukkit:createFinalJar'
buildscript {
repositories {
mavenCentral()
}
}
subprojects {
apply plugin: 'java-library'
apply plugin: 'maven-publish'
plugins {
id 'com.github.johnrengelman.shadow' version '7.1.0' apply false
id 'java'
}
group = 'me.William278'
project.version = '0.1'
project.properties.put('description', 'Synchronize data cross-server')
allprojects {
group 'me.William278'
version '0.1'
sourceCompatibility = 16
targetCompatibility = 16
compileJava { options.encoding = 'UTF-8' }
tasks.withType(JavaCompile) { options.encoding = 'UTF-8' }
javadoc { options.encoding = 'UTF-8' }
}
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
tasks.withType(Javadoc) {
options.encoding = 'UTF-8'
logger.lifecycle('Building CrossServerSync v' + version.toString())
subprojects {
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'java'
apply plugin: 'maven-publish'
compileJava {
options.release = 16
}
repositories {
mavenLocal()
}
tasks.withType(Copy).all {
duplicatesStrategy 'include'
mavenCentral()
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
maven { url 'https://repo.minebench.de/' }
maven { url 'https://repo.codemc.org/repository/maven-public' }
maven { url 'https://jitpack.io' }
}
}

@ -1,54 +1,18 @@
plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '7.1.0'
}
repositories {
mavenCentral()
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
maven { url 'https://repo.minebench.de/' }
maven { url 'https://repo.codemc.org/repository/maven-public' }
maven { url 'https://jitpack.io' }
}
dependencies {
api project(':common')
implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
compileOnly project(':common')
implementation project(path: ':common', configuration: 'shadow')
implementation 'org.bstats:bstats-bukkit:2.2.1'
implementation 'com.zaxxer:HikariCP:5.0.0'
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
}
implementation 'redis.clients:jedis:3.7.0'
processResources {
def props = [version: project.version]
inputs.properties props
expand props
filteringCharset 'UTF-8'
filesMatching('bungee.yml') {
expand props
}
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
}
shadowJar {
relocate 'org.bstats', 'me.william278.shaded.org.bukkit.bstats'
relocate 'de.themoep.minedown', 'me.william278.shaded.de.themeop.minedown'
dependencies {
include(dependency(':common'))
}
}
artifacts {
archives shadowJar
}
tasks.build.dependsOn tasks.shadowJar
task createFinalJar(type: Copy) {
dependsOn(tasks.build)
from file("build/libs/bukkit-${project.version}-all.jar")
into file("../build-output-final")
rename 'bukkit-', 'CrossServerSync-'
rename '-all', ''
relocate 'redis.clients', 'me.William278.crossserversync.libraries.jedis'
relocate 'org.bstats', 'me.William278.crossserversync.libraries.plan'
relocate 'org.apache.commons', 'me.William278.crossserversync.libraries.apache-commons'
relocate 'org.slf4j', 'me.William278.crossserversync.libraries.slf4j'
}
task prepareKotlinBuildScriptModel {}
tasks.register('prepareKotlinBuildScriptModel'){}

@ -1,17 +0,0 @@
package me.william278.crossserversync;
import org.bukkit.plugin.java.JavaPlugin;
public final class CrossServerSyncSpigot extends JavaPlugin {
@Override
public void onEnable() {
// Plugin startup logic
}
@Override
public void onDisable() {
// Plugin shutdown logic
}
}

@ -0,0 +1,50 @@
package me.william278.crossserversync.bukkit;
import me.william278.crossserversync.Settings;
import me.william278.crossserversync.redis.RedisListener;
import me.william278.crossserversync.redis.RedisMessage;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.logging.Level;
public class BukkitRedisListener extends RedisListener {
private static final CrossServerSyncBukkit plugin = CrossServerSyncBukkit.getInstance();
// Initialize the listener on the bukkit server
public BukkitRedisListener() {
listen();
}
/**
* Handle an incoming {@link RedisMessage}
*
* @param message The {@link RedisMessage} to handle
*/
@Override
public void handleMessage(RedisMessage message) {
// Ignore messages for proxy servers
if (message.getMessageTarget().targetServerType() != Settings.ServerType.BUKKIT) {
return;
}
// Handle the message for the player
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.getUniqueId() == message.getMessageTarget().targetPlayerName()) {
return;
}
}
}
/**
* Log to console
*
* @param level The {@link Level} to log
* @param message Message to log
*/
@Override
public void log(Level level, String message) {
plugin.getLogger().log(level, message);
}
}

@ -0,0 +1,37 @@
package me.william278.crossserversync.bukkit;
import me.william278.crossserversync.bukkit.config.ConfigLoader;
import org.bukkit.plugin.java.JavaPlugin;
public final class CrossServerSyncBukkit extends JavaPlugin {
private static CrossServerSyncBukkit instance;
public static CrossServerSyncBukkit getInstance() {
return instance;
}
@Override
public void onLoad() {
instance = this;
}
@Override
public void onEnable() {
// Plugin startup logic
// Load the config file
getConfig().options().copyDefaults(true);
saveDefaultConfig();
saveConfig();
reloadConfig();
ConfigLoader.loadSettings(getConfig());
// Initialize the redis listener
new BukkitRedisListener();
}
@Override
public void onDisable() {
// Plugin shutdown logic
}
}

@ -0,0 +1,137 @@
package me.william278.crossserversync.bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Map;
/**
* Class for serializing and deserializing player inventories and Ender Chests contents ({@link ItemStack[]}) as base64 strings.
* Based on https://gist.github.com/graywolf336/8153678 by graywolf336
* Modified for 1.16 via https://gist.github.com/graywolf336/8153678#gistcomment-3551376 by efindus
*
* @author efindus
* @author graywolf336
*/
public final class InventorySerializer {
/**
* Converts the player inventory to a Base64 encoded string.
*
* @param player whose inventory will be turned into an array of strings.
* @return string with serialized Inventory
* @throws IllegalStateException in the event the item stacks cannot be saved
*/
public static String getSerializedInventoryContents(Player player) throws IllegalStateException {
// This contains contents, armor and offhand (contents are indexes 0 - 35, armor 36 - 39, offhand - 40)
return itemStackArrayToBase64(player.getInventory().getContents());
}
/**
* Converts the player inventory to a Base64 encoded string.
*
* @param player whose Ender Chest will be turned into an array of strings.
* @return string with serialized Ender Chest
* @throws IllegalStateException in the event the item stacks cannot be saved
*/
public static String getSerializedEnderChestContents(Player player) throws IllegalStateException {
// This contains all slots (0-27) in the player's Ender Chest
return itemStackArrayToBase64(player.getEnderChest().getContents());
}
/**
* Sets a player's inventory from a set of {@link ItemStack}s
*
* @param player The player to set the inventory of
* @param items The array of {@link ItemStack}s to set
*/
public static void setPlayerItems(Player player, ItemStack[] items) {
setInventoryItems(player.getInventory(), items);
}
/**
* Sets a player's ender chest from a set of {@link ItemStack}s
*
* @param player The player to set the inventory of
* @param items The array of {@link ItemStack}s to set
*/
public static void setPlayerEnderChest(Player player, ItemStack[] items) {
setInventoryItems(player.getEnderChest(), items);
}
// Clears, then fills an inventory's items correctly.
private static void setInventoryItems(Inventory inventory, ItemStack[] items) {
inventory.clear();
int index = 0;
for (ItemStack item : items) {
if (item != null) {
inventory.setItem(index, item);
}
index++;
}
}
/**
* A method to serialize an {@link ItemStack} array to Base64 String.
*
* @param items to turn into a Base64 String.
* @return Base64 string of the items.
* @throws IllegalStateException in the event the item stacks cannot be saved
*/
public static String itemStackArrayToBase64(ItemStack[] items) throws IllegalStateException {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream)) {
dataOutput.writeInt(items.length);
for (ItemStack item : items) {
if (item != null) {
dataOutput.writeObject(item.serialize());
} else {
dataOutput.writeObject(null);
}
}
}
return Base64Coder.encodeLines(outputStream.toByteArray());
} catch (Exception e) {
throw new IllegalStateException("Unable to save item stacks.", e);
}
}
/**
* Gets an array of ItemStacks from Base64 string.
*
* @param data Base64 string to convert to ItemStack array.
* @return ItemStack array created from the Base64 string.
* @throws IOException in the event the class type cannot be decoded
*/
public static ItemStack[] itemStackArrayFromBase64(String data) throws IOException {
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(data))) {
BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream);
ItemStack[] items = new ItemStack[dataInput.readInt()];
for (int Index = 0; Index < items.length; Index++) {
@SuppressWarnings("unchecked") // Ignore the unchecked cast here
Map<String, Object> stack = (Map<String, Object>) dataInput.readObject();
if (stack != null) {
items[Index] = ItemStack.deserialize(stack);
} else {
items[Index] = null;
}
}
return items;
} catch (ClassNotFoundException e) {
throw new IOException("Unable to decode class type.", e);
}
}
}

@ -0,0 +1,15 @@
package me.william278.crossserversync.bukkit.config;
import me.william278.crossserversync.Settings;
import org.bukkit.configuration.file.FileConfiguration;
public class ConfigLoader {
public static void loadSettings(FileConfiguration config) throws IllegalArgumentException {
Settings.serverType = Settings.ServerType.BUKKIT;
Settings.redisHost = config.getString("redis_settings.host", "localhost");
Settings.redisPort = config.getInt("redis_settings.port", 6379);
Settings.redisPassword = config.getString("redis_settings.password", "");
}
}

@ -0,0 +1,4 @@
redis_settings:
host: 'localhost'
port: 6379
password: ''

@ -1,6 +0,0 @@
name: CrossServerSync
version: '${version}'
main: me.william278.crossserversync.CrossServerSyncSpigot
api-version: 1.16
author: William278
description: '${properties.description}'

@ -1,39 +1,17 @@
plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '7.1.0'
}
repositories {
mavenCentral()
maven { url = 'https://oss.sonatype.org/content/groups/public/' }
}
dependencies {
api project(':common')
compileOnly 'net.md-5:bungeecord-api:1.16-R0.4'
}
compileOnly project(':common')
implementation project(path: ':common', configuration: 'shadow')
shadowJar {
relocate 'org.bstats', 'me.william278.shaded.org.bukkit.bstats'
relocate 'de.themoep.minedown', 'me.william278.shaded.de.themeop.minedown'
dependencies {
include(dependency(':common'))
}
}
implementation 'redis.clients:jedis:3.7.0'
artifacts {
archives shadowJar
compileOnly 'net.md-5:bungeecord-api:1.16-R0.5-SNAPSHOT'
}
processResources {
def props = [version: project.version]
inputs.properties props
expand props
filteringCharset 'UTF-8'
filesMatching('bungee.yml') {
expand props
}
shadowJar {
relocate 'redis.clients', 'me.William278.crossserversync.libraries.jedis'
relocate 'org.bstats', 'me.William278.crossserversync.libraries.plan'
relocate 'org.apache.commons', 'me.William278.crossserversync.libraries.apache-commons'
relocate 'org.slf4j', 'me.William278.crossserversync.libraries.slf4j'
}
tasks.build.dependsOn tasks.shadowJar
tasks.register('prepareKotlinBuildScriptModel'){}

@ -1,16 +0,0 @@
package me.william278.crossserversync;
import net.md_5.bungee.api.plugin.Plugin;
public final class CrossServerSyncBungeeCord extends Plugin {
@Override
public void onEnable() {
// Plugin startup logic
}
@Override
public void onDisable() {
// Plugin shutdown logic
}
}

@ -0,0 +1,41 @@
package me.william278.crossserversync.bungeecord;
import me.william278.crossserversync.Settings;
import me.william278.crossserversync.redis.RedisListener;
import me.william278.crossserversync.redis.RedisMessage;
import java.util.logging.Level;
public class BungeeRedisListener extends RedisListener {
private static final CrossServerSyncBungeeCord plugin = CrossServerSyncBungeeCord.getInstance();
// Initialize the listener on the bungee
public BungeeRedisListener() {
listen();
}
/**
* Handle an incoming {@link RedisMessage}
*
* @param message The {@link RedisMessage} to handle
*/
@Override
public void handleMessage(RedisMessage message) {
// Ignore messages destined for Bukkit servers
if (message.getMessageTarget().targetServerType() != Settings.ServerType.BUNGEECORD) {
return;
}
}
/**
* Log to console
*
* @param level The {@link Level} to log
* @param message Message to log
*/
@Override
public void log(Level level, String message) {
plugin.getLogger().log(level, message);
}
}

@ -0,0 +1,44 @@
package me.william278.crossserversync.bungeecord;
import me.william278.crossserversync.bungeecord.config.ConfigLoader;
import me.william278.crossserversync.bungeecord.config.ConfigManager;
import net.md_5.bungee.api.plugin.Plugin;
import java.util.Objects;
public final class CrossServerSyncBungeeCord extends Plugin {
private static CrossServerSyncBungeeCord instance;
public static CrossServerSyncBungeeCord getInstance() {
return instance;
}
public PlayerDataCache cache;
@Override
public void onLoad() {
instance = this;
}
@Override
public void onEnable() {
// Plugin startup logic
// Load config
ConfigManager.loadConfig();
// Load settings from config
ConfigLoader.loadSettings(Objects.requireNonNull(ConfigManager.getConfig()));
// Setup player data cache
cache = new PlayerDataCache();
// Initialize the redis listener
new BungeeRedisListener();
}
@Override
public void onDisable() {
// Plugin shutdown logic
}
}

@ -0,0 +1,51 @@
package me.william278.crossserversync.bungeecord;
import me.william278.crossserversync.PlayerData;
import java.util.HashSet;
import java.util.UUID;
public class PlayerDataCache {
// The cached player data
public HashSet<PlayerData> playerData;
public PlayerDataCache() {
playerData = new HashSet<>();
}
/**
* Update ar add data for a player to the cache
* @param newData The player's new/updated {@link PlayerData}
*/
public void updatePlayer(PlayerData newData) {
// Remove the old data if it exists
PlayerData oldData = null;
for (PlayerData data : playerData) {
if (data.getPlayerUUID() == newData.getPlayerUUID()) {
oldData = data;
}
}
if (oldData != null) {
playerData.remove(oldData);
}
// Add the new data
playerData.add(newData);
}
/**
* Get a player's {@link PlayerData} by their {@link UUID}
* @param playerUUID The {@link UUID} of the player to check
* @return The player's {@link PlayerData}
*/
public PlayerData getPlayer(UUID playerUUID) {
for (PlayerData data : playerData) {
if (data.getPlayerUUID() == playerUUID) {
return data;
}
}
return null;
}
}

@ -0,0 +1,32 @@
package me.william278.crossserversync.bungeecord.config;
import me.william278.crossserversync.Settings;
import net.md_5.bungee.config.Configuration;
public class ConfigLoader {
public static void loadSettings(Configuration config) throws IllegalArgumentException {
Settings.serverType = Settings.ServerType.BUNGEECORD;
Settings.redisHost = config.getString("redis_settings.host", "localhost");
Settings.redisPort = config.getInt("redis_settings.port", 6379);
Settings.redisPassword = config.getString("redis_settings.password", "");
Settings.dataStorageType = Settings.DataStorageType.valueOf(config.getString("data_storage_settings.database_type", "sqlite").toUpperCase());
if (Settings.dataStorageType == Settings.DataStorageType.MYSQL) {
Settings.mySQLHost = config.getString("data_storage_settings.mysql_settings.host", "localhost");
Settings.mySQLPort = config.getInt("data_storage_settings.mysql_settings.port", 8123);
Settings.mySQLDatabase = config.getString("data_storage_settings.mysql_settings.database", "CrossServerSync");
Settings.mySQLUsername = config.getString("data_storage_settings.mysql_settings.username", "CrossServerSync");
Settings.mySQLPassword = config.getString("data_storage_settings.mysql_settings.password", "CrossServerSync");
Settings.mySQLParams = config.getString("data_storage_settings.mysql_settings.params", "CrossServerSync");
}
Settings.hikariMaximumPoolSize = config.getInt("data_storage_settings.hikari_pool_settings.maximum_pool_size", 10);
Settings.hikariMinimumIdle = config.getInt("data_storage_settings.hikari_pool_settings.minimum_idle", 10);
Settings.hikariMaximumLifetime = config.getLong("data_storage_settings.hikari_pool_settings.maximum_lifetime", 1800000);
Settings.hikariKeepAliveTime = config.getLong("data_storage_settings.hikari_pool_settings.keepalive_time", 10);
Settings.hikariConnectionTimeOut = config.getLong("data_storage_settings.hikari_pool_settings.connection_timeout", 5000);
}
}

@ -0,0 +1,44 @@
package me.william278.crossserversync.bungeecord.config;
import me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord;
import net.md_5.bungee.config.Configuration;
import net.md_5.bungee.config.ConfigurationProvider;
import net.md_5.bungee.config.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.logging.Level;
public class ConfigManager {
private static final CrossServerSyncBungeeCord plugin = CrossServerSyncBungeeCord.getInstance();
public static void loadConfig() {
try {
if (!plugin.getDataFolder().exists()) {
if (plugin.getDataFolder().mkdir()) {
plugin.getLogger().info("Created CrossServerSync data folder");
}
}
File configFile = new File(plugin.getDataFolder(), "config.yml");
if (!configFile.exists()) {
Files.copy(plugin.getResourceAsStream("bungee_config.yml"), configFile.toPath());
}
} catch (Exception e) {
plugin.getLogger().log(Level.CONFIG, "An exception occurred loading the configuration file", e);
}
}
public static Configuration getConfig() {
try {
File configFile = new File(plugin.getDataFolder(), "config.yml");
return ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile);
} catch (IOException e) {
plugin.getLogger().log(Level.CONFIG, "An IOException occurred fetching the configuration file", e);
return null;
}
}
}

@ -0,0 +1,19 @@
redis_settings:
host: 'localhost'
port: 6379
password: ''
data_storage_settings:
database_type: 'sqlite'
mysql_settings:
host: 'localhost'
port: 8123
database: 'CrossServerSync'
username: 'root'
password: 'pa55w0rd'
params: ''
hikari_pool_settings:
maximum_pool_size: 10
minimum_idle: 10
maximum_lifetime: 1800000
keepalive_time: 0
connection_timeout: 5000

@ -1,4 +0,0 @@
name: CrossServerSync
version: '${version}'
main: me.william278.crossserversync.CrossServerSyncBungeeCord
description: '${properties.description}'

@ -1,16 +1,32 @@
plugins {
id 'java'
dependencies {
implementation 'redis.clients:jedis:3.7.0'
}
dependencies {
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
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])
}
repositories {
mavenCentral()
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
processResources {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
dependsOn updateVersion
from 'build/sources/resources'
}
java {
withJavadocJar()
shadowJar {
dependsOn processResources
// Exclude some unnecessary files
exclude "**/module-info.class"
exclude "module-info.class"
// Relocations
relocate 'redis.clients', 'me.William278.crossserversync.libraries.jedis'
relocate 'org.apache.commons', 'me.William278.crossserversync.libraries.apache-commons'
relocate 'org.slf4j', 'me.William278.crossserversync.libraries.slf4j'
}

@ -0,0 +1,57 @@
package me.william278.crossserversync;
import java.io.Serializable;
import java.util.UUID;
public class PlayerData implements Serializable {
/**
* The UUID of the player who this data belongs to
*/
private final UUID playerUUID;
/**
* The unique version UUID of this data
*/
private final UUID dataVersionUUID;
/**
* Serialized inventory data
*/
private final String serializedInventory;
/**
* Serialized ender chest data
*/
private final String serializedEnderChest;
//todo add more stuff, like ender chest, player health, max health, hunger and status effects, et cetera
/**
* Create a new PlayerData object; a random data version UUID will be selected.
* @param playerUUID The UUID of the player
* @param serializedInventory The player's serialized inventory data
*/
public PlayerData(UUID playerUUID, String serializedInventory, String serializedEnderChest) {
this.dataVersionUUID = UUID.randomUUID();
this.playerUUID = playerUUID;
this.serializedInventory = serializedInventory;
this.serializedEnderChest = serializedEnderChest;
}
public UUID getPlayerUUID() {
return playerUUID;
}
public UUID getDataVersionUUID() {
return dataVersionUUID;
}
public String getSerializedInventory() {
return serializedInventory;
}
public String getSerializedEnderChest() {
return serializedEnderChest;
}
}

@ -0,0 +1,52 @@
package me.william278.crossserversync;
public class Settings {
/*
* General settings
*/
// The type of THIS server (Bungee or Bukkit)
public static ServerType serverType;
// Redis settings
public static String redisHost;
public static int redisPort;
public static String redisPassword;
/*
* Bungee / Proxy server-only settings
*/
// SQL settings
public static DataStorageType dataStorageType;
// MySQL specific settings
public static String mySQLHost;
public static String mySQLDatabase;
public static String mySQLUsername;
public static String mySQLPassword;
public static int mySQLPort;
public static String mySQLParams;
// Hikari connection pooling settings
public static int hikariMaximumPoolSize;
public static int hikariMinimumIdle;
public static long hikariMaximumLifetime;
public static long hikariKeepAliveTime;
public static long hikariConnectionTimeOut;
/*
* Enum definitions
*/
public enum ServerType {
BUKKIT,
BUNGEECORD
}
public enum DataStorageType {
MYSQL,
SQLITE
}
}

@ -0,0 +1,52 @@
package me.william278.crossserversync.redis;
import me.william278.crossserversync.Settings;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;
import java.util.logging.Level;
public abstract class RedisListener {
/**
* 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() {
Jedis jedis = new Jedis(Settings.redisHost, Settings.redisPort);
final String jedisPassword = Settings.redisPassword;
if (!jedisPassword.equals("")) {
jedis.auth(jedisPassword);
}
jedis.connect();
if (jedis.isConnected()) {
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 CrossServerSync channel
if (!channel.equals(RedisMessage.REDIS_CHANNEL)) {
return;
}
// Handle the message
handleMessage(new RedisMessage(message));
}
}, RedisMessage.REDIS_CHANNEL), "Redis Subscriber").start();
} else {
log(Level.SEVERE, "Failed to initialize the redis listener!");
}
}
}

@ -0,0 +1,117 @@
package me.william278.crossserversync.redis;
import me.william278.crossserversync.Settings;
import redis.clients.jedis.Jedis;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.StringJoiner;
import java.util.UUID;
public class RedisMessage {
public static String REDIS_CHANNEL = "CrossServerSync";
public static String MESSAGE_META_SEPARATOR = "♦";
public static String MESSAGE_DATA_SEPARATOR = "♣";
private final String messageData;
private final MessageType messageType;
private MessageTarget messageTarget;
/**
* Create a new RedisMessage
* @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) {
final StringJoiner messageDataJoiner = new StringJoiner(MESSAGE_DATA_SEPARATOR);
for (String dataElement : messageData) {
messageDataJoiner.add(dataElement);
}
this.messageData = messageDataJoiner.toString();
this.messageType = type;
this.messageTarget = target;
}
/**
* Get a new RedisMessage from an incoming message string
* @param messageString The message string to parse
*/
public RedisMessage(String messageString) {
String[] messageMetaElements = messageString.split(MESSAGE_META_SEPARATOR);
messageType = MessageType.valueOf(messageMetaElements[0]);
messageData = messageMetaElements[2];
try (ObjectInputStream stream = new ObjectInputStream(new ByteArrayInputStream(messageMetaElements[1].getBytes()))) {
messageTarget = (MessageTarget) stream.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* Returns the full, formatted message string with type, target & data
* @return The fully formatted message
*/
private String getFullMessage() {
return new StringJoiner(MESSAGE_META_SEPARATOR)
.add(messageType.toString()).add(messageTarget.toString()).add(messageData)
.toString();
}
/**
* Send the redis message
*/
public void send() {
try (Jedis publisher = new Jedis(Settings.redisHost, Settings.redisPort)) {
final String jedisPassword = Settings.redisPassword;
if (!jedisPassword.equals("")) {
publisher.auth(jedisPassword);
}
publisher.connect();
publisher.publish(REDIS_CHANNEL, getFullMessage());
}
}
public String getMessageData() {
return messageData;
}
public MessageType getMessageType() {
return messageType;
}
public MessageTarget getMessageTarget() {
return messageTarget;
}
/**
* Defines the type of the message
*/
public enum MessageType {
/**
* Sent by Bukkit servers to proxy when a player disconnects with a player's updated data, alongside the UUID of the last loaded {@link me.william278.crossserversync.PlayerData} for the user
*/
PLAYER_DATA_UPDATE,
/**
* Sent by Bukkit servers to proxy to request {@link me.william278.crossserversync.PlayerData} from the proxy.
*/
PLAYER_DATA_REQUEST,
/**
* Sent by the Proxy to reply to a {@code MessageType.PLAYER_DATA_REQUEST}, contains the latest {@link me.william278.crossserversync.PlayerData} for the requester.
*/
PLAYER_DATA_REPLY
}
/**
* 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 targetPlayerName) implements Serializable { }
}

@ -0,0 +1,5 @@
name: CrossServerSync
version: @version@
main: me.william278.crossserversync.bungeecord.CrossServerSyncBungeeCord
author: William278
description: 'Synchronize data cross-server'

@ -0,0 +1,6 @@
name: CrossServerSync
version: @version@
main: me.william278.crossserversync.bukkit.CrossServerSyncBukkit
api-version: 1.16
author: William278
description: 'Synchronize data cross-server'

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

@ -0,0 +1,27 @@
dependencies {
implementation project(path: ":common", configuration: 'shadow')
implementation project(path: ":bukkit", configuration: 'shadow')
implementation project(path: ":bungeecord", configuration: 'shadow')
}
shadowJar {
destinationDirectory.set(file("$rootDir/target/"))
archiveBaseName.set('CrossServerSync')
archiveClassifier.set('')
build {
dependsOn tasks.named("shadowJar")
}
}
publishing {
publications {
mavenJava(MavenPublication) {
groupId = 'me.William278'
artifactId = 'CrossServerSync-plugin'
version = "$project.version"
artifact shadowJar
}
}
}

@ -1,2 +1,12 @@
pluginManagement {
repositories {
gradlePluginPortal()
}
}
rootProject.name = 'CrossServerSync'
include 'common', 'bukkit', 'bungeecord'
include 'common'
include 'bukkit'
include 'bungeecord'
include 'plugin'
Loading…
Cancel
Save