diff --git a/api/src/main/java/net/william278/husksync/bukkit/api/HuskSyncAPI.java b/api/src/main/java/net/william278/husksync/bukkit/api/HuskSyncAPI.java
deleted file mode 100644
index b1c26a67..00000000
--- a/api/src/main/java/net/william278/husksync/bukkit/api/HuskSyncAPI.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package net.william278.husksync.bukkit.api;
-
-import net.william278.husksync.PlayerData;
-import net.william278.husksync.Settings;
-import net.william278.husksync.bukkit.listener.BukkitRedisListener;
-import net.william278.husksync.redis.RedisMessage;
-
-import java.io.IOException;
-import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
-
-/**
- * HuskSync's API. To access methods, use the {@link #getInstance()} entrypoint.
- *
- * @author William
- */
-public class HuskSyncAPI {
-
- private HuskSyncAPI() {
- }
-
- private static HuskSyncAPI instance;
-
- /**
- * The API entry point. Returns an instance of the {@link HuskSyncAPI}
- *
- * @return instance of the {@link HuskSyncAPI}
- */
- public static HuskSyncAPI getInstance() {
- if (instance == null) {
- instance = new HuskSyncAPI();
- }
- return instance;
- }
-
- /**
- * Returns a {@link CompletableFuture} that will fetch the {@link PlayerData} for a user given their {@link UUID},
- * which contains serialized synchronised data.
- *
- * This can then be deserialized into ItemStacks and other usable values using the {@code DataSerializer} class.
- *
- * 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 not necessarily the current state of their inventory if they are online.
- */
- public CompletableFuture getPlayerData(UUID playerUUID) throws IOException {
- // Create the request to be completed
- final UUID requestUUID = UUID.randomUUID();
- BukkitRedisListener.apiRequests.put(requestUUID, new CompletableFuture<>());
-
- // Remove the request from the map on completion
- BukkitRedisListener.apiRequests.get(requestUUID).whenComplete((playerData, throwable) -> BukkitRedisListener.apiRequests.remove(requestUUID));
-
- // Request the data via the proxy
- new RedisMessage(RedisMessage.MessageType.API_DATA_REQUEST,
- new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
- playerUUID.toString(), requestUUID.toString()).send();
-
- return BukkitRedisListener.apiRequests.get(requestUUID);
- }
-
- /**
- * Updates a player's {@link PlayerData} to the proxy cache and database.
- *
- * If the player is online on the Proxy network, they will be updated and overwritten with this data.
- *
- * @param playerData The {@link PlayerData} (which contains the {@link UUID}) of the player data to update to the central cache and database
- * @throws IOException If an exception occurs with serializing during processing of the update
- */
- public void updatePlayerData(PlayerData playerData) throws IOException {
- // Serialize and send the updated player data
- final String serializedPlayerData = RedisMessage.serialize(playerData);
- new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
- new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
- serializedPlayerData, Boolean.toString(true)).send();
- }
-
-}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 859e6507..8134007d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -9,6 +9,8 @@ version "$ext.plugin_version+${versionMetadata()}"
ext {
set 'version', version.toString()
+ set 'jedis_version', jedis_version.toString()
+ set 'sqlite_driver_version', sqlite_driver_version.toString()
}
import org.apache.tools.ant.filters.ReplaceTokens
@@ -27,9 +29,7 @@ allprojects {
mavenLocal()
mavenCentral()
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
- maven { url 'https://repo.velocitypowered.com/snapshots/' }
maven { url 'https://repo.minebench.de/' }
- maven { url 'https://repo.codemc.org/repository/maven-public' }
maven { url 'https://repo.alessiodp.com/releases/' }
maven { url 'https://jitpack.io' }
}
@@ -51,7 +51,7 @@ subprojects {
version rootProject.version
archivesBaseName = "${rootProject.name}-${project.name.capitalize()}"
- if (['bukkit', 'api', 'bungeecord', 'velocity', 'plugin'].contains(project.name)) {
+ if (['bukkit', 'api', 'plugin'].contains(project.name)) {
shadowJar {
destinationDirectory.set(file("$rootDir/target"))
archiveClassifier.set('')
diff --git a/bukkit/build.gradle b/bukkit/build.gradle
index 18ab7c80..e047f125 100644
--- a/bukkit/build.gradle
+++ b/bukkit/build.gradle
@@ -2,7 +2,6 @@ dependencies {
implementation project(path: ':common')
implementation 'org.bstats:bstats-bukkit:3.0.0'
- implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
implementation 'net.william278:mpdbdataconverter:1.0'
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
@@ -10,9 +9,13 @@ dependencies {
}
shadowJar {
- relocate 'de.themoep', 'net.william278.husksync.libraries'
- relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
relocate 'redis.clients', 'net.william278.husksync.libraries'
- relocate 'org.apache', 'net.william278.husksync.libraries'
- relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
+ relocate 'org.apache', 'net.william278.huskhomes.libraries'
+ relocate 'dev.dejvokep', 'net.william278.huskhomes.libraries'
+ relocate 'de.themoep', 'net.william278.huskhomes.libraries'
+ relocate 'org.jetbrains', 'net.william278.huskhomes.libraries'
+ relocate 'org.intellij', 'net.william278.huskhomes.libraries'
+ relocate 'com.zaxxer', 'net.william278.huskhomes.libraries'
+ relocate 'org.slf4j', 'net.william278.huskhomes.libraries.slf4j'
+ relocate 'com.google', 'net.william278.huskhomes.libraries'
}
\ No newline at end of file
diff --git a/bukkit/src/main/java/me.william278.husksync.bukkit.data/DataSerializer.java b/bukkit/src/main/java/me.william278.husksync.bukkit.data/DataSerializer.java
deleted file mode 100644
index 9f5e3028..00000000
--- a/bukkit/src/main/java/me.william278.husksync.bukkit.data/DataSerializer.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package me.william278.husksync.bukkit.data;
-
-import org.bukkit.Material;
-import org.bukkit.Statistic;
-import org.bukkit.World;
-import org.bukkit.entity.EntityType;
-
-import java.io.Serializable;
-import java.time.Instant;
-import java.util.*;
-
-/**
- * Holds legacy data store methods for data storage
- */
-@Deprecated
-@SuppressWarnings("DeprecatedIsStillUsed")
-public class DataSerializer {
-
- /**
- * A record used to store data for advancement synchronisation
- *
- * @deprecated Old format - Use {@link AdvancementRecordDate} instead
- */
- @Deprecated
- @SuppressWarnings("DeprecatedIsStillUsed")
- // Suppress deprecation warnings here (still used for backwards compatibility)
- public record AdvancementRecord(String advancementKey,
- ArrayList awardedAdvancementCriteria) implements Serializable {
- }
-
- /**
- * A record used to store data for a player's statistics
- */
- public record StatisticData(HashMap untypedStatisticValues,
- HashMap> blockStatisticValues,
- HashMap> itemStatisticValues,
- HashMap> entityStatisticValues) implements Serializable {
- }
-
- /**
- * A record used to store data for native advancement synchronisation, tracking advancement date progress
- */
- public record AdvancementRecordDate(String key, Map criteriaMap) implements Serializable {
- public AdvancementRecordDate(String key, List criteriaList) {
- this(key, new HashMap<>() {{
- criteriaList.forEach(s -> put(s, Date.from(Instant.EPOCH)));
- }});
- }
- }
-
- /**
- * A record used to store data for a player's location
- */
- public record PlayerLocation(double x, double y, double z, float yaw, float pitch,
- String worldName, World.Environment environment) implements Serializable {
- }
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/HuskSyncBukkit.java b/bukkit/src/main/java/net/william278/husksync/HuskSyncBukkit.java
deleted file mode 100644
index 88f1f7ef..00000000
--- a/bukkit/src/main/java/net/william278/husksync/HuskSyncBukkit.java
+++ /dev/null
@@ -1,163 +0,0 @@
-package net.william278.husksync;
-
-import net.william278.husksync.Settings;
-import net.william278.husksync.bukkit.util.BukkitUpdateChecker;
-import net.william278.husksync.bukkit.util.PlayerSetter;
-import net.william278.husksync.bukkit.config.ConfigLoader;
-import net.william278.husksync.bukkit.data.BukkitDataCache;
-import net.william278.husksync.bukkit.listener.BukkitRedisListener;
-import net.william278.husksync.bukkit.listener.BukkitEventListener;
-import net.william278.husksync.bukkit.migrator.MPDBDeserializer;
-import net.william278.husksync.redis.RedisMessage;
-import org.bstats.bukkit.Metrics;
-import org.bukkit.Bukkit;
-import org.bukkit.entity.Player;
-import org.bukkit.plugin.Plugin;
-import org.bukkit.plugin.java.JavaPlugin;
-import org.bukkit.scheduler.BukkitTask;
-
-import java.io.IOException;
-import java.util.UUID;
-import java.util.logging.Level;
-
-public final class HuskSyncBukkit extends JavaPlugin {
-
- // Bukkit bStats ID (Different to BungeeCord)
- private static final int METRICS_ID = 13140;
-
- private static HuskSyncBukkit instance;
- public static HuskSyncBukkit getInstance() {
- return instance;
- }
-
- public static BukkitDataCache bukkitCache;
-
- public static BukkitRedisListener redisListener;
-
- // Used for establishing a handshake with redis
- public static UUID serverUUID;
-
- // Has a handshake been established with the Bungee?
- public static boolean handshakeCompleted = false;
-
- // The handshake task to execute
- private static BukkitTask handshakeTask;
-
- // Whether MySqlPlayerDataBridge is installed
- public static boolean isMySqlPlayerDataBridgeInstalled;
-
- // Establish the handshake with the proxy
- public static void establishRedisHandshake() {
- serverUUID = UUID.randomUUID();
- getInstance().getLogger().log(Level.INFO, "Executing handshake with Proxy server...");
- final int[] attempts = {0}; // How many attempts to establish communication have been made
- handshakeTask = Bukkit.getScheduler().runTaskTimerAsynchronously(getInstance(), () -> {
- if (handshakeCompleted) {
- handshakeTask.cancel();
- return;
- }
- try {
- new RedisMessage(RedisMessage.MessageType.CONNECTION_HANDSHAKE,
- new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
- serverUUID.toString(),
- Boolean.toString(isMySqlPlayerDataBridgeInstalled),
- Bukkit.getName(),
- getInstance().getDescription().getVersion())
- .send();
- attempts[0]++;
- if (attempts[0] == 10) {
- getInstance().getLogger().log(Level.WARNING, "Failed to complete handshake with the Proxy server; Please make sure your Proxy server is online and has HuskSync installed in its' /plugins/ folder. HuskSync will continue to try and establish a connection.");
- }
- } catch (IOException e) {
- getInstance().getLogger().log(Level.SEVERE, "Failed to serialize Redis message for handshake establishment", e);
- }
- }, 0, 60);
- }
-
- private void closeRedisHandshake() {
- if (!handshakeCompleted) return;
- try {
- new RedisMessage(RedisMessage.MessageType.TERMINATE_HANDSHAKE,
- new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
- serverUUID.toString(),
- Bukkit.getName()).send();
- } catch (IOException e) {
- getInstance().getLogger().log(Level.SEVERE, "Failed to serialize Redis message for handshake termination", e);
- }
- }
-
- @Override
- public void onLoad() {
- instance = this;
- }
-
- @Override
- public void onEnable() {
- // Plugin startup logic
-
- // Load the config file
- getConfig().options().copyDefaults(true);
- saveDefaultConfig();
- saveConfig();
- reloadConfig();
- ConfigLoader.loadSettings(getConfig());
-
- // Do update checker
- if (Settings.automaticUpdateChecks) {
- new BukkitUpdateChecker().logToConsole();
- }
-
- // Check if MySqlPlayerDataBridge is installed
- Plugin mySqlPlayerDataBridge = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
- if (mySqlPlayerDataBridge != null) {
- isMySqlPlayerDataBridgeInstalled = mySqlPlayerDataBridge.isEnabled();
- MPDBDeserializer.setMySqlPlayerDataBridge();
- getLogger().info("MySQLPlayerDataBridge detected! Disabled data synchronisation to prevent data loss. To perform a migration, run \"husksync migrate\" in your Proxy (Bungeecord, Waterfall, etc) server console.");
- }
-
- // Initialize last data update UUID cache
- bukkitCache = new BukkitDataCache();
-
- // Initialize event listener
- getServer().getPluginManager().registerEvents(new BukkitEventListener(), this);
-
- // Initialize the redis listener
- redisListener = new BukkitRedisListener();
-
- // Ensure redis is connected; establish a handshake
- establishRedisHandshake();
-
- // Initialize bStats metrics
- try {
- new Metrics(this, METRICS_ID);
- } catch (Exception e) {
- getLogger().info("Skipped metrics initialization");
- }
-
- // Log to console
- getLogger().info("Enabled HuskSync (" + getServer().getName() + ") v" + getDescription().getVersion());
- }
-
- @Override
- public void onDisable() {
- // Update player data for disconnecting players
- if (HuskSyncBukkit.handshakeCompleted && !HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled && Bukkit.getOnlinePlayers().size() > 0) {
- getLogger().info("Saving data for remaining online players...");
- for (Player player : Bukkit.getOnlinePlayers()) {
- PlayerSetter.updatePlayerData(player, false);
-
- // Clear player inventory and ender chest
- player.getInventory().clear();
- player.getEnderChest().clear();
- }
- getLogger().info("Data save complete!");
- }
-
-
- // Send termination handshake to proxy
- closeRedisHandshake();
-
- // Plugin shutdown logic
- getLogger().info("Disabled HuskSync (" + getServer().getName() + ") v" + getDescription().getVersion());
- }
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/bukkit/config/ConfigLoader.java b/bukkit/src/main/java/net/william278/husksync/bukkit/config/ConfigLoader.java
deleted file mode 100644
index 52d50cd7..00000000
--- a/bukkit/src/main/java/net/william278/husksync/bukkit/config/ConfigLoader.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package net.william278.husksync.bukkit.config;
-
-import net.william278.husksync.Settings;
-import org.bukkit.configuration.file.FileConfiguration;
-
-public class ConfigLoader {
-
- public static void loadSettings(FileConfiguration config) throws IllegalArgumentException {
- Settings.serverType = Settings.ServerType.BUKKIT;
- Settings.automaticUpdateChecks = config.getBoolean("check_for_updates", true);
- Settings.cluster = config.getString("cluster_id", "main");
- Settings.redisHost = config.getString("redis_settings.host", "localhost");
- Settings.redisPort = config.getInt("redis_settings.port", 6379);
- Settings.redisPassword = config.getString("redis_settings.password", "");
- Settings.redisSSL = config.getBoolean("redis_settings.use_ssl", false);
-
- Settings.syncInventories = config.getBoolean("synchronisation_settings.inventories", true);
- Settings.syncEnderChests = config.getBoolean("synchronisation_settings.ender_chests", true);
- Settings.syncHealth = config.getBoolean("synchronisation_settings.health", true);
- Settings.syncHunger = config.getBoolean("synchronisation_settings.hunger", true);
- Settings.syncExperience = config.getBoolean("synchronisation_settings.experience", true);
- Settings.syncPotionEffects = config.getBoolean("synchronisation_settings.potion_effects", true);
- Settings.syncStatistics = config.getBoolean("synchronisation_settings.statistics", true);
- Settings.syncGameMode = config.getBoolean("synchronisation_settings.game_mode", true);
- Settings.syncAdvancements = config.getBoolean("synchronisation_settings.advancements", true);
- Settings.syncLocation = config.getBoolean("synchronisation_settings.location", false);
- Settings.syncFlight = config.getBoolean("synchronisation_settings.flight", false);
-
- Settings.useNativeImplementation = config.getBoolean("native_advancement_synchronization", false);
- Settings.saveOnWorldSave = config.getBoolean("save_on_world_save", true);
- Settings.synchronizationTimeoutRetryDelay = config.getLong("synchronization_timeout_retry_delay", 15L);
- }
-
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/bukkit/data/BukkitDataCache.java b/bukkit/src/main/java/net/william278/husksync/bukkit/data/BukkitDataCache.java
deleted file mode 100644
index 50996600..00000000
--- a/bukkit/src/main/java/net/william278/husksync/bukkit/data/BukkitDataCache.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package net.william278.husksync.bukkit.data;
-
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.UUID;
-
-public class BukkitDataCache {
-
- /**
- * Map of Player UUIDs to request on join
- */
- private static HashSet requestOnJoin;
-
- public boolean isPlayerRequestingOnJoin(UUID uuid) {
- return requestOnJoin.contains(uuid);
- }
-
- public void setRequestOnJoin(UUID uuid) {
- requestOnJoin.add(uuid);
- }
-
- public void removeRequestOnJoin(UUID uuid) {
- requestOnJoin.remove(uuid);
- }
-
- /**
- * Map of Player UUIDs whose data has not been set yet
- */
- private static HashSet awaitingDataFetch;
-
- public boolean isAwaitingDataFetch(UUID uuid) {
- return awaitingDataFetch.contains(uuid);
- }
-
- public void setAwaitingDataFetch(UUID uuid) {
- awaitingDataFetch.add(uuid);
- }
-
- public void removeAwaitingDataFetch(UUID uuid) {
- awaitingDataFetch.remove(uuid);
- }
-
- public HashSet getAwaitingDataFetch() {
- return awaitingDataFetch;
- }
-
- /**
- * Map of data being viewed by players
- */
- private static HashMap viewingPlayerData;
-
- public void setViewing(UUID uuid, DataViewer.DataView dataView) {
- viewingPlayerData.put(uuid, dataView);
- }
-
- public void removeViewing(UUID uuid) {
- viewingPlayerData.remove(uuid);
- }
-
- public boolean isViewing(UUID uuid) {
- return viewingPlayerData.containsKey(uuid);
- }
-
- public DataViewer.DataView getViewing(UUID uuid) {
- return viewingPlayerData.get(uuid);
- }
-
- // Cache object
- public BukkitDataCache() {
- requestOnJoin = new HashSet<>();
- viewingPlayerData = new HashMap<>();
- awaitingDataFetch = new HashSet<>();
- }
-}
\ No newline at end of file
diff --git a/bukkit/src/main/java/net/william278/husksync/bukkit/data/DataSerializer.java b/bukkit/src/main/java/net/william278/husksync/bukkit/data/DataSerializer.java
deleted file mode 100644
index dafb3a17..00000000
--- a/bukkit/src/main/java/net/william278/husksync/bukkit/data/DataSerializer.java
+++ /dev/null
@@ -1,327 +0,0 @@
-package net.william278.husksync.bukkit.data;
-
-import net.william278.husksync.redis.RedisMessage;
-import org.bukkit.*;
-import org.bukkit.advancement.Advancement;
-import org.bukkit.advancement.AdvancementProgress;
-import org.bukkit.entity.EntityType;
-import org.bukkit.entity.Player;
-import org.bukkit.inventory.ItemStack;
-import org.bukkit.potion.PotionEffect;
-import org.bukkit.util.io.BukkitObjectInputStream;
-import org.bukkit.util.io.BukkitObjectOutputStream;
-import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.*;
-
-/**
- * Class that contains static methods for serializing and deserializing data from {@link net.william278.husksync.PlayerData}
- */
-public class DataSerializer {
-
- /**
- * Returns a serialized array of {@link ItemStack}s
- *
- * @param inventoryContents The contents of the inventory
- * @return The serialized inventory contents
- */
- public static String serializeInventory(ItemStack[] inventoryContents) {
- // Return an empty string if there is no inventory item data to serialize
- if (inventoryContents.length == 0) {
- return "";
- }
-
- // Create an output stream that will be encoded into base 64
- ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
-
- try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
- // Define the length of the inventory array to serialize
- bukkitOutputStream.writeInt(inventoryContents.length);
-
- // Write each serialize each ItemStack to the output stream
- for (ItemStack inventoryItem : inventoryContents) {
- bukkitOutputStream.writeObject(serializeItemStack(inventoryItem));
- }
-
- // Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
- return Base64Coder.encodeLines(byteOutputStream.toByteArray());
- } catch (IOException e) {
- throw new IllegalArgumentException("Failed to serialize item stack data");
- }
- }
-
- /**
- * Returns an array of ItemStacks from serialized inventory data. Note: empty slots will be represented by {@code null}
- *
- * @param inventoryData The serialized {@link ItemStack[]} array
- * @return The inventory contents as an array of {@link ItemStack}s
- * @throws IOException If the deserialization fails reading data from the InputStream
- * @throws ClassNotFoundException If the deserialization class cannot be found
- */
- public static ItemStack[] deserializeInventory(String inventoryData) throws IOException, ClassNotFoundException {
- // Return empty array if there is no inventory data (set the player as having an empty inventory)
- if (inventoryData.isEmpty()) {
- return new ItemStack[0];
- }
-
- // Create a byte input stream to read the serialized data
- try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(inventoryData))) {
- try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
- // Read the length of the Bukkit input stream and set the length of the array to this value
- ItemStack[] inventoryContents = new ItemStack[bukkitInputStream.readInt()];
-
- // Set the ItemStacks in the array from deserialized ItemStack data
- int slotIndex = 0;
- for (ItemStack ignored : inventoryContents) {
- inventoryContents[slotIndex] = deserializeItemStack(bukkitInputStream.readObject());
- slotIndex++;
- }
-
- // Return the finished, serialized inventory contents
- return inventoryContents;
- }
- }
- }
-
- /**
- * Returns the serialized version of an {@link ItemStack} as a string to object Map
- *
- * @param item The {@link ItemStack} to serialize
- * @return The serialized {@link ItemStack}
- */
- private static Map serializeItemStack(ItemStack item) {
- return item != null ? item.serialize() : null;
- }
-
- /**
- * Returns the deserialized {@link ItemStack} from the Object read from the {@link BukkitObjectInputStream}
- *
- * @param serializedItemStack The serialized item stack; a String-Object map
- * @return The deserialized {@link ItemStack}
- */
- @SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
- private static ItemStack deserializeItemStack(Object serializedItemStack) {
- return serializedItemStack != null ? ItemStack.deserialize((Map) serializedItemStack) : null;
- }
-
- /**
- * Returns a serialized array of {@link PotionEffect}s
- *
- * @param potionEffects The potion effect array
- * @return The serialized potion effects
- */
- public static String serializePotionEffects(PotionEffect[] potionEffects) {
- // Return an empty string if there are no effects to serialize
- if (potionEffects.length == 0) {
- return "";
- }
-
- // Create an output stream that will be encoded into base 64
- ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
-
- try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
- // Define the length of the potion effect array to serialize
- bukkitOutputStream.writeInt(potionEffects.length);
-
- // Write each serialize each PotionEffect to the output stream
- for (PotionEffect potionEffect : potionEffects) {
- bukkitOutputStream.writeObject(serializePotionEffect(potionEffect));
- }
-
- // Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
- return Base64Coder.encodeLines(byteOutputStream.toByteArray());
- } catch (IOException e) {
- throw new IllegalArgumentException("Failed to serialize potion effect data");
- }
- }
-
- /**
- * Returns an array of ItemStacks from serialized potion effect data
- *
- * @param potionEffectData The serialized {@link PotionEffect[]} array
- * @return The {@link PotionEffect}s
- * @throws IOException If the deserialization fails reading data from the InputStream
- * @throws ClassNotFoundException If the deserialization class cannot be found
- */
- public static PotionEffect[] deserializePotionEffects(String potionEffectData) throws IOException, ClassNotFoundException {
- // Return empty array if there is no potion effect data (don't apply any effects to the player)
- if (potionEffectData.isEmpty()) {
- return new PotionEffect[0];
- }
-
- // Create a byte input stream to read the serialized data
- try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(potionEffectData))) {
- try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
- // Read the length of the Bukkit input stream and set the length of the array to this value
- PotionEffect[] potionEffects = new PotionEffect[bukkitInputStream.readInt()];
-
- // Set the potion effects in the array from deserialized PotionEffect data
- int potionIndex = 0;
- for (PotionEffect ignored : potionEffects) {
- potionEffects[potionIndex] = deserializePotionEffect(bukkitInputStream.readObject());
- potionIndex++;
- }
-
- // Return the finished, serialized potion effect array
- return potionEffects;
- }
- }
- }
-
- /**
- * Returns the serialized version of an {@link ItemStack} as a string to object Map
- *
- * @param potionEffect The {@link ItemStack} to serialize
- * @return The serialized {@link ItemStack}
- */
- private static Map serializePotionEffect(PotionEffect potionEffect) {
- return potionEffect != null ? potionEffect.serialize() : null;
- }
-
- /**
- * Returns the deserialized {@link PotionEffect} from the Object read from the {@link BukkitObjectInputStream}
- *
- * @param serializedPotionEffect The serialized potion effect; a String-Object map
- * @return The deserialized {@link PotionEffect}
- */
- @SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
- private static PotionEffect deserializePotionEffect(Object serializedPotionEffect) {
- return serializedPotionEffect != null ? new PotionEffect((Map) serializedPotionEffect) : null;
- }
-
- public static me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation deserializePlayerLocationData(String serializedLocationData) throws IOException {
- if (serializedLocationData.isEmpty()) {
- return null;
- }
- try {
- return (me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation) RedisMessage.deserialize(serializedLocationData);
- } catch (ClassNotFoundException e) {
- throw new IOException("Unable to decode class type.", e);
- }
- }
-
- public static String getSerializedLocation(Player player) throws IOException {
- final Location playerLocation = player.getLocation();
- return RedisMessage.serialize(new me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation(playerLocation.getX(), playerLocation.getY(), playerLocation.getZ(),
- playerLocation.getYaw(), playerLocation.getPitch(), player.getWorld().getName(), player.getWorld().getEnvironment()));
- }
-
- /**
- * Deserializes a player's advancement data as serialized with {@link #getSerializedAdvancements(Player)} into {@link me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate} data.
- *
- * @param serializedAdvancementData The serialized advancement data {@link String}
- * @return The deserialized {@link me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate} for the player
- * @throws IOException If the deserialization fails
- */
- @SuppressWarnings("unchecked") // Ignore the unchecked cast here
- public static List deserializeAdvancementData(String serializedAdvancementData) throws IOException {
- if (serializedAdvancementData.isEmpty()) {
- return new ArrayList<>();
- }
- try {
- List> deserialize = (List>) RedisMessage.deserialize(serializedAdvancementData);
-
- // Migrate old AdvancementRecord into date format
- if (!deserialize.isEmpty() && deserialize.get(0) instanceof me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecord) {
- deserialize = ((List) deserialize).stream()
- .map(o -> new me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate(
- o.advancementKey(),
- o.awardedAdvancementCriteria()
- )).toList();
- }
-
- return (List) deserialize;
- } catch (ClassNotFoundException e) {
- throw new IOException("Unable to decode class type.", e);
- }
- }
-
- /**
- * Returns a serialized {@link String} of a player's advancements that can be deserialized with {@link #deserializeStatisticData(String)}
- *
- * @param player {@link Player} to serialize advancement data of
- * @return The serialized advancement data as a {@link String}
- * @throws IOException If the serialization fails
- */
- public static String getSerializedAdvancements(Player player) throws IOException {
- Iterator serverAdvancements = Bukkit.getServer().advancementIterator();
- ArrayList advancementData = new ArrayList<>();
-
- while (serverAdvancements.hasNext()) {
- final AdvancementProgress progress = player.getAdvancementProgress(serverAdvancements.next());
- final NamespacedKey advancementKey = progress.getAdvancement().getKey();
-
- final Map awardedCriteria = new HashMap<>();
- progress.getAwardedCriteria().forEach(s -> awardedCriteria.put(s, progress.getDateAwarded(s)));
-
- advancementData.add(new me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate(advancementKey.getNamespace() + ":" + advancementKey.getKey(), awardedCriteria));
- }
-
- return RedisMessage.serialize(advancementData);
- }
-
- /**
- * Deserializes a player's statistic data as serialized with {@link #getSerializedStatisticData(Player)} into {@link me.william278.husksync.bukkit.data.DataSerializer.StatisticData}.
- *
- * @param serializedStatisticData The serialized statistic data {@link String}
- * @return The deserialized {@link me.william278.husksync.bukkit.data.DataSerializer.StatisticData} for the player
- * @throws IOException If the deserialization fails
- */
- public static me.william278.husksync.bukkit.data.DataSerializer.StatisticData deserializeStatisticData(String serializedStatisticData) throws IOException {
- if (serializedStatisticData.isEmpty()) {
- return new me.william278.husksync.bukkit.data.DataSerializer.StatisticData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
- }
- try {
- return (me.william278.husksync.bukkit.data.DataSerializer.StatisticData) RedisMessage.deserialize(serializedStatisticData);
- } catch (ClassNotFoundException e) {
- throw new IOException("Unable to decode class type.", e);
- }
- }
-
- /**
- * Returns a serialized {@link String} of a player's statistic data that can be deserialized with {@link #deserializeStatisticData(String)}
- *
- * @param player {@link Player} to serialize statistic data of
- * @return The serialized statistic data as a {@link String}
- * @throws IOException If the serialization fails
- */
- public static String getSerializedStatisticData(Player player) throws IOException {
- HashMap untypedStatisticValues = new HashMap<>();
- HashMap> blockStatisticValues = new HashMap<>();
- HashMap> itemStatisticValues = new HashMap<>();
- HashMap> entityStatisticValues = new HashMap<>();
- for (Statistic statistic : Statistic.values()) {
- switch (statistic.getType()) {
- case ITEM -> {
- HashMap itemValues = new HashMap<>();
- for (Material itemMaterial : Arrays.stream(Material.values()).filter(Material::isItem).toList()) {
- itemValues.put(itemMaterial, player.getStatistic(statistic, itemMaterial));
- }
- itemStatisticValues.put(statistic, itemValues);
- }
- case BLOCK -> {
- HashMap blockValues = new HashMap<>();
- for (Material blockMaterial : Arrays.stream(Material.values()).filter(Material::isBlock).toList()) {
- blockValues.put(blockMaterial, player.getStatistic(statistic, blockMaterial));
- }
- blockStatisticValues.put(statistic, blockValues);
- }
- case ENTITY -> {
- HashMap entityValues = new HashMap<>();
- for (EntityType type : Arrays.stream(EntityType.values()).filter(EntityType::isAlive).toList()) {
- entityValues.put(type, player.getStatistic(statistic, type));
- }
- entityStatisticValues.put(statistic, entityValues);
- }
- case UNTYPED -> untypedStatisticValues.put(statistic, player.getStatistic(statistic));
- }
- }
-
- me.william278.husksync.bukkit.data.DataSerializer.StatisticData statisticData = new me.william278.husksync.bukkit.data.DataSerializer.StatisticData(untypedStatisticValues, blockStatisticValues, itemStatisticValues, entityStatisticValues);
- return RedisMessage.serialize(statisticData);
- }
-
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/bukkit/data/DataViewer.java b/bukkit/src/main/java/net/william278/husksync/bukkit/data/DataViewer.java
deleted file mode 100644
index fa7dd32f..00000000
--- a/bukkit/src/main/java/net/william278/husksync/bukkit/data/DataViewer.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package net.william278.husksync.bukkit.data;
-
-import net.william278.husksync.HuskSyncBukkit;
-import net.william278.husksync.PlayerData;
-import net.william278.husksync.Settings;
-import net.william278.husksync.bukkit.util.PlayerSetter;
-import net.william278.husksync.redis.RedisMessage;
-import org.bukkit.Bukkit;
-import org.bukkit.entity.Player;
-import org.bukkit.inventory.Inventory;
-import org.bukkit.inventory.ItemStack;
-
-import java.io.IOException;
-
-/**
- * Class used for managing viewing inventories using inventory-see command
- */
-public class DataViewer {
-
- /**
- * Show a viewer's data to a viewer
- *
- * @param viewer The viewing {@link Player} who will see the data
- * @param data The {@link DataView} to show the viewer
- * @throws IOException If an exception occurred deserializing item data
- */
- public static void showData(Player viewer, DataView data) throws IOException, ClassNotFoundException {
- // Show an inventory with the viewer's inventory and equipment
- viewer.closeInventory();
- viewer.openInventory(createInventory(viewer, data));
-
- // Set the viewer as viewing
- HuskSyncBukkit.bukkitCache.setViewing(viewer.getUniqueId(), data);
- }
-
- /**
- * Handles what happens after a data viewer finishes viewing data
- *
- * @param viewer The viewing {@link Player} who was looking at data
- * @param inventory The {@link Inventory} that was being viewed
- * @throws IOException If an exception occurred serializing item data
- */
- public static void stopShowing(Player viewer, Inventory inventory) throws IOException {
- // Get the DataView the player was looking at
- DataView dataView = HuskSyncBukkit.bukkitCache.getViewing(viewer.getUniqueId());
-
- // Set the player as no longer viewing an inventory
- HuskSyncBukkit.bukkitCache.removeViewing(viewer.getUniqueId());
-
- // Get and update the PlayerData with the new item data
- PlayerData playerData = dataView.playerData();
- String serializedItemData = DataSerializer.serializeInventory(inventory.getContents());
- switch (dataView.inventoryType()) {
- case INVENTORY -> playerData.setSerializedInventory(serializedItemData);
- case ENDER_CHEST -> playerData.setSerializedEnderChest(serializedItemData);
- }
-
- // Send a redis message with the updated data after the viewing
- new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
- new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
- RedisMessage.serialize(playerData), Boolean.toString(true))
- .send();
- }
-
- /**
- * Creates the inventory object that the viewer will see
- *
- * @param viewer The {@link Player} who will view the data
- * @param data The {@link DataView} data to view
- * @return The {@link Inventory} that the viewer will see
- * @throws IOException If an exception occurred deserializing item data
- */
- private static Inventory createInventory(Player viewer, DataView data) throws IOException, ClassNotFoundException {
- Inventory inventory = switch (data.inventoryType) {
- case INVENTORY -> Bukkit.createInventory(viewer, 45, data.ownerName + "'s Inventory");
- case ENDER_CHEST -> Bukkit.createInventory(viewer, 27, data.ownerName + "'s Ender Chest");
- };
- PlayerSetter.setInventory(inventory, data.getDeserializedData());
- return inventory;
- }
-
- /**
- * Represents Player Data being viewed by a {@link Player}
- */
- public record DataView(PlayerData playerData, String ownerName, InventoryType inventoryType) {
- /**
- * What kind of item data is being viewed
- */
- public enum InventoryType {
- /**
- * A player's inventory
- */
- INVENTORY,
-
- /**
- * A player's ender chest
- */
- ENDER_CHEST
- }
-
- /**
- * Gets the deserialized data currently being viewed
- *
- * @return The deserialized item data, as an {@link ItemStack[]} array
- * @throws IOException If an exception occurred deserializing item data
- */
- public ItemStack[] getDeserializedData() throws IOException, ClassNotFoundException {
- return switch (inventoryType) {
- case INVENTORY -> DataSerializer.deserializeInventory(playerData.getSerializedInventory());
- case ENDER_CHEST -> DataSerializer.deserializeInventory(playerData.getSerializedEnderChest());
- };
- }
- }
-
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/bukkit/events/SyncCompleteEvent.java b/bukkit/src/main/java/net/william278/husksync/bukkit/events/SyncCompleteEvent.java
deleted file mode 100644
index 2d5a8243..00000000
--- a/bukkit/src/main/java/net/william278/husksync/bukkit/events/SyncCompleteEvent.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package net.william278.husksync.bukkit.events;
-
-import net.william278.husksync.PlayerData;
-import org.bukkit.entity.Player;
-import org.bukkit.event.HandlerList;
-import org.bukkit.event.player.PlayerEvent;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * Represents an event that will be fired when a {@link Player} has finished being synchronised with the correct {@link PlayerData}.
- */
-public class SyncCompleteEvent extends PlayerEvent {
-
- private static final HandlerList HANDLER_LIST = new HandlerList();
- private final PlayerData data;
-
- public SyncCompleteEvent(Player player, PlayerData data) {
- super(player);
- this.data = data;
- }
-
- /**
- * Returns the {@link PlayerData} which has just been set on the {@link Player}
- * @return The {@link PlayerData} that has been set
- */
- public PlayerData getData() {
- return data;
- }
-
- @Override
- public @NotNull HandlerList getHandlers() {
- return HANDLER_LIST;
- }
-
- public static HandlerList getHandlerList() {
- return HANDLER_LIST;
- }
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/bukkit/events/SyncEvent.java b/bukkit/src/main/java/net/william278/husksync/bukkit/events/SyncEvent.java
deleted file mode 100644
index 650f117b..00000000
--- a/bukkit/src/main/java/net/william278/husksync/bukkit/events/SyncEvent.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package net.william278.husksync.bukkit.events;
-
-import net.william278.husksync.PlayerData;
-import org.bukkit.entity.Player;
-import org.bukkit.event.Cancellable;
-import org.bukkit.event.HandlerList;
-import org.bukkit.event.player.PlayerEvent;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * Represents an event that will be fired before a {@link Player} is about to be synchronised with their {@link PlayerData}.
- */
-public class SyncEvent extends PlayerEvent implements Cancellable {
-
- private boolean cancelled;
- private static final HandlerList HANDLER_LIST = new HandlerList();
- private PlayerData data;
-
- public SyncEvent(Player player, PlayerData data) {
- super(player);
- this.data = data;
- }
-
- /**
- * Returns the {@link PlayerData} which has just been set on the {@link Player}
- *
- * @return The {@link PlayerData} that has been set
- */
- public PlayerData getData() {
- return data;
- }
-
- /**
- * Sets the {@link PlayerData} to be synchronised to this player
- *
- * @param data The {@link PlayerData} to set to the player
- */
- public void setData(PlayerData data) {
- this.data = data;
- }
-
- @Override
- public @NotNull HandlerList getHandlers() {
- return HANDLER_LIST;
- }
-
- public static HandlerList getHandlerList() {
- return HANDLER_LIST;
- }
-
- /**
- * Gets the cancellation state of this event. A cancelled event will not be executed in the server, but will still pass to other plugins
- *
- * @return true if this event is cancelled
- */
- @Override
- public boolean isCancelled() {
- return cancelled;
- }
-
- /**
- * Sets the cancellation state of this event. A cancelled event will not be executed in the server, but will still pass to other plugins.
- *
- * @param cancel true if you wish to cancel this event
- */
- @Override
- public void setCancelled(boolean cancel) {
- this.cancelled = cancel;
- }
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/bukkit/listener/BukkitEventListener.java b/bukkit/src/main/java/net/william278/husksync/bukkit/listener/BukkitEventListener.java
deleted file mode 100644
index 26f7cb19..00000000
--- a/bukkit/src/main/java/net/william278/husksync/bukkit/listener/BukkitEventListener.java
+++ /dev/null
@@ -1,166 +0,0 @@
-package net.william278.husksync.bukkit.listener;
-
-import net.william278.husksync.HuskSyncBukkit;
-import net.william278.husksync.Settings;
-import net.william278.husksync.bukkit.data.DataViewer;
-import net.william278.husksync.bukkit.util.PlayerSetter;
-import org.bukkit.Bukkit;
-import org.bukkit.entity.Player;
-import org.bukkit.event.EventHandler;
-import org.bukkit.event.EventPriority;
-import org.bukkit.event.Listener;
-import org.bukkit.event.block.BlockBreakEvent;
-import org.bukkit.event.block.BlockPlaceEvent;
-import org.bukkit.event.entity.EntityPickupItemEvent;
-import org.bukkit.event.inventory.InventoryCloseEvent;
-import org.bukkit.event.inventory.InventoryOpenEvent;
-import org.bukkit.event.player.*;
-import org.bukkit.event.world.WorldSaveEvent;
-
-import java.io.IOException;
-import java.util.logging.Level;
-
-public class BukkitEventListener implements Listener {
-
- private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
-
- @EventHandler(priority = EventPriority.LOWEST)
- public void onPlayerQuit(PlayerQuitEvent event) {
- // When a player leaves a Bukkit server
- final Player player = event.getPlayer();
-
- // If the player was awaiting data fetch, remove them and prevent data from being overwritten
- if (HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(player.getUniqueId())) {
- HuskSyncBukkit.bukkitCache.removeAwaitingDataFetch(player.getUniqueId());
- return;
- }
-
- if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled)
- return; // If the plugin has not been initialized correctly
-
- // Update the player's data
- Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
- // Update data to proxy
- PlayerSetter.updatePlayerData(player, true);
-
- // Clear player inventory and ender chest
- player.getInventory().clear();
- player.getEnderChest().clear();
- });
- }
-
- @EventHandler(priority = EventPriority.LOWEST)
- public void onPlayerJoin(PlayerJoinEvent event) {
- if (!plugin.isEnabled()) return; // If the plugin has not been initialized correctly
-
- // When a player joins a Bukkit server
- final Player player = event.getPlayer();
-
- // Mark the player as awaiting data fetch
- HuskSyncBukkit.bukkitCache.setAwaitingDataFetch(player.getUniqueId());
-
- if (!HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled) {
- return; // If the data handshake has not been completed yet (or MySqlPlayerDataBridge is installed)
- }
-
- // Send a redis message requesting the player data (if they need to)
- if (HuskSyncBukkit.bukkitCache.isPlayerRequestingOnJoin(player.getUniqueId())) {
- Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
- try {
- PlayerSetter.requestPlayerData(player.getUniqueId());
- } catch (IOException e) {
- plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData fetch request", e);
- }
- });
- } else {
- // If the player's data wasn't set after the synchronization timeout retry delay ticks, ensure it will be
- Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, () -> {
- if (player.isOnline()) {
- try {
- if (HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(player.getUniqueId())) {
- PlayerSetter.requestPlayerData(player.getUniqueId());
- }
- } catch (IOException e) {
- plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData fetch request", e);
- }
- }
- }, Settings.synchronizationTimeoutRetryDelay);
- }
- }
-
- @EventHandler
- public void onInventoryClose(InventoryCloseEvent event) {
- if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId()))
- return; // If the plugin has not been initialized correctly
-
- // When a player closes an Inventory
- final Player player = (Player) event.getPlayer();
-
- // Handle a player who has finished viewing a player's item data
- if (HuskSyncBukkit.bukkitCache.isViewing(player.getUniqueId())) {
- try {
- DataViewer.stopShowing(player, event.getInventory());
- } catch (IOException e) {
- plugin.getLogger().log(Level.SEVERE, "Failed to serialize updated item data", e);
- }
- }
- }
-
- /*
- * Events to cancel if the player has not been set yet
- */
-
- @EventHandler(priority = EventPriority.HIGHEST)
- public void onDropItem(PlayerDropItemEvent event) {
- if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
- event.setCancelled(true); // If the plugin / player has not been set
- }
- }
-
- @EventHandler(priority = EventPriority.HIGHEST)
- public void onPickupItem(EntityPickupItemEvent event) {
- if (event.getEntity() instanceof Player player) {
- if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(player.getUniqueId())) {
- event.setCancelled(true); // If the plugin / player has not been set
- }
- }
- }
-
- @EventHandler(priority = EventPriority.HIGHEST)
- public void onPlayerInteract(PlayerInteractEvent event) {
- if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
- event.setCancelled(true); // If the plugin / player has not been set
- }
- }
-
- @EventHandler(priority = EventPriority.HIGHEST)
- public void onBlockPlace(BlockPlaceEvent event) {
- if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
- event.setCancelled(true); // If the plugin / player has not been set
- }
- }
-
- @EventHandler(priority = EventPriority.HIGHEST)
- public void onBlockBreak(BlockBreakEvent event) {
- if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
- event.setCancelled(true); // If the plugin / player has not been set
- }
- }
-
- @EventHandler(priority = EventPriority.HIGHEST)
- public void onInventoryOpen(InventoryOpenEvent event) {
- if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
- event.setCancelled(true); // If the plugin / player has not been set
- }
- }
-
- @EventHandler(priority = EventPriority.NORMAL)
- public void onWorldSave(WorldSaveEvent event) {
- if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted) {
- return;
- }
- for (Player playerInWorld : event.getWorld().getPlayers()) {
- PlayerSetter.updatePlayerData(playerInWorld, false);
- }
- }
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/bukkit/listener/BukkitRedisListener.java b/bukkit/src/main/java/net/william278/husksync/bukkit/listener/BukkitRedisListener.java
deleted file mode 100644
index 6d6ec97a..00000000
--- a/bukkit/src/main/java/net/william278/husksync/bukkit/listener/BukkitRedisListener.java
+++ /dev/null
@@ -1,221 +0,0 @@
-package net.william278.husksync.bukkit.listener;
-
-import de.themoep.minedown.MineDown;
-import net.william278.husksync.HuskSyncBukkit;
-import net.william278.husksync.PlayerData;
-import net.william278.husksync.Settings;
-import net.william278.husksync.bukkit.config.ConfigLoader;
-import net.william278.husksync.bukkit.data.DataViewer;
-import net.william278.husksync.bukkit.migrator.MPDBDeserializer;
-import net.william278.husksync.bukkit.util.PlayerSetter;
-import net.william278.husksync.migrator.MPDBPlayerData;
-import net.william278.husksync.redis.RedisListener;
-import net.william278.husksync.redis.RedisMessage;
-import net.william278.husksync.util.MessageManager;
-import org.bukkit.Bukkit;
-import org.bukkit.entity.Player;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
-import java.util.logging.Level;
-
-public class BukkitRedisListener extends RedisListener {
-
- private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
-
- public static HashMap> apiRequests = new HashMap<>();
-
- // Initialize the listener on the bukkit server
- public BukkitRedisListener() {
- super();
- listen();
- }
-
- /**
- * Handle an incoming {@link RedisMessage}
- *
- * @param message The {@link RedisMessage} to handle
- */
- @Override
- public void handleMessage(RedisMessage message) {
- // Ignore messages for proxy servers
- if (!message.getMessageTarget().targetServerType().equals(Settings.ServerType.BUKKIT)) {
- return;
- }
- // Ignore messages if the plugin is disabled
- if (!plugin.isEnabled()) {
- return;
- }
- // Ignore messages for other clusters if applicable
- final String targetClusterId = message.getMessageTarget().targetClusterId();
- if (targetClusterId != null) {
- if (!targetClusterId.equalsIgnoreCase(Settings.cluster)) {
- return;
- }
- }
-
- // Handle the incoming redis message; either for a specific player or the system
- if (message.getMessageTarget().targetPlayerUUID() == null) {
- switch (message.getMessageType()) {
- case REQUEST_DATA_ON_JOIN -> {
- UUID playerUUID = UUID.fromString(message.getMessageDataElements()[1]);
- switch (RedisMessage.RequestOnJoinUpdateType.valueOf(message.getMessageDataElements()[0])) {
- case ADD_REQUESTER -> HuskSyncBukkit.bukkitCache.setRequestOnJoin(playerUUID);
- case REMOVE_REQUESTER -> HuskSyncBukkit.bukkitCache.removeRequestOnJoin(playerUUID);
- }
- }
- case CONNECTION_HANDSHAKE -> {
- UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
- String proxyBrand = message.getMessageDataElements()[1];
- if (serverUUID.equals(HuskSyncBukkit.serverUUID)) {
- HuskSyncBukkit.handshakeCompleted = true;
- log(Level.INFO, "Completed handshake with " + proxyBrand + " proxy (" + serverUUID + ")");
-
- // If there are any players awaiting a data update, request it
- for (UUID uuid : HuskSyncBukkit.bukkitCache.getAwaitingDataFetch()) {
- try {
- PlayerSetter.requestPlayerData(uuid);
- } catch (IOException e) {
- log(Level.SEVERE, "Failed to serialize handshake message data");
- }
- }
- }
- }
- case TERMINATE_HANDSHAKE -> {
- UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
- String proxyBrand = message.getMessageDataElements()[1];
- if (serverUUID.equals(HuskSyncBukkit.serverUUID)) {
- HuskSyncBukkit.handshakeCompleted = false;
- log(Level.WARNING, proxyBrand + " proxy has terminated communications; attempting to re-establish (" + serverUUID + ")");
-
- // Attempt to re-establish communications via another handshake
- Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, HuskSyncBukkit::establishRedisHandshake, 20);
- }
- }
- case DECODE_MPDB_DATA -> {
- UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
- String encodedData = message.getMessageDataElements()[1];
- Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
- if (serverUUID.equals(HuskSyncBukkit.serverUUID)) {
- try {
- MPDBPlayerData data = (MPDBPlayerData) RedisMessage.deserialize(encodedData);
- new RedisMessage(RedisMessage.MessageType.DECODED_MPDB_DATA_SET,
- new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
- RedisMessage.serialize(MPDBDeserializer.convertMPDBData(data)),
- data.playerName)
- .send();
- } catch (IOException | ClassNotFoundException e) {
- log(Level.SEVERE, "Failed to serialize encoded MPDB data");
- }
- }
- });
- }
- case API_DATA_RETURN -> {
- final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[0]);
- if (apiRequests.containsKey(requestUUID)) {
- try {
- final PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageDataElements()[1]);
- apiRequests.get(requestUUID).complete(data);
- } catch (IOException | ClassNotFoundException e) {
- log(Level.SEVERE, "Failed to serialize returned API-requested player data");
- }
- }
-
- }
- case API_DATA_CANCEL -> {
- final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[0]);
- // Cancel requests if no data could be found on the proxy
- if (apiRequests.containsKey(requestUUID)) {
- apiRequests.get(requestUUID).cancel(true);
- }
- }
- case RELOAD_CONFIG -> {
- plugin.reloadConfig();
- ConfigLoader.loadSettings(plugin.getConfig());
- }
- }
- } else {
- for (Player player : Bukkit.getOnlinePlayers()) {
- if (player.getUniqueId().equals(message.getMessageTarget().targetPlayerUUID())) {
- switch (message.getMessageType()) {
- case PLAYER_DATA_SET -> {
- if (HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled) return;
- try {
- // Deserialize the received PlayerData
- PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageData());
-
- // Set the player's data
- PlayerSetter.setPlayerFrom(player, data);
- } catch (IOException | ClassNotFoundException e) {
- log(Level.SEVERE, "Failed to deserialize PlayerData when handling data from the proxy");
- e.printStackTrace();
- }
- }
- case SEND_PLUGIN_INFORMATION -> {
- String proxyBrand = message.getMessageDataElements()[0];
- String proxyVersion = message.getMessageDataElements()[1];
- assert plugin.getDescription().getDescription() != null;
- player.spigot().sendMessage(new MineDown(MessageManager.PLUGIN_INFORMATION.toString()
- .replaceAll("%plugin_description%", plugin.getDescription().getDescription())
- .replaceAll("%proxy_brand%", proxyBrand)
- .replaceAll("%proxy_version%", proxyVersion)
- .replaceAll("%bukkit_brand%", Bukkit.getName())
- .replaceAll("%bukkit_version%", plugin.getDescription().getVersion()))
- .toComponent());
- }
- case OPEN_INVENTORY -> {
- // Get the name of the inventory owner
- String inventoryOwnerName = message.getMessageDataElements()[0];
-
- // Synchronously do inventory setting, etc
- Bukkit.getScheduler().runTask(plugin, () -> {
- try {
- // Get that player's data
- PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageDataElements()[1]);
-
- // Show the data to the player
- DataViewer.showData(player, new DataViewer.DataView(data, inventoryOwnerName, DataViewer.DataView.InventoryType.INVENTORY));
- } catch (IOException | ClassNotFoundException e) {
- log(Level.SEVERE, "Failed to deserialize PlayerData when handling inventory-see data from the proxy");
- e.printStackTrace();
- }
- });
- }
- case OPEN_ENDER_CHEST -> {
- // Get the name of the inventory owner
- String enderChestOwnerName = message.getMessageDataElements()[0];
-
- // Synchronously do inventory setting, etc
- Bukkit.getScheduler().runTask(plugin, () -> {
- try {
- // Get that player's data
- PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageDataElements()[1]);
-
- // Show the data to the player
- DataViewer.showData(player, new DataViewer.DataView(data, enderChestOwnerName, DataViewer.DataView.InventoryType.ENDER_CHEST));
- } catch (IOException | ClassNotFoundException e) {
- log(Level.SEVERE, "Failed to deserialize PlayerData when handling ender chest-see data from the proxy");
- e.printStackTrace();
- }
- });
- }
- }
- return;
- }
- }
- }
- }
-
- /**
- * Log to console
- *
- * @param level The {@link Level} to log
- * @param message Message to log
- */
- @Override
- public void log(Level level, String message) {
- plugin.getLogger().log(level, message);
- }
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/bukkit/migrator/MPDBDeserializer.java b/bukkit/src/main/java/net/william278/husksync/bukkit/migrator/MPDBDeserializer.java
deleted file mode 100644
index 3a7598ec..00000000
--- a/bukkit/src/main/java/net/william278/husksync/bukkit/migrator/MPDBDeserializer.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package net.william278.husksync.bukkit.migrator;
-
-import net.william278.husksync.HuskSyncBukkit;
-import net.william278.husksync.PlayerData;
-import net.william278.husksync.bukkit.data.DataSerializer;
-import net.william278.husksync.bukkit.util.PlayerSetter;
-import net.william278.husksync.migrator.MPDBPlayerData;
-import net.william278.mpdbconverter.MPDBConverter;
-import org.bukkit.Bukkit;
-import org.bukkit.event.inventory.InventoryType;
-import org.bukkit.inventory.Inventory;
-import org.bukkit.inventory.ItemStack;
-import org.bukkit.plugin.Plugin;
-
-import java.util.logging.Level;
-
-public class MPDBDeserializer {
-
- private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
-
- // Instance of MySqlPlayerDataBridge
- private static MPDBConverter mpdbConverter;
-
- public static void setMySqlPlayerDataBridge() {
- Plugin mpdbPlugin = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
- assert mpdbPlugin != null;
- mpdbConverter = MPDBConverter.getInstance(mpdbPlugin);
- }
-
- /**
- * Convert MySqlPlayerDataBridge ({@link MPDBPlayerData}) data to HuskSync's {@link PlayerData}
- *
- * @param mpdbPlayerData The {@link MPDBPlayerData} to convert
- * @return The converted {@link PlayerData}
- */
- public static PlayerData convertMPDBData(MPDBPlayerData mpdbPlayerData) {
- PlayerData playerData = PlayerData.DEFAULT_PLAYER_DATA(mpdbPlayerData.playerUUID);
- playerData.useDefaultData = false;
- if (!HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled) {
- plugin.getLogger().log(Level.SEVERE, "MySqlPlayerDataBridge is not installed, failed to serialize data!");
- return null;
- }
-
- // Convert the data
- try {
- // Set inventory contents
- Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
- if (!mpdbPlayerData.inventoryData.isEmpty() && !mpdbPlayerData.inventoryData.equalsIgnoreCase("none")) {
- PlayerSetter.setInventory(inventory, mpdbConverter.getItemStackFromSerializedData(mpdbPlayerData.inventoryData));
- }
-
- // Set armor (if there is data; MPDB stores empty data with literally the word "none". Obviously.)
- int armorSlot = 36;
- if (!mpdbPlayerData.armorData.isEmpty() && !mpdbPlayerData.armorData.equalsIgnoreCase("none")) {
- ItemStack[] armorItems = mpdbConverter.getItemStackFromSerializedData(mpdbPlayerData.armorData);
- for (ItemStack armorPiece : armorItems) {
- if (armorPiece != null) {
- inventory.setItem(armorSlot, armorPiece);
- }
- armorSlot++;
- }
-
- }
-
- // Now apply the contents and clear the temporary inventory variable
- playerData.setSerializedInventory(DataSerializer.serializeInventory(inventory.getContents()));
-
- // Set ender chest (again, if there is data)
- ItemStack[] enderChestData;
- if (!mpdbPlayerData.enderChestData.isEmpty() && !mpdbPlayerData.enderChestData.equalsIgnoreCase("none")) {
- enderChestData = mpdbConverter.getItemStackFromSerializedData(mpdbPlayerData.enderChestData);
- } else {
- enderChestData = new ItemStack[0];
- }
- playerData.setSerializedEnderChest(DataSerializer.serializeInventory(enderChestData));
-
- // Set experience
- playerData.setExpLevel(mpdbPlayerData.expLevel);
- playerData.setExpProgress(mpdbPlayerData.expProgress);
- playerData.setTotalExperience(mpdbPlayerData.totalExperience);
- } catch (Exception e) {
- plugin.getLogger().log(Level.WARNING, "Failed to convert MPDB data to HuskSync's format!");
- e.printStackTrace();
- }
- return playerData;
- }
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/bukkit/util/BukkitUpdateChecker.java b/bukkit/src/main/java/net/william278/husksync/bukkit/util/BukkitUpdateChecker.java
deleted file mode 100644
index c25327a1..00000000
--- a/bukkit/src/main/java/net/william278/husksync/bukkit/util/BukkitUpdateChecker.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package net.william278.husksync.bukkit.util;
-
-import net.william278.husksync.HuskSyncBukkit;
-import net.william278.husksync.util.UpdateChecker;
-
-import java.util.logging.Level;
-
-public class BukkitUpdateChecker extends UpdateChecker {
-
- private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
-
- public BukkitUpdateChecker() {
- super(plugin.getDescription().getVersion());
- }
-
- @Override
- public void log(Level level, String message) {
- plugin.getLogger().log(level, message);
- }
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/bukkit/util/PlayerSetter.java b/bukkit/src/main/java/net/william278/husksync/bukkit/util/PlayerSetter.java
deleted file mode 100644
index c2bcd52b..00000000
--- a/bukkit/src/main/java/net/william278/husksync/bukkit/util/PlayerSetter.java
+++ /dev/null
@@ -1,479 +0,0 @@
-package net.william278.husksync.bukkit.util;
-
-import net.william278.husksync.HuskSyncBukkit;
-import net.william278.husksync.PlayerData;
-import net.william278.husksync.Settings;
-import net.william278.husksync.bukkit.events.SyncCompleteEvent;
-import net.william278.husksync.bukkit.events.SyncEvent;
-import net.william278.husksync.bukkit.data.DataSerializer;
-import net.william278.husksync.bukkit.util.nms.AdvancementUtils;
-import net.william278.husksync.redis.RedisMessage;
-import org.bukkit.*;
-import org.bukkit.advancement.Advancement;
-import org.bukkit.advancement.AdvancementProgress;
-import org.bukkit.attribute.Attribute;
-import org.bukkit.entity.EntityType;
-import org.bukkit.entity.Player;
-import org.bukkit.inventory.Inventory;
-import org.bukkit.inventory.ItemStack;
-import org.bukkit.potion.PotionEffect;
-import org.bukkit.potion.PotionEffectType;
-
-import java.io.IOException;
-import java.util.*;
-import java.util.logging.Level;
-
-public class PlayerSetter {
-
- private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
-
- /**
- * Returns the new serialized PlayerData for a player.
- *
- * @param player The {@link Player} to get the new serialized PlayerData for
- * @return The {@link PlayerData}, serialized as a {@link String}
- * @throws IOException If the serialization fails
- */
- private static String getNewSerializedPlayerData(Player player) throws IOException {
- final double maxHealth = getMaxHealth(player); // Get the player's max health (used to determine health as well)
- return RedisMessage.serialize(new PlayerData(player.getUniqueId(),
- DataSerializer.serializeInventory(player.getInventory().getContents()),
- DataSerializer.serializeInventory(player.getEnderChest().getContents()),
- Math.min(player.getHealth(), maxHealth),
- maxHealth,
- player.isHealthScaled() ? player.getHealthScale() : 0D,
- player.getFoodLevel(),
- player.getSaturation(),
- player.getExhaustion(),
- player.getInventory().getHeldItemSlot(),
- DataSerializer.serializePotionEffects(getPlayerPotionEffects(player)),
- player.getTotalExperience(),
- player.getLevel(),
- player.getExp(),
- player.getGameMode().toString(),
- DataSerializer.getSerializedStatisticData(player),
- player.isFlying(),
- DataSerializer.getSerializedAdvancements(player),
- DataSerializer.getSerializedLocation(player)));
- }
-
- /**
- * Returns a {@link Player}'s maximum health, minus any health boost effects
- *
- * @param player The {@link Player} to get the maximum health of
- * @return The {@link Player}'s max health
- */
- private static double getMaxHealth(Player player) {
- double maxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).getBaseValue();
-
- // If the player has additional health bonuses from synchronised potion effects, subtract these from this number as they are synchronised separately
- if (player.hasPotionEffect(PotionEffectType.HEALTH_BOOST) && maxHealth > 20D) {
- PotionEffect healthBoostEffect = player.getPotionEffect(PotionEffectType.HEALTH_BOOST);
- assert healthBoostEffect != null;
- double healthBoostBonus = 4 * (healthBoostEffect.getAmplifier() + 1);
- maxHealth -= healthBoostBonus;
- }
- return maxHealth;
- }
-
- /**
- * Returns a {@link Player}'s active potion effects in a {@link PotionEffect} array
- *
- * @param player The {@link Player} to get the effects of
- * @return The {@link PotionEffect} array
- */
- private static PotionEffect[] getPlayerPotionEffects(Player player) {
- PotionEffect[] potionEffects = new PotionEffect[player.getActivePotionEffects().size()];
- int arrayIndex = 0;
- for (PotionEffect effect : player.getActivePotionEffects()) {
- potionEffects[arrayIndex] = effect;
- arrayIndex++;
- }
- return potionEffects;
- }
-
- /**
- * Update a {@link Player}'s data, sending it to the proxy
- *
- * @param player {@link Player} to send data to proxy
- * @param bounceBack whether the plugin should bounce-back the updated data to the player (used for server switching)
- */
- public static void updatePlayerData(Player player, boolean bounceBack) {
- // Send a redis message with the player's last updated PlayerData version UUID and their new PlayerData
- try {
- final String serializedPlayerData = getNewSerializedPlayerData(player);
- new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
- new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
- serializedPlayerData, Boolean.toString(bounceBack)).send();
- } catch (IOException e) {
- plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData update to the proxy", e);
- }
- }
-
- /**
- * Request a {@link Player}'s data from the proxy
- *
- * @param playerUUID The {@link UUID} of the {@link Player} to fetch PlayerData from
- * @throws IOException If the request Redis message data fails to serialize
- */
- public static void requestPlayerData(UUID playerUUID) throws IOException {
- new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_REQUEST,
- new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
- playerUUID.toString()).send();
- }
-
- /**
- * Set a player from their PlayerData, based on settings
- *
- * @param player The {@link Player} to set
- * @param dataToSet The {@link PlayerData} to assign to the player
- */
- public static void setPlayerFrom(Player player, PlayerData dataToSet) {
- Bukkit.getScheduler().runTask(plugin, () -> {
- // Handle the SyncEvent
- SyncEvent syncEvent = new SyncEvent(player, dataToSet);
- Bukkit.getPluginManager().callEvent(syncEvent);
- final PlayerData data = syncEvent.getData();
- if (syncEvent.isCancelled()) {
- return;
- }
-
- // If the data is flagged as being default data, skip setting
- if (data.useDefaultData) {
- HuskSyncBukkit.bukkitCache.removeAwaitingDataFetch(player.getUniqueId());
- return;
- }
-
- // Clear player
- player.getInventory().clear();
- player.getEnderChest().clear();
- player.setExp(0);
- player.setLevel(0);
-
- HuskSyncBukkit.bukkitCache.removeAwaitingDataFetch(player.getUniqueId());
-
- // Set the player's data from the PlayerData
- try {
- if (Settings.syncAdvancements) {
- List advancementRecords
- = DataSerializer.deserializeAdvancementData(data.getSerializedAdvancements());
-
- if (Settings.useNativeImplementation) {
- try {
- nativeSyncPlayerAdvancements(player, advancementRecords);
- } catch (Exception e) {
- plugin.getLogger().log(Level.WARNING,
- "Your server does not support a native implementation of achievements synchronization");
- plugin.getLogger().log(Level.WARNING,
- "Your server version is {0}. Please disable using native implementation!", Bukkit.getVersion());
-
- Settings.useNativeImplementation = false;
- setPlayerAdvancements(player, advancementRecords, data);
- plugin.getLogger().log(Level.SEVERE, e.getMessage(), e);
- }
- } else {
- setPlayerAdvancements(player, advancementRecords, data);
- }
- }
- if (Settings.syncInventories) {
- setPlayerInventory(player, DataSerializer.deserializeInventory(data.getSerializedInventory()));
- player.getInventory().setHeldItemSlot(data.getSelectedSlot());
- }
- if (Settings.syncEnderChests) {
- setPlayerEnderChest(player, DataSerializer.deserializeInventory(data.getSerializedEnderChest()));
- }
- if (Settings.syncHealth) {
- setPlayerHealth(player, data.getHealth(), data.getMaxHealth(), data.getHealthScale());
- }
- if (Settings.syncHunger) {
- player.setFoodLevel(data.getHunger());
- player.setSaturation(data.getSaturation());
- player.setExhaustion(data.getSaturationExhaustion());
- }
- if (Settings.syncExperience) {
- // This is also handled when syncing advancements to ensure its correct
- setPlayerExperience(player, data);
- }
- if (Settings.syncPotionEffects) {
- setPlayerPotionEffects(player, DataSerializer.deserializePotionEffects(data.getSerializedEffectData()));
- }
- if (Settings.syncStatistics) {
- setPlayerStatistics(player, DataSerializer.deserializeStatisticData(data.getSerializedStatistics()));
- }
- if (Settings.syncGameMode) {
- player.setGameMode(GameMode.valueOf(data.getGameMode()));
- }
- if (Settings.syncLocation) {
- setPlayerLocation(player, DataSerializer.deserializePlayerLocationData(data.getSerializedLocation()));
- }
- if (Settings.syncFlight) {
- if (data.isFlying()) {
- player.setAllowFlight(true);
- }
- player.setFlying(player.getAllowFlight() && data.isFlying());
- }
-
- // Handle the SyncCompleteEvent
- Bukkit.getPluginManager().callEvent(new SyncCompleteEvent(player, data));
- } catch (IOException | ClassNotFoundException e) {
- plugin.getLogger().log(Level.SEVERE, "Failed to deserialize PlayerData", e);
- }
- });
- }
-
- /**
- * Sets a player's ender chest from a set of {@link ItemStack}s
- *
- * @param player The player to set the inventory of
- * @param items The array of {@link ItemStack}s to set
- */
- private static void setPlayerEnderChest(Player player, ItemStack[] items) {
- setInventory(player.getEnderChest(), items);
- }
-
- /**
- * Sets a player's inventory from a set of {@link ItemStack}s
- *
- * @param player The player to set the inventory of
- * @param items The array of {@link ItemStack}s to set
- */
- private static void setPlayerInventory(Player player, ItemStack[] items) {
- setInventory(player.getInventory(), items);
- }
-
- /**
- * Sets an inventory's contents from an array of {@link ItemStack}s
- *
- * @param inventory The inventory to set
- * @param items The {@link ItemStack}s to fill it with
- */
- public static void setInventory(Inventory inventory, ItemStack[] items) {
- inventory.clear();
- int index = 0;
- for (ItemStack item : items) {
- if (item != null) {
- inventory.setItem(index, item);
- }
- index++;
- }
- }
-
- /**
- * Set a player's current potion effects from a set of {@link PotionEffect[]}
- *
- * @param player The player to set the potion effects of
- * @param effects The array of {@link PotionEffect}s to set
- */
- private static void setPlayerPotionEffects(Player player, PotionEffect[] effects) {
- for (PotionEffect effect : player.getActivePotionEffects()) {
- player.removePotionEffect(effect.getType());
- }
- for (PotionEffect effect : effects) {
- player.addPotionEffect(effect);
- }
- }
-
- private static void nativeSyncPlayerAdvancements(final Player player, final List advancementRecords) {
- final Object playerAdvancements = AdvancementUtils.getPlayerAdvancements(player);
-
- // Clear
- AdvancementUtils.clearPlayerAdvancements(playerAdvancements);
- AdvancementUtils.clearVisibleAdvancements(playerAdvancements);
-
- advancementRecords.forEach(advancementRecord -> {
- NamespacedKey namespacedKey = Objects.requireNonNull(
- NamespacedKey.fromString(advancementRecord.key()),
- "Invalid Namespaced key of " + advancementRecord.key()
- );
-
- Advancement bukkitAdvancement = Bukkit.getAdvancement(namespacedKey);
- if (bukkitAdvancement == null) {
- plugin.getLogger().log(Level.WARNING, "Ignored advancement '{0}' - it doesn't exist anymore?", namespacedKey);
- return;
- }
-
- Object advancement = AdvancementUtils.getHandle(bukkitAdvancement);
- Map criteriaList = advancementRecord.criteriaMap();
- {
- Map nativeCriteriaMap = new HashMap<>();
- criteriaList.forEach((criteria, date) ->
- nativeCriteriaMap.put(criteria, AdvancementUtils.newCriterionProgress(date))
- );
- Object nativeAdvancementProgress = AdvancementUtils.newAdvancementProgress(nativeCriteriaMap);
-
- AdvancementUtils.startProgress(playerAdvancements, advancement, nativeAdvancementProgress);
- }
- });
- AdvancementUtils.ensureAllVisible(playerAdvancements); // Set all completed advancement is visible
- AdvancementUtils.markPlayerAdvancementsFirst(playerAdvancements); // Mark the sending of visible advancement as the first
- }
-
- /**
- * Update a player's advancements and progress to match the advancementData
- *
- * @param player The player to set the advancements of
- * @param advancementData The ArrayList of {@link me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate}s to set
- */
- private static void setPlayerAdvancements(Player player, List advancementData, PlayerData data) {
- // Temporarily disable advancement announcing if needed
- boolean announceAdvancementUpdate = false;
- if (Boolean.TRUE.equals(player.getWorld().getGameRuleValue(GameRule.ANNOUNCE_ADVANCEMENTS))) {
- player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false);
- announceAdvancementUpdate = true;
- }
- final boolean finalAnnounceAdvancementUpdate = announceAdvancementUpdate;
-
- // Run async because advancement loading is very slow
- Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
-
- // Apply the advancements to the player
- final Iterator serverAdvancements = Bukkit.getServer().advancementIterator();
- while (serverAdvancements.hasNext()) { // Iterate through all advancements
- boolean correctExperienceCheck = false; // Determines whether the experience might have changed warranting an update
- Advancement advancement = serverAdvancements.next();
- AdvancementProgress playerProgress = player.getAdvancementProgress(advancement);
- for (me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate record : advancementData) {
- // If the advancement is one on the data
- if (record.key().equals(advancement.getKey().getNamespace() + ":" + advancement.getKey().getKey())) {
-
- // Award all criteria that the player does not have that they do on the cache
- ArrayList currentlyAwardedCriteria = new ArrayList<>(playerProgress.getAwardedCriteria());
- for (String awardCriteria : record.criteriaMap().keySet()) {
- if (!playerProgress.getAwardedCriteria().contains(awardCriteria)) {
- Bukkit.getScheduler().runTask(plugin, () -> player.getAdvancementProgress(advancement).awardCriteria(awardCriteria));
- correctExperienceCheck = true;
- }
- currentlyAwardedCriteria.remove(awardCriteria);
- }
-
- // Revoke all criteria that the player does have but should not
- for (String awardCriteria : currentlyAwardedCriteria) {
- Bukkit.getScheduler().runTask(plugin, () -> player.getAdvancementProgress(advancement).revokeCriteria(awardCriteria));
- }
- break;
- }
- }
-
- // Update the player's experience in case the advancement changed that
- if (correctExperienceCheck) {
- if (Settings.syncExperience) {
- setPlayerExperience(player, data);
- }
- }
- }
-
- // Re-enable announcing advancements (back on main thread again)
- Bukkit.getScheduler().runTask(plugin, () -> {
- if (finalAnnounceAdvancementUpdate) {
- player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, true);
- }
- });
- });
- }
-
- /**
- * Set a player's statistics (in the Statistic menu)
- *
- * @param player The player to set the statistics of
- * @param statisticData The {@link me.william278.husksync.bukkit.data.DataSerializer.StatisticData} to set
- */
- private static void setPlayerStatistics(Player player, me.william278.husksync.bukkit.data.DataSerializer.StatisticData statisticData) {
- // Set untyped statistics
- for (Statistic statistic : statisticData.untypedStatisticValues().keySet()) {
- player.setStatistic(statistic, statisticData.untypedStatisticValues().get(statistic));
- }
-
- // Set block statistics
- for (Statistic statistic : statisticData.blockStatisticValues().keySet()) {
- for (Material blockMaterial : statisticData.blockStatisticValues().get(statistic).keySet()) {
- player.setStatistic(statistic, blockMaterial, statisticData.blockStatisticValues().get(statistic).get(blockMaterial));
- }
- }
-
- // Set item statistics
- for (Statistic statistic : statisticData.itemStatisticValues().keySet()) {
- for (Material itemMaterial : statisticData.itemStatisticValues().get(statistic).keySet()) {
- player.setStatistic(statistic, itemMaterial, statisticData.itemStatisticValues().get(statistic).get(itemMaterial));
- }
- }
-
- // Set entity statistics
- for (Statistic statistic : statisticData.entityStatisticValues().keySet()) {
- for (EntityType entityType : statisticData.entityStatisticValues().get(statistic).keySet()) {
- player.setStatistic(statistic, entityType, statisticData.entityStatisticValues().get(statistic).get(entityType));
- }
- }
- }
-
- /**
- * Set a player's exp level, exp points & score
- *
- * @param player The {@link Player} to set
- * @param data The {@link PlayerData} to set them
- */
- private static void setPlayerExperience(Player player, PlayerData data) {
- player.setTotalExperience(data.getTotalExperience());
- player.setLevel(data.getExpLevel());
- player.setExp(data.getExpProgress());
- }
-
- /**
- * Set a player's location from {@link me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation} data
- *
- * @param player The {@link Player} to teleport
- * @param location The {@link me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation}
- */
- private static void setPlayerLocation(Player player, me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation location) {
- // Don't teleport if the location is invalid
- if (location == null) {
- return;
- }
-
- // Determine the world; if the names match, use that
- World world = Bukkit.getWorld(location.worldName());
- if (world == null) {
-
- // If the names don't match, find the corresponding world with the same dimension environment
- for (World worldOnServer : Bukkit.getWorlds()) {
- if (worldOnServer.getEnvironment().equals(location.environment())) {
- world = worldOnServer;
- }
- }
-
- // If that still fails, return
- if (world == null) {
- return;
- }
- }
-
- // Teleport the player
- player.teleport(new Location(world, location.x(), location.y(), location.z(), location.yaw(), location.pitch()));
- }
-
- /**
- * Correctly set a {@link Player}'s health data
- *
- * @param player The {@link Player} to set
- * @param health Health to set to the player
- * @param maxHealth Max health to set to the player
- * @param healthScale Health scaling to apply to the player
- */
- private static void setPlayerHealth(Player player, double health, double maxHealth, double healthScale) {
- // Set max health
- if (maxHealth != 0D) {
- Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).setBaseValue(maxHealth);
- }
-
- // Set health
- double currentHealth = player.getHealth();
- if (health != currentHealth) player.setHealth(currentHealth > maxHealth ? maxHealth : health);
-
- // Set health scaling if needed
- if (healthScale != 0D) {
- player.setHealthScale(healthScale);
- } else {
- player.setHealthScale(maxHealth);
- }
- player.setHealthScaled(healthScale != 0D);
- }
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/bukkit/util/nms/AdvancementUtils.java b/bukkit/src/main/java/net/william278/husksync/bukkit/util/nms/AdvancementUtils.java
deleted file mode 100644
index f8da0527..00000000
--- a/bukkit/src/main/java/net/william278/husksync/bukkit/util/nms/AdvancementUtils.java
+++ /dev/null
@@ -1,146 +0,0 @@
-package net.william278.husksync.bukkit.util.nms;
-
-import net.william278.husksync.util.ThrowSupplier;
-import org.bukkit.advancement.Advancement;
-import org.bukkit.entity.Player;
-
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.Date;
-import java.util.Map;
-import java.util.Set;
-
-public class AdvancementUtils {
-
- public final static Class> PLAYER_ADVANCEMENT;
- private final static Field PLAYER_ADVANCEMENTS_MAP;
- private final static Field PLAYER_VISIBLE_SET;
- private final static Field PLAYER_ADVANCEMENTS;
- private final static Field CRITERIA_MAP;
- private final static Field CRITERIA_DATE;
- private final static Field IS_FIRST_PACKET;
- private final static Method GET_HANDLE;
- private final static Method START_PROGRESS;
- private final static Method ENSURE_ALL_VISIBLE;
- private final static Class> ADVANCEMENT_PROGRESS;
- private final static Class> CRITERION_PROGRESS;
-
- static {
- Class> SERVER_PLAYER = MinecraftVersionUtils.getMinecraftClass("level.EntityPlayer");
- PLAYER_ADVANCEMENTS = ThrowSupplier.get(() -> SERVER_PLAYER.getDeclaredField("cs"));
- PLAYER_ADVANCEMENTS.setAccessible(true);
-
- Class> CRAFT_ADVANCEMENT = MinecraftVersionUtils.getBukkitClass("advancement.CraftAdvancement");
- GET_HANDLE = ThrowSupplier.get(() -> CRAFT_ADVANCEMENT.getDeclaredMethod("getHandle"));
-
- ADVANCEMENT_PROGRESS = ThrowSupplier.get(() -> Class.forName("net.minecraft.advancements.AdvancementProgress"));
- CRITERIA_MAP = ThrowSupplier.get(() -> ADVANCEMENT_PROGRESS.getDeclaredField("a"));
- CRITERIA_MAP.setAccessible(true);
-
- CRITERION_PROGRESS = ThrowSupplier.get(() -> Class.forName("net.minecraft.advancements.CriterionProgress"));
- CRITERIA_DATE = ThrowSupplier.get(() -> CRITERION_PROGRESS.getDeclaredField("b"));
- CRITERIA_DATE.setAccessible(true);
-
- Class> ADVANCEMENT = ThrowSupplier.get(() -> Class.forName("net.minecraft.advancements.Advancement"));
-
- PLAYER_ADVANCEMENT = MinecraftVersionUtils.getMinecraftClass("AdvancementDataPlayer");
- PLAYER_ADVANCEMENTS_MAP = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredField("h"));
- PLAYER_ADVANCEMENTS_MAP.setAccessible(true);
-
- PLAYER_VISIBLE_SET = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredField("i"));
- PLAYER_VISIBLE_SET.setAccessible(true);
-
- START_PROGRESS = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredMethod("a", ADVANCEMENT, ADVANCEMENT_PROGRESS));
- START_PROGRESS.setAccessible(true);
-
- ENSURE_ALL_VISIBLE = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredMethod("c"));
- ENSURE_ALL_VISIBLE.setAccessible(true);
-
- IS_FIRST_PACKET = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredField("n"));
- IS_FIRST_PACKET.setAccessible(true);
- }
-
- public static void markPlayerAdvancementsFirst(final Object playerAdvancements) {
- try {
- IS_FIRST_PACKET.set(playerAdvancements, true);
- } catch (IllegalAccessException e) {
- throw new RuntimeException(e.getMessage(), e);
- }
- }
-
- public static Object getPlayerAdvancements(Player player) {
- Object nativePlayer = EntityUtils.getHandle(player);
- try {
- return PLAYER_ADVANCEMENTS.get(nativePlayer);
- } catch (IllegalAccessException e) {
- throw new RuntimeException(e.getMessage(), e);
- }
- }
-
- public static void clearPlayerAdvancements(final Object playerAdvancement) {
- try {
- ((Map, ?>) PLAYER_ADVANCEMENTS_MAP.get(playerAdvancement))
- .clear();
- } catch (IllegalAccessException e) {
- throw new RuntimeException(e.getMessage(), e);
- }
- }
-
- public static Object getHandle(Advancement advancement) {
- try {
- return GET_HANDLE.invoke(advancement);
- } catch (IllegalAccessException | InvocationTargetException e) {
- throw new RuntimeException(e.getMessage(), e);
- }
- }
-
- public static Object newCriterionProgress(final Date date) {
- try {
- Object nativeCriterionProgress = CRITERION_PROGRESS.getDeclaredConstructor().newInstance();
- CRITERIA_DATE.set(nativeCriterionProgress, date);
- return nativeCriterionProgress;
- } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
- throw new RuntimeException(e.getMessage(), e);
- }
- }
-
- @SuppressWarnings("unchecked") // Suppress unchecked cast warnings here
- public static Object newAdvancementProgress(final Map criteria) {
- try {
- Object nativeAdvancementProgress = ADVANCEMENT_PROGRESS.getDeclaredConstructor().newInstance();
-
- final Map criteriaMap = (Map) CRITERIA_MAP.get(nativeAdvancementProgress);
- criteriaMap.putAll(criteria);
-
- return nativeAdvancementProgress;
- } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
- throw new RuntimeException(e.getMessage(), e);
- }
- }
-
- public static void startProgress(final Object playerAdvancements, final Object advancement, final Object advancementProgress) {
- try {
- START_PROGRESS.invoke(playerAdvancements, advancement, advancementProgress);
- } catch (IllegalAccessException | InvocationTargetException e) {
- throw new RuntimeException(e.getMessage(), e);
- }
- }
-
- public static void ensureAllVisible(final Object playerAdvancements) {
- try {
- ENSURE_ALL_VISIBLE.invoke(playerAdvancements);
- } catch (IllegalAccessException | InvocationTargetException e) {
- throw new RuntimeException(e.getMessage(), e);
- }
- }
-
- public static void clearVisibleAdvancements(final Object playerAdvancements) {
- try {
- ((Set>) PLAYER_VISIBLE_SET.get(playerAdvancements))
- .clear();
- } catch (IllegalAccessException e) {
- throw new RuntimeException(e.getMessage(), e);
- }
- }
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/bukkit/util/nms/EntityUtils.java b/bukkit/src/main/java/net/william278/husksync/bukkit/util/nms/EntityUtils.java
deleted file mode 100644
index 9eaeaea6..00000000
--- a/bukkit/src/main/java/net/william278/husksync/bukkit/util/nms/EntityUtils.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package net.william278.husksync.bukkit.util.nms;
-
-import net.william278.husksync.util.ThrowSupplier;
-import org.bukkit.entity.LivingEntity;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
-public class EntityUtils {
-
- private final static Method GET_HANDLE;
-
- static {
- final Class> CRAFT_ENTITY = MinecraftVersionUtils.getBukkitClass("entity.CraftEntity");
- GET_HANDLE = ThrowSupplier.get(() -> CRAFT_ENTITY.getDeclaredMethod("getHandle"));
- }
-
- public static Object getHandle(LivingEntity livingEntity) throws RuntimeException {
- try {
- return GET_HANDLE.invoke(livingEntity);
- } catch (IllegalAccessException | InvocationTargetException e) {
- throw new RuntimeException(e.getMessage(), e);
- }
- }
-
-}
diff --git a/bukkit/src/main/java/net/william278/husksync/bukkit/util/nms/MinecraftVersionUtils.java b/bukkit/src/main/java/net/william278/husksync/bukkit/util/nms/MinecraftVersionUtils.java
deleted file mode 100644
index 45069210..00000000
--- a/bukkit/src/main/java/net/william278/husksync/bukkit/util/nms/MinecraftVersionUtils.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package net.william278.husksync.bukkit.util.nms;
-
-import net.william278.husksync.util.ThrowSupplier;
-import net.william278.husksync.util.VersionUtils;
-import org.bukkit.Bukkit;
-
-public class MinecraftVersionUtils {
-
- public final static String CRAFTBUKKIT_PACKAGE_PATH = Bukkit.getServer().getClass().getPackage().getName();
-
- public final static String PACKAGE_VERSION = CRAFTBUKKIT_PACKAGE_PATH.split("\\.")[3];
- public final static VersionUtils.Version SERVER_VERSION
- = VersionUtils.Version.of(Bukkit.getBukkitVersion().split("-")[0]);
- public final static String MINECRAFT_PACKAGE = SERVER_VERSION.compareTo(VersionUtils.Version.of("1.17")) < 0 ?
- "net.minecraft.server.".concat(PACKAGE_VERSION) : "net.minecraft.server";
-
- public static Class> getBukkitClass(String path) {
- return ThrowSupplier.get(() -> Class.forName(CRAFTBUKKIT_PACKAGE_PATH.concat(".").concat(path)));
- }
-
- public static Class> getMinecraftClass(String path) {
- return ThrowSupplier.get(() -> Class.forName(MINECRAFT_PACKAGE.concat(".").concat(path)));
- }
-
-}
diff --git a/bukkit/src/main/resources/config.yml b/bukkit/src/main/resources/config.yml
deleted file mode 100644
index 1fb6a37b..00000000
--- a/bukkit/src/main/resources/config.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-redis_settings:
- host: 'localhost'
- port: 6379
- password: ''
- use_ssl: false
-synchronisation_settings:
- inventories: true
- ender_chests: true
- health: true
- hunger: true
- experience: true
- potion_effects: true
- statistics: true
- game_mode: true
- advancements: true
- location: false
- flight: false
-cluster_id: 'main'
-check_for_updates: true
-synchronization_timeout_retry_delay: 15
-save_on_world_save: true
-native_advancement_synchronization: false
\ No newline at end of file
diff --git a/bungeecord/src/main/java/net/william278/husksync/HuskSyncBungeeCord.java b/bungeecord/src/main/java/net/william278/husksync/HuskSyncBungeeCord.java
deleted file mode 100644
index 9474567b..00000000
--- a/bungeecord/src/main/java/net/william278/husksync/HuskSyncBungeeCord.java
+++ /dev/null
@@ -1,171 +0,0 @@
-package net.william278.husksync;
-
-import net.byteflux.libby.BungeeLibraryManager;
-import net.byteflux.libby.Library;
-import net.md_5.bungee.api.ProxyServer;
-import net.md_5.bungee.api.plugin.Plugin;
-import net.william278.husksync.bungeecord.command.BungeeCommand;
-import net.william278.husksync.bungeecord.config.ConfigLoader;
-import net.william278.husksync.bungeecord.config.ConfigManager;
-import net.william278.husksync.bungeecord.listener.BungeeEventListener;
-import net.william278.husksync.bungeecord.listener.BungeeRedisListener;
-import net.william278.husksync.bungeecord.util.BungeeLogger;
-import net.william278.husksync.bungeecord.util.BungeeUpdateChecker;
-import net.william278.husksync.migrator.MPDBMigrator;
-import net.william278.husksync.proxy.data.DataManager;
-import net.william278.husksync.redis.RedisMessage;
-import net.william278.husksync.util.Logger;
-import org.bstats.bungeecord.Metrics;
-
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.Objects;
-import java.util.logging.Level;
-
-public final class HuskSyncBungeeCord extends Plugin {
-
- // BungeeCord bStats ID (different to Bukkit)
- private static final int METRICS_ID = 13141;
-
- private static HuskSyncBungeeCord instance;
-
- public static HuskSyncBungeeCord getInstance() {
- return instance;
- }
-
- // Whether the plugin is ready to accept redis messages
- public static boolean readyForRedis = false;
-
- // Whether the plugin is in the process of disabling and should skip responding to handshake confirmations
- public static boolean isDisabling = false;
-
- /**
- * Set of all the {@link Server}s that have completed the synchronisation handshake with HuskSync on the proxy
- */
- public static HashSet synchronisedServers;
-
- public static DataManager dataManager;
-
- public static MPDBMigrator mpdbMigrator;
-
- public static BungeeRedisListener redisListener;
-
- private Logger logger;
-
- public Logger getBungeeLogger() {
- return logger;
- }
-
- @Override
- public void onLoad() {
- instance = this;
- logger = new BungeeLogger(getLogger());
- fetchDependencies();
- }
-
- @Override
- public void onEnable() {
- // Plugin startup logic
- synchronisedServers = new HashSet<>();
-
- // Load config
- ConfigManager.loadConfig();
-
- // Load settings from config
- ConfigLoader.loadSettings(Objects.requireNonNull(ConfigManager.getConfig()));
-
- // Load messages
- ConfigManager.loadMessages();
-
- // Load locales from messages
- ConfigLoader.loadMessageStrings(Objects.requireNonNull(ConfigManager.getMessages()));
-
- // Do update checker
- if (Settings.automaticUpdateChecks) {
- new BungeeUpdateChecker(getDescription().getVersion()).logToConsole();
- }
-
- // Setup data manager
- dataManager = new DataManager(getBungeeLogger(), getDataFolder());
-
- // Ensure the data manager initialized correctly
- if (dataManager.hasFailedInitialization) {
- getBungeeLogger().severe("Failed to initialize the HuskSync database(s).\n" +
- "HuskSync will now abort loading itself (" + getProxy().getName() + ") v" + getDescription().getVersion());
- }
-
- // Setup player data cache
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- dataManager.playerDataCache.put(cluster, new DataManager.PlayerDataCache());
- }
-
- // Initialize the redis listener
- redisListener = new BungeeRedisListener();
-
- // Register listener
- getProxy().getPluginManager().registerListener(this, new BungeeEventListener());
-
- // Register command
- getProxy().getPluginManager().registerCommand(this, new BungeeCommand());
-
- // Prepare the migrator for use if needed
- mpdbMigrator = new MPDBMigrator(getBungeeLogger());
-
- // Initialize bStats metrics
- try {
- new Metrics(this, METRICS_ID);
- } catch (Exception e) {
- getBungeeLogger().info("Skipped metrics initialization");
- }
-
- // Log to console
- getBungeeLogger().info("Enabled HuskSync (" + getProxy().getName() + ") v" + getDescription().getVersion());
-
- // Mark as ready for redis message processing
- readyForRedis = true;
- }
-
- @Override
- public void onDisable() {
- // Plugin shutdown logic
- isDisabling = true;
-
- // Send terminating handshake message
- for (Server server : synchronisedServers) {
- try {
- new RedisMessage(RedisMessage.MessageType.TERMINATE_HANDSHAKE,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, server.clusterId()),
- server.serverUUID().toString(),
- ProxyServer.getInstance().getName()).send();
- } catch (IOException e) {
- getBungeeLogger().log(Level.SEVERE, "Failed to serialize Redis message for handshake termination", e);
- }
- }
-
- dataManager.closeDatabases();
-
- // Log to console
- getBungeeLogger().info("Disabled HuskSync (" + getProxy().getName() + ") v" + getDescription().getVersion());
- }
-
- // Load dependencies
- private void fetchDependencies() {
- BungeeLibraryManager manager = new BungeeLibraryManager(getInstance());
-
- Library mySqlLib = Library.builder()
- .groupId("mysql")
- .artifactId("mysql-connector-java")
- .version("8.0.29")
- .build();
-
- Library sqLiteLib = Library.builder()
- .groupId("org.xerial")
- .artifactId("sqlite-jdbc")
- .version("3.36.0.3")
- .build();
-
- manager.addMavenCentral();
- manager.loadLibrary(mySqlLib);
- manager.loadLibrary(sqLiteLib);
- }
-}
diff --git a/bungeecord/src/main/java/net/william278/husksync/bungeecord/command/BungeeCommand.java b/bungeecord/src/main/java/net/william278/husksync/bungeecord/command/BungeeCommand.java
deleted file mode 100644
index f3064ffb..00000000
--- a/bungeecord/src/main/java/net/william278/husksync/bungeecord/command/BungeeCommand.java
+++ /dev/null
@@ -1,424 +0,0 @@
-package net.william278.husksync.bungeecord.command;
-
-import de.themoep.minedown.MineDown;
-import net.william278.husksync.HuskSyncBungeeCord;
-import net.william278.husksync.PlayerData;
-import net.william278.husksync.Server;
-import net.william278.husksync.Settings;
-import net.william278.husksync.bungeecord.config.ConfigLoader;
-import net.william278.husksync.bungeecord.config.ConfigManager;
-import net.william278.husksync.bungeecord.util.BungeeUpdateChecker;
-import net.william278.husksync.migrator.MPDBMigrator;
-import net.william278.husksync.proxy.command.HuskSyncCommand;
-import net.william278.husksync.redis.RedisMessage;
-import net.william278.husksync.util.MessageManager;
-import net.md_5.bungee.api.CommandSender;
-import net.md_5.bungee.api.ProxyServer;
-import net.md_5.bungee.api.connection.ProxiedPlayer;
-import net.md_5.bungee.api.plugin.Command;
-import net.md_5.bungee.api.plugin.TabExecutor;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Locale;
-import java.util.Objects;
-import java.util.logging.Level;
-import java.util.stream.Collectors;
-
-public class BungeeCommand extends Command implements TabExecutor, HuskSyncCommand {
-
- private final static HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
-
- public BungeeCommand() {
- super("husksync", null, "hs");
- }
-
- @Override
- public void execute(CommandSender sender, String[] args) {
- if (sender instanceof ProxiedPlayer player) {
- if (HuskSyncBungeeCord.synchronisedServers.size() == 0) {
- player.sendMessage(new MineDown(MessageManager.getMessage("error_no_servers_proxied")).toComponent());
- return;
- }
- if (args.length >= 1) {
- switch (args[0].toLowerCase(Locale.ROOT)) {
- case "about", "info" -> sendAboutInformation(player);
- case "update" -> {
- if (!player.hasPermission("husksync.command.inventory")) {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
- return;
- }
- sender.sendMessage(new MineDown("[Checking for HuskSync updates...](gray)").toComponent());
- ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
- // Check Bukkit servers needing updates
- int updatesNeeded = 0;
- String bukkitBrand = "Spigot";
- String bukkitVersion = "1.0";
- for (Server server : HuskSyncBungeeCord.synchronisedServers) {
- BungeeUpdateChecker updateChecker = new BungeeUpdateChecker(server.huskSyncVersion());
- if (!updateChecker.isUpToDate()) {
- updatesNeeded++;
- bukkitBrand = server.serverBrand();
- bukkitVersion = server.huskSyncVersion();
- }
- }
-
- // Check Bungee servers needing updates and send message
- BungeeUpdateChecker proxyUpdateChecker = new BungeeUpdateChecker(plugin.getDescription().getVersion());
- if (proxyUpdateChecker.isUpToDate() && updatesNeeded == 0) {
- sender.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date, running Version " + proxyUpdateChecker.getLatestVersion() + "](#00fb9a)").toComponent());
- } else {
- sender.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| Your server(s) are not up-to-date:](#00fb9a)").toComponent());
- if (!proxyUpdateChecker.isUpToDate()) {
- sender.sendMessage(new MineDown("[•](white) [HuskSync on the " + ProxyServer.getInstance().getName() + " proxy is outdated (Latest: " + proxyUpdateChecker.getLatestVersion() + ", Running: " + proxyUpdateChecker.getCurrentVersion() + ")](#00fb9a)").toComponent());
- }
- if (updatesNeeded > 0) {
- sender.sendMessage(new MineDown("[•](white) [HuskSync on " + updatesNeeded + " connected " + bukkitBrand + " server(s) are outdated (Latest: " + proxyUpdateChecker.getLatestVersion() + ", Running: " + bukkitVersion + ")](#00fb9a)").toComponent());
- }
- sender.sendMessage(new MineDown("[•](white) [Download links:](#00fb9a) [[⏩ Spigot]](gray open_url=https://www.spigotmc.org/resources/husktowns.92672/updates) [•](#262626) [[⏩ Polymart]](gray open_url=https://polymart.org/resource/husktowns.1056/updates)").toComponent());
- }
- });
- }
- case "invsee", "openinv", "inventory" -> {
- if (!player.hasPermission("husksync.command.inventory")) {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
- return;
- }
- String clusterId;
- if (Settings.clusters.size() > 1) {
- if (args.length == 3) {
- clusterId = args[2];
- } else {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
- return;
- }
- } else {
- clusterId = "main";
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- clusterId = cluster.clusterId();
- break;
- }
- }
- if (args.length == 2 || args.length == 3) {
- String playerName = args[1];
- openInventory(player, playerName, clusterId);
- } else {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax").replaceAll("%1%",
- "/husksync invsee ")).toComponent());
- }
- }
- case "echest", "enderchest" -> {
- if (!player.hasPermission("husksync.command.ender_chest")) {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
- return;
- }
- String clusterId;
- if (Settings.clusters.size() > 1) {
- if (args.length == 3) {
- clusterId = args[2];
- } else {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
- return;
- }
- } else {
- clusterId = "main";
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- clusterId = cluster.clusterId();
- break;
- }
- }
- if (args.length == 2 || args.length == 3) {
- String playerName = args[1];
- openEnderChest(player, playerName, clusterId);
- } else {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax")
- .replaceAll("%1%", "/husksync echest ")).toComponent());
- }
- }
- case "migrate" -> {
- if (!player.hasPermission("husksync.command.admin")) {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
- return;
- }
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_console_command_only")
- .replaceAll("%1%", ProxyServer.getInstance().getName())).toComponent());
- }
- case "status" -> {
- if (!player.hasPermission("husksync.command.admin")) {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
- return;
- }
- int playerDataSize = 0;
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- playerDataSize += HuskSyncBungeeCord.dataManager.playerDataCache.get(cluster).playerData.size();
- }
- sender.sendMessage(new MineDown(MessageManager.PLUGIN_STATUS.toString()
- .replaceAll("%1%", String.valueOf(HuskSyncBungeeCord.synchronisedServers.size()))
- .replaceAll("%2%", String.valueOf(playerDataSize))).toComponent());
- }
- case "reload" -> {
- if (!player.hasPermission("husksync.command.admin")) {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
- return;
- }
- ConfigManager.loadConfig();
- ConfigLoader.loadSettings(Objects.requireNonNull(ConfigManager.getConfig()));
-
- ConfigManager.loadMessages();
- ConfigLoader.loadMessageStrings(Objects.requireNonNull(ConfigManager.getMessages()));
-
- // Send reload request to all bukkit servers
- try {
- new RedisMessage(RedisMessage.MessageType.RELOAD_CONFIG,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null),
- "reload")
- .send();
- } catch (IOException e) {
- plugin.getBungeeLogger().log(Level.WARNING, "Failed to serialize reload notification message data");
- }
-
- sender.sendMessage(new MineDown(MessageManager.getMessage("reload_complete")).toComponent());
- }
- default -> sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax").replaceAll("%1%",
- "/husksync ")).toComponent());
- }
- } else {
- sendAboutInformation(player);
- }
- } else {
- // Database migration wizard
- if (args.length >= 1) {
- if (args[0].equalsIgnoreCase("migrate")) {
- MPDBMigrator migrator = HuskSyncBungeeCord.mpdbMigrator;
- if (args.length == 1) {
- sender.sendMessage(new MineDown(
- """
- === MySQLPlayerDataBridge Migration Wizard ==========
- This will migrate data from the MySQLPlayerDataBridge
- plugin to HuskSync.
-
- Data that will be migrated:
- - Inventories
- - Ender Chests
- - Experience points
-
- Other non-vital data, such as current health, hunger
- & potion effects will not be migrated to ensure that
- migration does not take an excessive amount of time.
-
- To do this, you need to have MySqlPlayerDataBridge
- and HuskSync installed on one Spigot server as well
- as HuskSync installed on the proxy (which you have)
-
- >To proceed, type: husksync migrate setup""").toComponent());
- } else {
- switch (args[1].toLowerCase()) {
- case "setup" -> sender.sendMessage(new MineDown(
- """
- === MySQLPlayerDataBridge Migration Wizard ==========
- The following database settings will be used.
- Please make sure they match the correct settings to
- access your MySQLPlayerDataBridge Data
-
- sourceHost: %1%
- sourcePort: %2%
- sourceDatabase: %3%
- sourceUsername: %4%
- sourcePassword: %5%
-
- sourceInventoryTableName: %6%
- sourceEnderChestTableName: %7%
- sourceExperienceTableName: %8%
-
- targetCluster: %9%
-
- To change a setting, type:
- husksync migrate setting
-
- Please ensure no players are logged in to the network
- and that at least one Spigot server is online with
- both HuskSync AND MySqlPlayerDataBridge installed AND
- that the server has been configured with the correct
- Redis credentials.
-
- Warning: Data will be saved to your configured data
- source, which is currently a %10% database.
- Please make sure you are happy with this, or stop
- the proxy server and edit this in config.yml
-
- Warning: Migration will overwrite any current data
- saved by HuskSync. It will not, however, delete any
- data from the source MySQLPlayerDataBridge database.
-
- >When done, type: husksync migrate start"""
- .replaceAll("%1%", migrator.migrationSettings.sourceHost)
- .replaceAll("%2%", String.valueOf(migrator.migrationSettings.sourcePort))
- .replaceAll("%3%", migrator.migrationSettings.sourceDatabase)
- .replaceAll("%4%", migrator.migrationSettings.sourceUsername)
- .replaceAll("%5%", migrator.migrationSettings.sourcePassword)
- .replaceAll("%6%", migrator.migrationSettings.inventoryDataTable)
- .replaceAll("%7%", migrator.migrationSettings.enderChestDataTable)
- .replaceAll("%8%", migrator.migrationSettings.expDataTable)
- .replaceAll("%9%", migrator.migrationSettings.targetCluster)
- .replaceAll("%10%", Settings.dataStorageType.toString())
- ).toComponent());
- case "setting" -> {
- if (args.length == 4) {
- String value = args[3];
- switch (args[2]) {
- case "sourceHost", "host" -> migrator.migrationSettings.sourceHost = value;
- case "sourcePort", "port" -> {
- try {
- migrator.migrationSettings.sourcePort = Integer.parseInt(value);
- } catch (NumberFormatException e) {
- sender.sendMessage(new MineDown("Error: Invalid value; port must be a number").toComponent());
- return;
- }
- }
- case "sourceDatabase", "database" -> migrator.migrationSettings.sourceDatabase = value;
- case "sourceUsername", "username" -> migrator.migrationSettings.sourceUsername = value;
- case "sourcePassword", "password" -> migrator.migrationSettings.sourcePassword = value;
- case "sourceInventoryTableName", "inventoryTableName", "inventoryTable" -> migrator.migrationSettings.inventoryDataTable = value;
- case "sourceEnderChestTableName", "enderChestTableName", "enderChestTable" -> migrator.migrationSettings.enderChestDataTable = value;
- case "sourceExperienceTableName", "experienceTableName", "experienceTable" -> migrator.migrationSettings.expDataTable = value;
- case "targetCluster", "cluster" -> migrator.migrationSettings.targetCluster = value;
- default -> {
- sender.sendMessage(new MineDown("Error: Invalid setting; please use \"husksync migrate setup\" to view a list").toComponent());
- return;
- }
- }
- sender.sendMessage(new MineDown("Successfully updated setting: \"" + args[2] + "\" --> \"" + value + "\"").toComponent());
- } else {
- sender.sendMessage(new MineDown("Error: Invalid usage. Syntax: husksync migrate setting ").toComponent());
- }
- }
- case "start" -> {
- sender.sendMessage(new MineDown("Starting MySQLPlayerDataBridge migration!...").toComponent());
-
- // If the migrator is ready, execute the migration asynchronously
- if (HuskSyncBungeeCord.mpdbMigrator.readyToMigrate(ProxyServer.getInstance().getOnlineCount(),
- HuskSyncBungeeCord.synchronisedServers)) {
- ProxyServer.getInstance().getScheduler().runAsync(plugin, () ->
- HuskSyncBungeeCord.mpdbMigrator.executeMigrationOperations(HuskSyncBungeeCord.dataManager,
- HuskSyncBungeeCord.synchronisedServers, HuskSyncBungeeCord.redisListener));
- }
- }
- default -> sender.sendMessage(new MineDown("Error: Invalid argument for migration. Use \"husksync migrate\" to start the process").toComponent());
- }
- }
- return;
- }
- }
- sender.sendMessage(new MineDown("Error: Invalid syntax. Usage: husksync migrate ").toComponent());
- }
- }
-
- // View the inventory of a player specified by their name
- private void openInventory(ProxiedPlayer viewer, String targetPlayerName, String clusterId) {
- if (viewer.getName().equalsIgnoreCase(targetPlayerName)) {
- viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_inventory")).toComponent());
- return;
- }
- if (ProxyServer.getInstance().getPlayer(targetPlayerName) != null) {
- viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_inventory_online")).toComponent());
- return;
- }
- ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- if (!cluster.clusterId().equals(clusterId)) continue;
- PlayerData playerData = HuskSyncBungeeCord.dataManager.getPlayerDataByName(targetPlayerName, cluster.clusterId());
- if (playerData == null) {
- viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent());
- return;
- }
- try {
- new RedisMessage(RedisMessage.MessageType.OPEN_INVENTORY,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId(), null),
- targetPlayerName, RedisMessage.serialize(playerData))
- .send();
- viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_inventory_of").replaceAll("%1%",
- targetPlayerName)).toComponent());
- } catch (IOException e) {
- plugin.getBungeeLogger().log(Level.WARNING, "Failed to serialize inventory-see player data", e);
- }
- return;
- }
- viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
- });
- }
-
- // View the ender chest of a player specified by their name
- public void openEnderChest(ProxiedPlayer viewer, String targetPlayerName, String clusterId) {
- if (viewer.getName().equalsIgnoreCase(targetPlayerName)) {
- viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_ender_chest")).toComponent());
- return;
- }
- if (ProxyServer.getInstance().getPlayer(targetPlayerName) != null) {
- viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_ender_chest_online")).toComponent());
- return;
- }
- ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- if (!cluster.clusterId().equals(clusterId)) continue;
- PlayerData playerData = HuskSyncBungeeCord.dataManager.getPlayerDataByName(targetPlayerName, cluster.clusterId());
- if (playerData == null) {
- viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent());
- return;
- }
- try {
- new RedisMessage(RedisMessage.MessageType.OPEN_ENDER_CHEST,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId(), null),
- targetPlayerName, RedisMessage.serialize(playerData))
- .send();
- viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_ender_chest_of").replaceAll("%1%",
- targetPlayerName)).toComponent());
- } catch (IOException e) {
- plugin.getBungeeLogger().log(Level.WARNING, "Failed to serialize inventory-see player data", e);
- }
- return;
- }
- viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
- });
- }
-
- /**
- * Send information about the plugin
- *
- * @param player The player to send it to
- */
- private void sendAboutInformation(ProxiedPlayer player) {
- try {
- new RedisMessage(RedisMessage.MessageType.SEND_PLUGIN_INFORMATION,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, player.getUniqueId(), null),
- plugin.getProxy().getName(), plugin.getDescription().getVersion()).send();
- } catch (IOException e) {
- plugin.getBungeeLogger().log(Level.WARNING, "Failed to serialize plugin information to send", e);
- }
- }
-
- // Tab completion
- @Override
- public Iterable onTabComplete(CommandSender sender, String[] args) {
- if (sender instanceof ProxiedPlayer player) {
- if (args.length == 1) {
- final ArrayList subCommands = new ArrayList<>();
- for (SubCommand subCommand : SUB_COMMANDS) {
- if (subCommand.permission() != null) {
- if (!player.hasPermission(subCommand.permission())) {
- continue;
- }
- }
- subCommands.add(subCommand.command());
- }
- // Automatically filter the sub commands' order in tab completion by what the player has typed
- return subCommands.stream().filter(val -> val.startsWith(args[0]))
- .sorted().collect(Collectors.toList());
- } else {
- return Collections.emptyList();
- }
- }
- return Collections.emptyList();
- }
-
-}
diff --git a/bungeecord/src/main/java/net/william278/husksync/bungeecord/config/ConfigLoader.java b/bungeecord/src/main/java/net/william278/husksync/bungeecord/config/ConfigLoader.java
deleted file mode 100644
index e4e0d3a6..00000000
--- a/bungeecord/src/main/java/net/william278/husksync/bungeecord/config/ConfigLoader.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package net.william278.husksync.bungeecord.config;
-
-import net.william278.husksync.HuskSyncBungeeCord;
-import net.william278.husksync.Settings;
-import net.william278.husksync.util.MessageManager;
-import net.md_5.bungee.config.Configuration;
-
-import java.util.HashMap;
-
-public class ConfigLoader {
-
- private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
-
- private static Configuration copyDefaults(Configuration config) {
- // Get the config version and update if needed
- String configVersion = config.getString("config_file_version", "1.0");
- if (configVersion.contains("-dev")) {
- configVersion = configVersion.replaceAll("-dev", "");
- }
- if (!configVersion.equals(plugin.getDescription().getVersion())) {
- if (configVersion.equalsIgnoreCase("1.0")) {
- config.set("check_for_updates", true);
- }
- if (configVersion.equalsIgnoreCase("1.0") || configVersion.equalsIgnoreCase("1.0.1") || configVersion.equalsIgnoreCase("1.0.2") || configVersion.equalsIgnoreCase("1.0.3")) {
- config.set("clusters.main.player_table", "husksync_players");
- config.set("clusters.main.data_table", "husksync_data");
- }
- config.set("config_file_version", plugin.getDescription().getVersion());
- }
- // Save the config back
- ConfigManager.saveConfig(config);
- return config;
- }
-
- public static void loadSettings(Configuration loadedConfig) throws IllegalArgumentException {
- Configuration config = copyDefaults(loadedConfig);
-
- Settings.language = config.getString("language", "en-gb");
-
- Settings.serverType = Settings.ServerType.PROXY;
- Settings.automaticUpdateChecks = config.getBoolean("check_for_updates", true);
- Settings.redisHost = config.getString("redis_settings.host", "localhost");
- Settings.redisPort = config.getInt("redis_settings.port", 6379);
- Settings.redisPassword = config.getString("redis_settings.password", "");
- Settings.redisSSL = config.getBoolean("redis_settings.use_ssl", false);
-
- Settings.dataStorageType = Settings.DataStorageType.valueOf(config.getString("data_storage_settings.database_type", "sqlite").toUpperCase());
- if (Settings.dataStorageType == Settings.DataStorageType.MYSQL) {
- Settings.mySQLHost = config.getString("data_storage_settings.mysql_settings.host", "localhost");
- Settings.mySQLPort = config.getInt("data_storage_settings.mysql_settings.port", 3306);
- Settings.mySQLDatabase = config.getString("data_storage_settings.mysql_settings.database", "HuskSync");
- Settings.mySQLUsername = config.getString("data_storage_settings.mysql_settings.username", "root");
- Settings.mySQLPassword = config.getString("data_storage_settings.mysql_settings.password", "pa55w0rd");
- Settings.mySQLParams = config.getString("data_storage_settings.mysql_settings.params", "?autoReconnect=true&useSSL=false");
- }
-
- Settings.hikariMaximumPoolSize = config.getInt("data_storage_settings.hikari_pool_settings.maximum_pool_size", 10);
- Settings.hikariMinimumIdle = config.getInt("data_storage_settings.hikari_pool_settings.minimum_idle", 10);
- Settings.hikariMaximumLifetime = config.getLong("data_storage_settings.hikari_pool_settings.maximum_lifetime", 1800000);
- Settings.hikariKeepAliveTime = config.getLong("data_storage_settings.hikari_pool_settings.keepalive_time", 0);
- Settings.hikariConnectionTimeOut = config.getLong("data_storage_settings.hikari_pool_settings.connection_timeout", 5000);
-
- Settings.bounceBackSynchronisation = config.getBoolean("bounce_back_synchronization", true);
-
- // Read cluster data
- Configuration section = config.getSection("clusters");
- final String settingDatabaseName = Settings.mySQLDatabase != null ? Settings.mySQLDatabase : "HuskSync";
- for (String clusterId : section.getKeys()) {
- final String playerTableName = config.getString("clusters." + clusterId + ".player_table", "husksync_players");
- final String dataTableName = config.getString("clusters." + clusterId + ".data_table", "husksync_data");
- final String databaseName = config.getString("clusters." + clusterId + ".database", settingDatabaseName);
- Settings.clusters.add(new Settings.SynchronisationCluster(clusterId, databaseName, playerTableName, dataTableName));
- }
- }
-
- public static void loadMessageStrings(Configuration config) {
- final HashMap messages = new HashMap<>();
- for (String messageId : config.getKeys()) {
- messages.put(messageId, config.getString(messageId));
- }
- MessageManager.setMessages(messages);
- }
-
-}
diff --git a/bungeecord/src/main/java/net/william278/husksync/bungeecord/config/ConfigManager.java b/bungeecord/src/main/java/net/william278/husksync/bungeecord/config/ConfigManager.java
deleted file mode 100644
index b38dd470..00000000
--- a/bungeecord/src/main/java/net/william278/husksync/bungeecord/config/ConfigManager.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package net.william278.husksync.bungeecord.config;
-
-import net.william278.husksync.HuskSyncBungeeCord;
-import net.william278.husksync.Settings;
-import net.md_5.bungee.config.Configuration;
-import net.md_5.bungee.config.ConfigurationProvider;
-import net.md_5.bungee.config.YamlConfiguration;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.util.logging.Level;
-
-public class ConfigManager {
-
- private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
-
- public static void loadConfig() {
- try {
- if (!plugin.getDataFolder().exists()) {
- if (plugin.getDataFolder().mkdir()) {
- plugin.getBungeeLogger().info("Created HuskSync data folder");
- }
- }
- File configFile = new File(plugin.getDataFolder(), "config.yml");
- if (!configFile.exists()) {
- Files.copy(plugin.getResourceAsStream("proxy-config.yml"), configFile.toPath());
- plugin.getBungeeLogger().info("Created HuskSync config file");
- }
- } catch (Exception e) {
- plugin.getBungeeLogger().log(Level.CONFIG, "An exception occurred loading the configuration file", e);
- }
- }
-
- public static void saveConfig(Configuration config) {
- try {
- ConfigurationProvider.getProvider(YamlConfiguration.class).save(config, new File(plugin.getDataFolder(), "config.yml"));
- } catch (IOException e) {
- plugin.getBungeeLogger().log(Level.CONFIG, "An exception occurred loading the configuration file", e);
- }
- }
-
- public static void loadMessages() {
- try {
- if (!plugin.getDataFolder().exists()) {
- if (plugin.getDataFolder().mkdir()) {
- plugin.getBungeeLogger().info("Created HuskSync data folder");
- }
- }
- File messagesFile = new File(plugin.getDataFolder(), "messages_" + Settings.language + ".yml");
- if (!messagesFile.exists()) {
- Files.copy(plugin.getResourceAsStream("languages/" + Settings.language + ".yml"), messagesFile.toPath());
- plugin.getBungeeLogger().info("Created HuskSync messages file");
- }
- } catch (Exception e) {
- plugin.getBungeeLogger().log(Level.CONFIG, "An exception occurred loading the messages file", e);
- }
- }
-
- public static Configuration getConfig() {
- try {
- File configFile = new File(plugin.getDataFolder(), "config.yml");
- return ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile);
- } catch (IOException e) {
- plugin.getBungeeLogger().log(Level.CONFIG, "An IOException occurred fetching the configuration file", e);
- return null;
- }
- }
-
- public static Configuration getMessages() {
- try {
- File configFile = new File(plugin.getDataFolder(), "messages_" + Settings.language + ".yml");
- return ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile);
- } catch (IOException e) {
- plugin.getBungeeLogger().log(Level.CONFIG, "An IOException occurred fetching the messages file", e);
- return null;
- }
- }
-
-}
-
diff --git a/bungeecord/src/main/java/net/william278/husksync/bungeecord/listener/BungeeEventListener.java b/bungeecord/src/main/java/net/william278/husksync/bungeecord/listener/BungeeEventListener.java
deleted file mode 100644
index c1340f89..00000000
--- a/bungeecord/src/main/java/net/william278/husksync/bungeecord/listener/BungeeEventListener.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package net.william278.husksync.bungeecord.listener;
-
-import net.william278.husksync.HuskSyncBungeeCord;
-import net.william278.husksync.PlayerData;
-import net.william278.husksync.Settings;
-import net.william278.husksync.redis.RedisMessage;
-import net.md_5.bungee.api.ProxyServer;
-import net.md_5.bungee.api.connection.ProxiedPlayer;
-import net.md_5.bungee.api.event.PostLoginEvent;
-import net.md_5.bungee.api.plugin.Listener;
-import net.md_5.bungee.event.EventHandler;
-import net.md_5.bungee.event.EventPriority;
-
-import java.io.IOException;
-import java.util.Map;
-import java.util.logging.Level;
-
-public class BungeeEventListener implements Listener {
-
- private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
-
- @EventHandler(priority = EventPriority.LOWEST)
- public void onPostLogin(PostLoginEvent event) {
- final ProxiedPlayer player = event.getPlayer();
- ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
- // Ensure the player has data on SQL and that it is up-to-date
- HuskSyncBungeeCord.dataManager.ensurePlayerExists(player.getUniqueId(), player.getName());
-
- // Get the player's data from SQL
- final Map data = HuskSyncBungeeCord.dataManager.getPlayerData(player.getUniqueId());
-
- // Update the player's data from SQL onto the cache
- assert data != null;
- for (Settings.SynchronisationCluster cluster : data.keySet()) {
- HuskSyncBungeeCord.dataManager.playerDataCache.get(cluster).updatePlayer(data.get(cluster));
- }
-
- // Send a message asking the bukkit to request data on join
- try {
- new RedisMessage(RedisMessage.MessageType.REQUEST_DATA_ON_JOIN,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null),
- RedisMessage.RequestOnJoinUpdateType.ADD_REQUESTER.toString(), player.getUniqueId().toString()).send();
- } catch (IOException e) {
- plugin.getBungeeLogger().log(Level.SEVERE, "Failed to serialize request data on join message data");
- e.printStackTrace();
- }
- });
- }
-
-}
diff --git a/bungeecord/src/main/java/net/william278/husksync/bungeecord/listener/BungeeRedisListener.java b/bungeecord/src/main/java/net/william278/husksync/bungeecord/listener/BungeeRedisListener.java
deleted file mode 100644
index 966726a3..00000000
--- a/bungeecord/src/main/java/net/william278/husksync/bungeecord/listener/BungeeRedisListener.java
+++ /dev/null
@@ -1,234 +0,0 @@
-package net.william278.husksync.bungeecord.listener;
-
-import de.themoep.minedown.MineDown;
-import net.william278.husksync.HuskSyncBungeeCord;
-import net.william278.husksync.Server;
-import net.william278.husksync.util.MessageManager;
-import net.william278.husksync.PlayerData;
-import net.william278.husksync.Settings;
-import net.william278.husksync.migrator.MPDBMigrator;
-import net.william278.husksync.redis.RedisListener;
-import net.william278.husksync.redis.RedisMessage;
-import net.md_5.bungee.api.ChatMessageType;
-import net.md_5.bungee.api.ProxyServer;
-import net.md_5.bungee.api.connection.ProxiedPlayer;
-
-import java.io.IOException;
-import java.util.Objects;
-import java.util.UUID;
-import java.util.logging.Level;
-
-public class BungeeRedisListener extends RedisListener {
-
- private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
-
- // Initialize the listener on the bungee
- public BungeeRedisListener() {
- super();
- listen();
- }
-
- private PlayerData getPlayerCachedData(UUID uuid, String clusterId) {
- PlayerData data = null;
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- if (cluster.clusterId().equals(clusterId)) {
- // Get the player data from the cache
- PlayerData cachedData = HuskSyncBungeeCord.dataManager.playerDataCache.get(cluster).getPlayer(uuid);
- if (cachedData != null) {
- return cachedData;
- }
-
- data = Objects.requireNonNull(HuskSyncBungeeCord.dataManager.getPlayerData(uuid)).get(cluster); // Get their player data from MySQL
- HuskSyncBungeeCord.dataManager.playerDataCache.get(cluster).updatePlayer(data); // Update the cache
- break;
- }
- }
- return data; // Return the data
- }
-
- /**
- * Handle an incoming {@link RedisMessage}
- *
- * @param message The {@link RedisMessage} to handle
- */
- @Override
- public void handleMessage(RedisMessage message) {
- // Ignore messages destined for Bukkit servers
- if (message.getMessageTarget().targetServerType() != Settings.ServerType.PROXY) {
- return;
- }
- // Only process redis messages when ready
- if (!HuskSyncBungeeCord.readyForRedis) {
- return;
- }
-
- switch (message.getMessageType()) {
- case PLAYER_DATA_REQUEST -> ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
- // Get the UUID of the requesting player
- final UUID requestingPlayerUUID = UUID.fromString(message.getMessageData());
- try {
- // Send the reply, serializing the message data
- new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, requestingPlayerUUID, message.getMessageTarget().targetClusterId()),
- RedisMessage.serialize(getPlayerCachedData(requestingPlayerUUID, message.getMessageTarget().targetClusterId())))
- .send();
-
- // Send an update to all bukkit servers removing the player from the requester cache
- new RedisMessage(RedisMessage.MessageType.REQUEST_DATA_ON_JOIN,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
- RedisMessage.RequestOnJoinUpdateType.REMOVE_REQUESTER.toString(), requestingPlayerUUID.toString())
- .send();
-
- // Send synchronisation complete message
- ProxiedPlayer player = ProxyServer.getInstance().getPlayer(requestingPlayerUUID);
- if (player != null) {
- player.sendMessage(ChatMessageType.ACTION_BAR, new MineDown(MessageManager.getMessage("synchronisation_complete")).toComponent());
- }
- } catch (IOException e) {
- log(Level.SEVERE, "Failed to serialize data when replying to a data request");
- e.printStackTrace();
- }
- });
- case PLAYER_DATA_UPDATE -> ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
- // Deserialize the PlayerData received
- PlayerData playerData;
- final String serializedPlayerData = message.getMessageDataElements()[0];
- final boolean bounceBack = Boolean.parseBoolean(message.getMessageDataElements()[1]);
- try {
- playerData = (PlayerData) RedisMessage.deserialize(serializedPlayerData);
- } catch (IOException | ClassNotFoundException e) {
- log(Level.SEVERE, "Failed to deserialize PlayerData when handling a player update request");
- e.printStackTrace();
- return;
- }
-
- // Update the data in the cache and SQL
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- if (cluster.clusterId().equals(message.getMessageTarget().targetClusterId())) {
- HuskSyncBungeeCord.dataManager.updatePlayerData(playerData, cluster);
- break;
- }
- }
-
- // Reply with the player data if they are still online (switching server)
- if (Settings.bounceBackSynchronisation && bounceBack) {
- try {
- ProxiedPlayer player = ProxyServer.getInstance().getPlayer(playerData.getPlayerUUID());
- if (player != null) {
- new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, playerData.getPlayerUUID(), message.getMessageTarget().targetClusterId()),
- serializedPlayerData)
- .send();
-
- // Send synchronisation complete message
- player.sendMessage(ChatMessageType.ACTION_BAR, new MineDown(MessageManager.getMessage("synchronisation_complete")).toComponent());
- }
- } catch (IOException e) {
- log(Level.SEVERE, "Failed to re-serialize PlayerData when handling a player update request");
- e.printStackTrace();
- }
- }
- });
- case CONNECTION_HANDSHAKE -> {
- // Reply to a Bukkit server's connection handshake to complete the process
- if (HuskSyncBungeeCord.isDisabling) return; // Return if the Proxy is disabling
- final UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
- final boolean hasMySqlPlayerDataBridge = Boolean.parseBoolean(message.getMessageDataElements()[1]);
- final String bukkitBrand = message.getMessageDataElements()[2];
- final String huskSyncVersion = message.getMessageDataElements()[3];
- try {
- new RedisMessage(RedisMessage.MessageType.CONNECTION_HANDSHAKE,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
- serverUUID.toString(), plugin.getProxy().getName())
- .send();
- HuskSyncBungeeCord.synchronisedServers.add(
- new Server(serverUUID, hasMySqlPlayerDataBridge,
- huskSyncVersion, bukkitBrand, message.getMessageTarget().targetClusterId()));
- log(Level.INFO, "Completed handshake with " + bukkitBrand + " server (" + serverUUID + ")");
- } catch (IOException e) {
- log(Level.SEVERE, "Failed to serialize handshake message data");
- e.printStackTrace();
- }
- }
- case TERMINATE_HANDSHAKE -> {
- // Terminate the handshake with a Bukkit server
- final UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
- final String bukkitBrand = message.getMessageDataElements()[1];
-
- // Remove a server from the synchronised server list
- Server serverToRemove = null;
- for (Server server : HuskSyncBungeeCord.synchronisedServers) {
- if (server.serverUUID().equals(serverUUID)) {
- serverToRemove = server;
- break;
- }
- }
- HuskSyncBungeeCord.synchronisedServers.remove(serverToRemove);
- log(Level.INFO, "Terminated the handshake with " + bukkitBrand + " server (" + serverUUID + ")");
- }
- case DECODED_MPDB_DATA_SET -> {
- // Deserialize the PlayerData received
- PlayerData playerData;
- final String serializedPlayerData = message.getMessageDataElements()[0];
- final String playerName = message.getMessageDataElements()[1];
- try {
- playerData = (PlayerData) RedisMessage.deserialize(serializedPlayerData);
- } catch (IOException | ClassNotFoundException e) {
- log(Level.SEVERE, "Failed to deserialize PlayerData when handling incoming decoded MPDB data");
- e.printStackTrace();
- return;
- }
-
- // Get the migrator
- MPDBMigrator migrator = HuskSyncBungeeCord.mpdbMigrator;
-
- // Add the incoming data to the data to be saved
- migrator.incomingPlayerData.put(playerData, playerName);
-
- // Increment players migrated
- migrator.playersMigrated++;
- plugin.getBungeeLogger().log(Level.INFO, "Migrated " + migrator.playersMigrated + "/" + migrator.migratedDataSent + " players.");
-
- // When all the data has been received, save it
- if (migrator.migratedDataSent == migrator.playersMigrated) {
- ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> migrator.loadIncomingData(migrator.incomingPlayerData,
- HuskSyncBungeeCord.dataManager));
- }
- }
- case API_DATA_REQUEST -> ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
- final UUID playerUUID = UUID.fromString(message.getMessageDataElements()[0]);
- final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[1]);
- try {
- final PlayerData data = getPlayerCachedData(playerUUID, message.getMessageTarget().targetClusterId());
-
- if (data == null) {
- new RedisMessage(RedisMessage.MessageType.API_DATA_CANCEL,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
- requestUUID.toString())
- .send();
- } else {
- // Send the reply alongside the request UUID, serializing the requested message data
- new RedisMessage(RedisMessage.MessageType.API_DATA_RETURN,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
- requestUUID.toString(),
- RedisMessage.serialize(data))
- .send();
- }
- } catch (IOException e) {
- plugin.getBungeeLogger().log(Level.SEVERE, "Failed to serialize PlayerData requested via the API");
- }
- });
- }
- }
-
- /**
- * Log to console
- *
- * @param level The {@link Level} to log
- * @param message Message to log
- */
- @Override
- public void log(Level level, String message) {
- plugin.getBungeeLogger().log(level, message);
- }
-}
\ No newline at end of file
diff --git a/bungeecord/src/main/java/net/william278/husksync/bungeecord/util/BungeeLogger.java b/bungeecord/src/main/java/net/william278/husksync/bungeecord/util/BungeeLogger.java
deleted file mode 100644
index 481e6983..00000000
--- a/bungeecord/src/main/java/net/william278/husksync/bungeecord/util/BungeeLogger.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package net.william278.husksync.bungeecord.util;
-
-import net.william278.husksync.util.Logger;
-
-import java.util.logging.Level;
-
-public record BungeeLogger(java.util.logging.Logger parent) implements Logger {
-
- @Override
- public void log(Level level, String message, Exception e) {
- parent.log(level, message, e);
- }
-
- @Override
- public void log(Level level, String message) {
- parent.log(level, message);
- }
-
- @Override
- public void info(String message) {
- parent.info(message);
- }
-
- @Override
- public void severe(String message) {
- parent.severe(message);
- }
-
- @Override
- public void config(String message) {
- parent.config(message);
- }
-}
diff --git a/bungeecord/src/main/java/net/william278/husksync/bungeecord/util/BungeeUpdateChecker.java b/bungeecord/src/main/java/net/william278/husksync/bungeecord/util/BungeeUpdateChecker.java
deleted file mode 100644
index 06b0caae..00000000
--- a/bungeecord/src/main/java/net/william278/husksync/bungeecord/util/BungeeUpdateChecker.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package net.william278.husksync.bungeecord.util;
-
-import net.william278.husksync.HuskSyncBungeeCord;
-import net.william278.husksync.util.UpdateChecker;
-
-import java.util.logging.Level;
-
-public class BungeeUpdateChecker extends UpdateChecker {
-
- private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
-
- public BungeeUpdateChecker(String versionToCheck) {
- super(versionToCheck);
- }
-
- @Override
- public void log(Level level, String message) {
- plugin.getBungeeLogger().log(level, message);
- }
-}
diff --git a/common/build.gradle b/common/build.gradle
index 2181c512..b0a7207f 100644
--- a/common/build.gradle
+++ b/common/build.gradle
@@ -1,11 +1,23 @@
-plugins {
- id 'java'
-}
-
dependencies {
- compileOnly 'com.zaxxer:HikariCP:5.0.1'
+ implementation 'commons-io:commons-io:2.11.0'
+ implementation 'dev.dejvokep:boosted-yaml:1.2'
+ implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
+ implementation 'com.zaxxer:HikariCP:5.0.1'
+ implementation 'com.google.code.gson:gson:2.9.0'
+
+ compileOnly 'org.jetbrains:annotations:23.0.0'
+ compileOnly 'org.xerial:sqlite-jdbc:' + sqlite_driver_version
+ compileOnly 'redis.clients:jedis:' + jedis_version
}
shadowJar {
- relocate 'com.zaxxer', 'net.william278.husksync.libraries'
+ relocate 'redis.clients', 'net.william278.husksync.libraries'
+ relocate 'org.apache', 'net.william278.huskhomes.libraries'
+ relocate 'dev.dejvokep', 'net.william278.huskhomes.libraries'
+ relocate 'de.themoep', 'net.william278.huskhomes.libraries'
+ relocate 'org.jetbrains', 'net.william278.huskhomes.libraries'
+ relocate 'org.intellij', 'net.william278.huskhomes.libraries'
+ relocate 'com.zaxxer', 'net.william278.huskhomes.libraries'
+ relocate 'org.slf4j', 'net.william278.huskhomes.libraries.slf4j'
+ relocate 'com.google', 'net.william278.huskhomes.libraries'
}
\ No newline at end of file
diff --git a/common/src/main/java/net/william278/husksync/HuskSync.java b/common/src/main/java/net/william278/husksync/HuskSync.java
new file mode 100644
index 00000000..848fd23c
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/HuskSync.java
@@ -0,0 +1,38 @@
+package net.william278.husksync;
+
+import net.william278.husksync.config.Locales;
+import net.william278.husksync.config.Settings;
+import net.william278.husksync.listener.EventListener;
+import net.william278.husksync.player.OnlineUser;
+import net.william278.husksync.redis.RedisManager;
+import net.william278.husksync.database.Database;
+import net.william278.husksync.util.Logger;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+
+public interface HuskSync {
+
+ @NotNull Set getOnlineUsers();
+
+ @NotNull Optional getOnlineUser(@NotNull UUID uuid);
+
+ @NotNull EventListener getEventListener();
+
+ @NotNull Database getDatabase();
+
+ @NotNull RedisManager getRedisManager();
+
+ @NotNull Settings getSettings();
+
+ @NotNull Locales getLocales();
+
+ @NotNull Logger getLogger();
+
+ @NotNull String getVersion();
+
+ void reload();
+
+}
diff --git a/common/src/main/java/net/william278/husksync/PlayerData.java b/common/src/main/java/net/william278/husksync/PlayerData.java
deleted file mode 100644
index 82927a66..00000000
--- a/common/src/main/java/net/william278/husksync/PlayerData.java
+++ /dev/null
@@ -1,533 +0,0 @@
-package net.william278.husksync;
-
-import java.io.*;
-import java.time.Instant;
-import java.util.UUID;
-
-/**
- * Cross-platform class used to represent a player's data. Data from this can be deserialized using the DataSerializer class on Bukkit platforms.
- */
-public class PlayerData implements Serializable {
-
- /**
- * The UUID of the player who this data belongs to
- */
- private final UUID playerUUID;
-
- /**
- * The unique version UUID of this data
- */
- private final UUID dataVersionUUID;
-
- /**
- * Epoch time identifying when the data was last updated or created
- */
- private long timestamp;
-
- /**
- * A special flag that will be {@code true} if the player is new to the network and should not have their data set when joining the Bukkit
- */
- public boolean useDefaultData = false;
-
- /*
- * Player data records
- */
- private String serializedInventory;
- private String serializedEnderChest;
- private double health;
- private double maxHealth;
- private double healthScale;
- private int hunger;
- private float saturation;
- private float saturationExhaustion;
- private int selectedSlot;
- private String serializedEffectData;
- private int totalExperience;
- private int expLevel;
- private float expProgress;
- private String gameMode;
- private String serializedStatistics;
- private boolean isFlying;
- private String serializedAdvancements;
- private String serializedLocation;
-
- /**
- * Constructor to create new PlayerData from a bukkit {@code Player}'s data
- *
- * @param playerUUID The Player's UUID
- * @param serializedInventory Their serialized inventory
- * @param serializedEnderChest Their serialized ender chest
- * @param health Their health
- * @param maxHealth Their max health
- * @param healthScale Their health scale
- * @param hunger Their hunger
- * @param saturation Their saturation
- * @param saturationExhaustion Their saturation exhaustion
- * @param selectedSlot Their selected hot bar slot
- * @param serializedStatusEffects Their serialized status effects
- * @param totalExperience Their total experience points ("Score")
- * @param expLevel Their exp level
- * @param expProgress Their exp progress to the next level
- * @param gameMode Their game mode ({@code SURVIVAL}, {@code CREATIVE}, etc.)
- * @param serializedStatistics Their serialized statistics data (Displayed in Statistics menu in ESC menu)
- */
- public PlayerData(UUID playerUUID, String serializedInventory, String serializedEnderChest, double health, double maxHealth,
- double healthScale, int hunger, float saturation, float saturationExhaustion, int selectedSlot,
- String serializedStatusEffects, int totalExperience, int expLevel, float expProgress, String gameMode,
- String serializedStatistics, boolean isFlying, String serializedAdvancements, String serializedLocation) {
- this.dataVersionUUID = UUID.randomUUID();
- this.timestamp = Instant.now().getEpochSecond();
- this.playerUUID = playerUUID;
- this.serializedInventory = serializedInventory;
- this.serializedEnderChest = serializedEnderChest;
- this.health = health;
- this.maxHealth = maxHealth;
- this.healthScale = healthScale;
- this.hunger = hunger;
- this.saturation = saturation;
- this.saturationExhaustion = saturationExhaustion;
- this.selectedSlot = selectedSlot;
- this.serializedEffectData = serializedStatusEffects;
- this.totalExperience = totalExperience;
- this.expLevel = expLevel;
- this.expProgress = expProgress;
- this.gameMode = gameMode;
- this.serializedStatistics = serializedStatistics;
- this.isFlying = isFlying;
- this.serializedAdvancements = serializedAdvancements;
- this.serializedLocation = serializedLocation;
- }
-
- /**
- * Constructor for a PlayerData object from an existing object that was stored in SQL
- *
- * @param playerUUID The player whose data this is' UUID
- * @param dataVersionUUID The PlayerData version UUID
- * @param serializedInventory Their serialized inventory
- * @param serializedEnderChest Their serialized ender chest
- * @param health Their health
- * @param maxHealth Their max health
- * @param healthScale Their health scale
- * @param hunger Their hunger
- * @param saturation Their saturation
- * @param saturationExhaustion Their saturation exhaustion
- * @param selectedSlot Their selected hot bar slot
- * @param serializedStatusEffects Their serialized status effects
- * @param totalExperience Their total experience points ("Score")
- * @param expLevel Their exp level
- * @param expProgress Their exp progress to the next level
- * @param gameMode Their game mode ({@code SURVIVAL}, {@code CREATIVE}, etc.)
- * @param serializedStatistics Their serialized statistics data (Displayed in Statistics menu in ESC menu)
- */
- public PlayerData(UUID playerUUID, UUID dataVersionUUID, long timestamp, String serializedInventory, String serializedEnderChest,
- double health, double maxHealth, double healthScale, int hunger, float saturation, float saturationExhaustion,
- int selectedSlot, String serializedStatusEffects, int totalExperience, int expLevel, float expProgress,
- String gameMode, String serializedStatistics, boolean isFlying, String serializedAdvancements,
- String serializedLocation) {
- this.playerUUID = playerUUID;
- this.dataVersionUUID = dataVersionUUID;
- this.timestamp = timestamp;
- this.serializedInventory = serializedInventory;
- this.serializedEnderChest = serializedEnderChest;
- this.health = health;
- this.maxHealth = maxHealth;
- this.healthScale = healthScale;
- this.hunger = hunger;
- this.saturation = saturation;
- this.saturationExhaustion = saturationExhaustion;
- this.selectedSlot = selectedSlot;
- this.serializedEffectData = serializedStatusEffects;
- this.totalExperience = totalExperience;
- this.expLevel = expLevel;
- this.expProgress = expProgress;
- this.gameMode = gameMode;
- this.serializedStatistics = serializedStatistics;
- this.isFlying = isFlying;
- this.serializedAdvancements = serializedAdvancements;
- this.serializedLocation = serializedLocation;
- }
-
- /**
- * Get default PlayerData for a new user
- *
- * @param playerUUID The bukkit Player's UUID
- * @return Default {@link PlayerData}
- */
- public static PlayerData DEFAULT_PLAYER_DATA(UUID playerUUID) {
- PlayerData data = new PlayerData(playerUUID, "", "", 20,
- 20, 20, 20, 10, 1, 0,
- "", 0, 0, 0, "SURVIVAL",
- "", false, "", "");
- data.useDefaultData = true;
- return data;
- }
-
- /**
- * Get the {@link UUID} of the player whose data this is
- *
- * @return the player's {@link UUID}
- */
- public UUID getPlayerUUID() {
- return playerUUID;
- }
-
- /**
- * Get the unique version {@link UUID} of the PlayerData
- *
- * @return The unique data version
- */
- public UUID getDataVersionUUID() {
- return dataVersionUUID;
- }
-
- /**
- * Get the timestamp when this data was created or last updated
- *
- * @return time since epoch of last data update or creation
- */
- public long getDataTimestamp() {
- return timestamp;
- }
-
- /**
- * Returns the serialized player {@code ItemStack[]} inventory
- *
- * @return The player's serialized inventory
- */
- public String getSerializedInventory() {
- return serializedInventory;
- }
-
- /**
- * Returns the serialized player {@code ItemStack[]} ender chest
- *
- * @return The player's serialized ender chest
- */
- public String getSerializedEnderChest() {
- return serializedEnderChest;
- }
-
- /**
- * Returns the player's health value
- *
- * @return the player's health
- */
- public double getHealth() {
- return health;
- }
-
- /**
- * Returns the player's max health value
- *
- * @return the player's max health
- */
- public double getMaxHealth() {
- return maxHealth;
- }
-
- /**
- * Returns the player's health scale value {@see https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/entity/Player.html#getHealthScale()}
- *
- * @return the player's health scaling value
- */
- public double getHealthScale() {
- return healthScale;
- }
-
- /**
- * Returns the player's hunger points
- *
- * @return the player's hunger level
- */
- public int getHunger() {
- return hunger;
- }
-
- /**
- * Returns the player's saturation points
- *
- * @return the player's saturation level
- */
- public float getSaturation() {
- return saturation;
- }
-
- /**
- * Returns the player's saturation exhaustion value {@see https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/entity/HumanEntity.html#getExhaustion()}
- *
- * @return the player's saturation exhaustion
- */
- public float getSaturationExhaustion() {
- return saturationExhaustion;
- }
-
- /**
- * Returns the number of the player's currently selected hotbar slot
- *
- * @return the player's selected hotbar slot
- */
- public int getSelectedSlot() {
- return selectedSlot;
- }
-
- /**
- * Returns a serialized {@link String} of the player's current status effects
- *
- * @return the player's serialized status effect data
- */
- public String getSerializedEffectData() {
- return serializedEffectData;
- }
-
- /**
- * Returns the player's total experience score (used for presenting the death screen score value)
- *
- * @return the player's total experience score
- */
- public int getTotalExperience() {
- return totalExperience;
- }
-
- /**
- * Returns a serialized {@link String} of the player's statistics
- *
- * @return the player's serialized statistic records
- */
- public String getSerializedStatistics() {
- return serializedStatistics;
- }
-
- /**
- * Returns the player's current experience level
- *
- * @return the player's exp level
- */
- public int getExpLevel() {
- return expLevel;
- }
-
- /**
- * Returns the player's progress to the next experience level
- *
- * @return the player's exp progress
- */
- public float getExpProgress() {
- return expProgress;
- }
-
- /**
- * Returns the player's current game mode as a string ({@code SURVIVAL}, {@code CREATIVE}, etc.)
- *
- * @return the player's game mode
- */
- public String getGameMode() {
- return gameMode;
- }
-
- /**
- * Returns if the player is currently flying
- *
- * @return {@code true} if the player is in flight; {@code false} otherwise
- */
- public boolean isFlying() {
- return isFlying;
- }
-
- /**
- * Returns a serialized {@link String} of the player's advancements
- *
- * @return the player's serialized advancement data
- */
- public String getSerializedAdvancements() {
- return serializedAdvancements;
- }
-
- /**
- * Returns a serialized {@link String} of the player's current location
- *
- * @return the player's serialized location
- */
- public String getSerializedLocation() {
- return serializedLocation;
- }
-
- /**
- * Update the player's inventory data
- *
- * @param serializedInventory A serialized {@code String}; new inventory data
- */
- public void setSerializedInventory(String serializedInventory) {
- this.serializedInventory = serializedInventory;
- this.timestamp = Instant.now().getEpochSecond();
- }
-
- /**
- * Update the player's ender chest data
- *
- * @param serializedEnderChest A serialized {@code String}; new ender chest inventory data
- */
- public void setSerializedEnderChest(String serializedEnderChest) {
- this.serializedEnderChest = serializedEnderChest;
- this.timestamp = Instant.now().getEpochSecond();
- }
-
- /**
- * Update the player's health
- *
- * @param health new health value
- */
- public void setHealth(double health) {
- this.health = health;
- this.timestamp = Instant.now().getEpochSecond();
- }
-
- /**
- * Update the player's max health
- *
- * @param maxHealth new maximum health value
- */
- public void setMaxHealth(double maxHealth) {
- this.maxHealth = maxHealth;
- this.timestamp = Instant.now().getEpochSecond();
- }
-
- /**
- * Update the player's health scale
- *
- * @param healthScale new health scaling value
- */
- public void setHealthScale(double healthScale) {
- this.healthScale = healthScale;
- this.timestamp = Instant.now().getEpochSecond();
- }
-
- /**
- * Update the player's hunger meter
- *
- * @param hunger new hunger value
- */
- public void setHunger(int hunger) {
- this.hunger = hunger;
- this.timestamp = Instant.now().getEpochSecond();
- }
-
- /**
- * Update the player's saturation level
- *
- * @param saturation new saturation value
- */
- public void setSaturation(float saturation) {
- this.saturation = saturation;
- this.timestamp = Instant.now().getEpochSecond();
- }
-
- /**
- * Update the player's saturation exhaustion value
- *
- * @param saturationExhaustion new exhaustion value
- */
- public void setSaturationExhaustion(float saturationExhaustion) {
- this.saturationExhaustion = saturationExhaustion;
- this.timestamp = Instant.now().getEpochSecond();
- }
-
- /**
- * Update the player's selected hotbar slot
- *
- * @param selectedSlot new hotbar slot number (0-9)
- */
- public void setSelectedSlot(int selectedSlot) {
- this.selectedSlot = selectedSlot;
- this.timestamp = Instant.now().getEpochSecond();
- }
-
- /**
- * Update the player's status effect data
- *
- * @param serializedEffectData A serialized {@code String} of the player's new status effect data
- */
- public void setSerializedEffectData(String serializedEffectData) {
- this.serializedEffectData = serializedEffectData;
- this.timestamp = Instant.now().getEpochSecond();
- }
-
- /**
- * Set the player's total experience points (used to display score on death screen)
- *
- * @param totalExperience the player's new total experience score
- */
- public void setTotalExperience(int totalExperience) {
- this.totalExperience = totalExperience;
- this.timestamp = Instant.now().getEpochSecond();
- }
-
- /**
- * Set the player's exp level
- *
- * @param expLevel the player's new exp level
- */
- public void setExpLevel(int expLevel) {
- this.expLevel = expLevel;
- this.timestamp = Instant.now().getEpochSecond();
- }
-
- /**
- * Set the player's progress to their next exp level
- *
- * @param expProgress the player's new experience progress
- */
- public void setExpProgress(float expProgress) {
- this.expProgress = expProgress;
- this.timestamp = Instant.now().getEpochSecond();
- }
-
- /**
- * Set the player's game mode
- *
- * @param gameMode the player's new game mode ({@code SURVIVAL}, {@code CREATIVE}, etc.)
- */
- public void setGameMode(String gameMode) {
- this.gameMode = gameMode;
- this.timestamp = Instant.now().getEpochSecond();
- }
-
- /**
- * Update the player's statistics data
- *
- * @param serializedStatistics A serialized {@code String}; new statistic data
- */
- public void setSerializedStatistics(String serializedStatistics) {
- this.serializedStatistics = serializedStatistics;
- this.timestamp = Instant.now().getEpochSecond();
- }
-
- /**
- * Set if the player is flying
- *
- * @param flying whether the player is flying
- */
- public void setFlying(boolean flying) {
- isFlying = flying;
- this.timestamp = Instant.now().getEpochSecond();
- }
-
- /**
- * Update the player's advancement data
- *
- * @param serializedAdvancements A serialized {@code String}; new advancement data
- */
- public void setSerializedAdvancements(String serializedAdvancements) {
- this.serializedAdvancements = serializedAdvancements;
- this.timestamp = Instant.now().getEpochSecond();
- }
-
- /**
- * Update the player's location data
- *
- * @param serializedLocation A serialized {@code String}; new location data
- */
- public void setSerializedLocation(String serializedLocation) {
- this.serializedLocation = serializedLocation;
- this.timestamp = Instant.now().getEpochSecond();
- }
-}
diff --git a/common/src/main/java/net/william278/husksync/Server.java b/common/src/main/java/net/william278/husksync/Server.java
deleted file mode 100644
index 1c88d1c1..00000000
--- a/common/src/main/java/net/william278/husksync/Server.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package net.william278.husksync;
-
-import java.util.UUID;
-
-/**
- * A record representing a server synchronised on the network and whether it has MySqlPlayerDataBridge installed
- */
-public record Server(UUID serverUUID, boolean hasMySqlPlayerDataBridge, String huskSyncVersion, String serverBrand,
- String clusterId) {
-}
diff --git a/common/src/main/java/net/william278/husksync/Settings.java b/common/src/main/java/net/william278/husksync/Settings.java
deleted file mode 100644
index 19712f90..00000000
--- a/common/src/main/java/net/william278/husksync/Settings.java
+++ /dev/null
@@ -1,99 +0,0 @@
-package net.william278.husksync;
-
-import java.util.ArrayList;
-
-/**
- * Settings class, holds values loaded from the plugin config (either Bukkit or Bungee)
- */
-public class Settings {
-
- /*
- * General settings
- */
-
- // Whether to do automatic update checks on startup
- public static boolean automaticUpdateChecks;
-
- // The type of THIS server (Bungee or Bukkit)
- public static ServerType serverType;
-
- // Redis settings
- public static String redisHost;
- public static int redisPort;
- public static String redisPassword;
- public static boolean redisSSL;
-
- /*
- * Bungee / Proxy server-only settings
- */
-
- // Messages language
- public static String language;
-
- // Cluster IDs
- public static ArrayList clusters = new ArrayList<>();
-
- // SQL settings
- public static DataStorageType dataStorageType;
-
- // Bounce-back synchronisation (default)
- public static boolean bounceBackSynchronisation;
-
- // MySQL specific settings
- public static String mySQLHost;
- public static String mySQLDatabase;
- public static String mySQLUsername;
- public static String mySQLPassword;
- public static int mySQLPort;
- public static String mySQLParams;
-
- // Hikari connection pooling settings
- public static int hikariMaximumPoolSize;
- public static int hikariMinimumIdle;
- public static long hikariMaximumLifetime;
- public static long hikariKeepAliveTime;
- public static long hikariConnectionTimeOut;
-
- /*
- * Bukkit server-only settings
- */
-
- // Synchronisation options
- public static boolean syncInventories;
- public static boolean syncEnderChests;
- public static boolean syncHealth;
- public static boolean syncHunger;
- public static boolean syncExperience;
- public static boolean syncPotionEffects;
- public static boolean syncStatistics;
- public static boolean syncGameMode;
- public static boolean syncAdvancements;
- public static boolean syncLocation;
- public static boolean syncFlight;
- public static long synchronizationTimeoutRetryDelay;
- public static boolean saveOnWorldSave;
- public static boolean useNativeImplementation;
-
- // This Cluster ID
- public static String cluster;
-
- /*
- * Enum definitions
- */
-
- public enum ServerType {
- BUKKIT,
- PROXY,
- }
-
- public enum DataStorageType {
- MYSQL,
- SQLITE
- }
-
- /**
- * Defines information for a synchronisation cluster as listed on the proxy
- */
- public record SynchronisationCluster(String clusterId, String databaseName, String playerTableName, String dataTableName) {
- }
-}
diff --git a/common/src/main/java/net/william278/husksync/command/CommandBase.java b/common/src/main/java/net/william278/husksync/command/CommandBase.java
new file mode 100644
index 00000000..c13038ae
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/command/CommandBase.java
@@ -0,0 +1,58 @@
+package net.william278.husksync.command;
+
+import net.william278.husksync.HuskSync;
+import net.william278.husksync.player.OnlineUser;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Represents an abstract cross-platform representation for a plugin command
+ */
+public abstract class CommandBase {
+
+ /**
+ * The input string to match for this command
+ */
+ public final String command;
+
+ /**
+ * The permission node required to use this command
+ */
+ public final String permission;
+
+ /**
+ * Alias input strings for this command
+ */
+ public final String[] aliases;
+
+ /**
+ * Instance of the implementing plugin
+ */
+ public final HuskSync plugin;
+
+
+ public CommandBase(@NotNull String command, @NotNull Permission permission, @NotNull HuskSync implementor, String... aliases) {
+ this.command = command;
+ this.permission = permission.node;
+ this.plugin = implementor;
+ this.aliases = aliases;
+ }
+
+ /**
+ * Fires when the command is executed
+ *
+ * @param player {@link OnlineUser} executing the command
+ * @param args Command arguments
+ */
+ public abstract void onExecute(@NotNull OnlineUser player, @NotNull String[] args);
+
+ /**
+ * Returns the localised description string of this command
+ *
+ * @return the command description
+ */
+ public String getDescription() {
+ return plugin.getLocales().getRawLocale(command + "_command_description")
+ .orElse("A HuskHomes command");
+ }
+
+}
diff --git a/common/src/main/java/net/william278/husksync/command/ConsoleExecutable.java b/common/src/main/java/net/william278/husksync/command/ConsoleExecutable.java
new file mode 100644
index 00000000..a7f39dab
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/command/ConsoleExecutable.java
@@ -0,0 +1,17 @@
+package net.william278.husksync.command;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Interface providing console execution of commands
+ */
+public interface ConsoleExecutable {
+
+ /**
+ * What to do when console executes a command
+ *
+ * @param args command argument strings
+ */
+ void onConsoleExecute(@NotNull String[] args);
+
+}
diff --git a/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java b/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java
new file mode 100644
index 00000000..0b6d8aca
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java
@@ -0,0 +1,90 @@
+package net.william278.husksync.command;
+
+import de.themoep.minedown.MineDown;
+import net.william278.husksync.HuskSync;
+import net.william278.husksync.config.Locales;
+import net.william278.husksync.player.OnlineUser;
+import net.william278.husksync.util.UpdateChecker;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.logging.Level;
+
+public class HuskSyncCommand extends CommandBase implements TabCompletable, ConsoleExecutable {
+
+ public HuskSyncCommand(@NotNull HuskSync implementor) {
+ super("husksync", Permission.COMMAND_HUSKSYNC, implementor);
+ }
+
+ @Override
+ public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
+ if (args.length < 1) {
+ displayPluginInformation(player);
+ return;
+ }
+ switch (args[0].toLowerCase()) {
+ case "update", "version" -> {
+ if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_UPDATE.node)) {
+ plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
+ return;
+ }
+ final UpdateChecker updateChecker = new UpdateChecker(plugin.getVersion(), plugin.getLogger());
+ updateChecker.fetchLatestVersion().thenAccept(latestVersion -> {
+ if (updateChecker.isUpdateAvailable(latestVersion)) {
+ player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| A new update is available:](#00fb9a) [HuskSync " + updateChecker.fetchLatestVersion() + "](#00fb9a bold)" +
+ "[•](white) [Currently running:](#00fb9a) [Version " + updateChecker.getCurrentVersion() + "](gray)" +
+ "[•](white) [Download links:](#00fb9a) [[⏩ Spigot]](gray open_url=https://www.spigotmc.org/resources/husksync.97144/updates) [•](#262626) [[⏩ Polymart]](gray open_url=https://polymart.org/resource/husksync.1634/updates) [•](#262626) [[⏩ Songoda]](gray open_url=https://songoda.com/marketplace/product/husksync-a-modern-cross-server-player-data-synchronization-system.758)"));
+ } else {
+ player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date, running version " + latestVersion));
+ }
+ });
+ }
+ case "info", "about" -> displayPluginInformation(player);
+ case "reload" -> {
+ if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_RELOAD.node)) {
+ plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
+ return;
+ }
+ plugin.reload();
+ player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) fb9a&| Reloaded config & message files."));
+ }
+ default ->
+ plugin.getLocales().getLocale("error_invalid_syntax", "/husksync ").ifPresent(player::sendMessage);
+ }
+ }
+
+ @Override
+ public void onConsoleExecute(@NotNull String[] args) {
+ if (args.length < 1) {
+ plugin.getLogger().log(Level.INFO, "Console usage: /husksync ");
+ return;
+ }
+ switch (args[0].toLowerCase()) {
+ case "update", "version" -> new UpdateChecker(plugin.getVersion(), plugin.getLogger()).logToConsole();
+ case "info", "about" -> plugin.getLogger().log(Level.INFO, plugin.getLocales().stripMineDown(
+ Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getVersion())));
+ case "reload" -> {
+ plugin.reload();
+ plugin.getLogger().log(Level.INFO, "Reloaded config & message files.");
+ }
+ case "migrate" -> {
+ //todo - MPDB migrator
+ }
+ default ->
+ plugin.getLogger().log(Level.INFO, "Invalid syntax. Console usage: /husksync ");
+ }
+ }
+
+ @Override
+ public List onTabComplete(@NotNull OnlineUser player, @NotNull String[] args) {
+ return null;
+ }
+
+ private void displayPluginInformation(@NotNull OnlineUser player) {
+ if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_INFO.node)) {
+ plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
+ return;
+ }
+ player.sendMessage(new MineDown(Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getVersion())));
+ }
+}
diff --git a/common/src/main/java/net/william278/husksync/command/Permission.java b/common/src/main/java/net/william278/husksync/command/Permission.java
new file mode 100644
index 00000000..3fd07b72
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/command/Permission.java
@@ -0,0 +1,98 @@
+package net.william278.husksync.command;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Static plugin permission nodes required to execute commands
+ */
+public enum Permission {
+
+ /*
+ * /husksync command permissions
+ */
+
+ /**
+ * Lets the user use the {@code /husksync} command (subcommand permissions required)
+ */
+ COMMAND_HUSKSYNC("husksync.command.husksync", DefaultAccess.EVERYONE),
+ /**
+ * Lets the user view plugin info {@code /husksync info}
+ */
+ COMMAND_HUSKSYNC_INFO("husksync.command.husksync.info", DefaultAccess.EVERYONE),
+ /**
+ * Lets the user reload the plugin {@code /husksync reload}
+ */
+ COMMAND_HUSKSYNC_RELOAD("husksync.command.husksync.reload", DefaultAccess.OPERATORS),
+ /**
+ * Lets the user view the plugin version and check for updates {@code /husksync update}
+ */
+ COMMAND_HUSKSYNC_UPDATE("husksync.command.husksync.update", DefaultAccess.OPERATORS),
+ /**
+ * Lets the user save a player's data {@code /husksync save (player)}
+ */
+ COMMAND_HUSKSYNC_SAVE("husksync.command.husksync.save", DefaultAccess.OPERATORS),
+ /**
+ * Lets the user save all online player data {@code /husksync saveall}
+ */
+ COMMAND_HUSKSYNC_SAVE_ALL("husksync.command.husksync.saveall", DefaultAccess.OPERATORS),
+ /**
+ * Lets the user view a player's backup data {@code /husksync backup (player)}
+ */
+ COMMAND_HUSKSYNC_BACKUPS("husksync.command.husksync.backups", DefaultAccess.OPERATORS),
+ /**
+ * Lets the user restore a player's backup data {@code /husksync backup (player) restore (backup_uuid)}
+ */
+ COMMAND_HUSKSYNC_BACKUPS_RESTORE("husksync.command.husksync.backups.restore", DefaultAccess.OPERATORS),
+
+ /*
+ * /invsee command permissions
+ */
+
+ /**
+ * Lets the user use the {@code /invsee (player)} command and view offline players' inventories
+ */
+ COMMAND_VIEW_INVENTORIES("husksync.command.invsee", DefaultAccess.OPERATORS),
+ /**
+ * Lets the user edit the contents of offline players' inventories
+ */
+ COMMAND_EDIT_INVENTORIES("husksync.command.invsee.edit", DefaultAccess.OPERATORS),
+
+ /*
+ * /echest command permissions
+ */
+
+ /**
+ * Lets the user use the {@code /echest (player)} command and view offline players' ender chests
+ */
+ COMMAND_VIEW_ENDER_CHESTS("husksync.command.echest", DefaultAccess.OPERATORS),
+ /**
+ * Lets the user edit the contents of offline players' ender chests
+ */
+ COMMAND_EDIT_ENDER_CHESTS("husksync.command.echest.edit", DefaultAccess.OPERATORS);
+
+ public final String node;
+ public final DefaultAccess defaultAccess;
+
+ Permission(@NotNull String node, @NotNull DefaultAccess defaultAccess) {
+ this.node = node;
+ this.defaultAccess = defaultAccess;
+ }
+
+ /**
+ * Identifies who gets what permissions by default
+ */
+ public enum DefaultAccess {
+ /**
+ * Everyone gets this permission node by default
+ */
+ EVERYONE,
+ /**
+ * Nobody gets this permission node by default
+ */
+ NOBODY,
+ /**
+ * Server operators ({@code /op}) get this permission node by default
+ */
+ OPERATORS
+ }
+}
diff --git a/common/src/main/java/net/william278/husksync/command/TabCompletable.java b/common/src/main/java/net/william278/husksync/command/TabCompletable.java
new file mode 100644
index 00000000..6aca66fc
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/command/TabCompletable.java
@@ -0,0 +1,22 @@
+package net.william278.husksync.command;
+
+import net.william278.husksync.player.OnlineUser;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * Interface providing tab completions for a command
+ */
+public interface TabCompletable {
+
+ /**
+ * What should be returned when the player attempts to TAB-complete the command
+ *
+ * @param player {@link OnlineUser} doing the TAB completion
+ * @param args Current command arguments
+ * @return List of String arguments to offer TAB suggestions
+ */
+ List onTabComplete(@NotNull OnlineUser player, @NotNull String[] args);
+
+}
diff --git a/common/src/main/java/net/william278/husksync/config/Locales.java b/common/src/main/java/net/william278/husksync/config/Locales.java
new file mode 100644
index 00000000..aa6bd46f
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/config/Locales.java
@@ -0,0 +1,139 @@
+package net.william278.husksync.config;
+
+import de.themoep.minedown.MineDown;
+import dev.dejvokep.boostedyaml.YamlDocument;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.HashMap;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Loaded locales used by the plugin to display various locales
+ */
+public class Locales {
+
+ public static final String PLUGIN_INFORMATION = """
+ [HuskSync](#00fb9a bold) [| Version %version%(#00fb9a)
+ [A modern, cross-server player data synchronization system](gray)
+ [• Author:](white) [William278](gray show_text=&7Click to visit website open_url=https://william278.net)
+ [• Contributors:](white) [HarvelsX](gray show_text=&7Code)
+ [• 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)
+ [• Plugin Info:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/)
+ [• Report Issues:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/issues)
+ [• Support Discord:](white) [[Link]](#00fb9a show_text=&7Click to join open_url=https://discord.gg/tVYhJfyDWG)""";
+
+ @NotNull
+ private final HashMap rawLocales;
+
+ private Locales(@NotNull YamlDocument localesConfig) {
+ this.rawLocales = new HashMap<>();
+ for (String localeId : localesConfig.getRoutesAsStrings(false)) {
+ rawLocales.put(localeId, localesConfig.getString(localeId));
+ }
+ }
+
+ /**
+ * Returns an un-formatted locale loaded from the locales file
+ *
+ * @param localeId String identifier of the locale, corresponding to a key in the file
+ * @return An {@link Optional} containing the locale corresponding to the id, if it exists
+ */
+ public Optional getRawLocale(@NotNull String localeId) {
+ if (rawLocales.containsKey(localeId)) {
+ return Optional.of(rawLocales.get(localeId));
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Returns an un-formatted locale loaded from the locales file, with replacements applied
+ *
+ * @param localeId String identifier of the locale, corresponding to a key in the file
+ * @param replacements Ordered array of replacement strings to fill in placeholders with
+ * @return An {@link Optional} containing the replacement-applied locale corresponding to the id, if it exists
+ */
+ public Optional getRawLocale(@NotNull String localeId, @NotNull String... replacements) {
+ return getRawLocale(localeId).map(locale -> applyReplacements(locale, replacements));
+ }
+
+ /**
+ * Returns a MineDown-formatted locale from the locales file
+ *
+ * @param localeId String identifier of the locale, corresponding to a key in the file
+ * @return An {@link Optional} containing the formatted locale corresponding to the id, if it exists
+ */
+ public Optional getLocale(@NotNull String localeId) {
+ return getRawLocale(localeId).map(MineDown::new);
+ }
+
+ /**
+ * Returns a MineDown-formatted locale from the locales file, with replacements applied
+ *
+ * @param localeId String identifier of the locale, corresponding to a key in the file
+ * @param replacements Ordered array of replacement strings to fill in placeholders with
+ * @return An {@link Optional} containing the replacement-applied, formatted locale corresponding to the id, if it exists
+ */
+ public Optional getLocale(@NotNull String localeId, @NotNull String... replacements) {
+ return getRawLocale(localeId, replacements).map(MineDown::new);
+ }
+
+ /**
+ * Apply placeholder replacements to a raw locale
+ *
+ * @param rawLocale The raw, unparsed locale
+ * @param replacements Ordered array of replacement strings to fill in placeholders with
+ * @return the raw locale, with inserted placeholders
+ */
+ private String applyReplacements(@NotNull String rawLocale, @NotNull String... replacements) {
+ int replacementIndexer = 1;
+ for (String replacement : replacements) {
+ String replacementString = "%" + replacementIndexer + "%";
+ rawLocale = rawLocale.replace(replacementString, replacement);
+ replacementIndexer = replacementIndexer + 1;
+ }
+ return rawLocale;
+ }
+
+ /**
+ * Load the locales from a BoostedYaml {@link YamlDocument} locales file
+ *
+ * @param localesConfig The loaded {@link YamlDocument} locales.yml file
+ * @return the loaded {@link Locales}
+ */
+ public static Locales load(@NotNull YamlDocument localesConfig) {
+ return new Locales(localesConfig);
+ }
+
+ /**
+ * Strips a string of basic MineDown formatting, used for displaying plugin info to console
+ *
+ * @param string The string to strip
+ * @return The MineDown-stripped string
+ */
+ public String stripMineDown(@NotNull String string) {
+ final String[] in = string.split("\n");
+ final StringBuilder out = new StringBuilder();
+ String regex = "[^\\[\\]() ]*\\[([^()]+)]\\([^()]+open_url=(\\S+).*\\)";
+
+ for (int i = 0; i < in.length; i++) {
+ Pattern pattern = Pattern.compile(regex);
+ Matcher m = pattern.matcher(in[i]);
+
+ if (m.find()) {
+ out.append(in[i].replace(m.group(0), ""));
+ out.append(m.group(2));
+ } else {
+ out.append(in[i]);
+ }
+
+ if (i + 1 != in.length) {
+ out.append("\n");
+ }
+ }
+
+ return out.toString();
+ }
+
+}
diff --git a/common/src/main/java/net/william278/husksync/config/Settings.java b/common/src/main/java/net/william278/husksync/config/Settings.java
new file mode 100644
index 00000000..0ca41baf
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/config/Settings.java
@@ -0,0 +1,270 @@
+package net.william278.husksync.config;
+
+import dev.dejvokep.boostedyaml.YamlDocument;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Settings used for the plugin, as read from the config file
+ */
+public class Settings {
+
+ /**
+ * Map of {@link ConfigOption}s read from the config file
+ */
+ private final HashMap configOptions;
+
+ // Load the settings from the document
+ private Settings(@NotNull YamlDocument config) {
+ this.configOptions = new HashMap<>();
+ Arrays.stream(ConfigOption.values()).forEach(configOption -> configOptions
+ .put(configOption, switch (configOption.optionType) {
+ case BOOLEAN -> configOption.getBooleanValue(config);
+ case STRING -> configOption.getStringValue(config);
+ case DOUBLE -> configOption.getDoubleValue(config);
+ case FLOAT -> configOption.getFloatValue(config);
+ case INTEGER -> configOption.getIntValue(config);
+ case STRING_LIST -> configOption.getStringListValue(config);
+ }));
+ }
+
+ /**
+ * Get the value of the specified {@link ConfigOption}
+ *
+ * @param option the {@link ConfigOption} to check
+ * @return the value of the {@link ConfigOption} as a boolean
+ * @throws ClassCastException if the option is not a boolean
+ */
+ public boolean getBooleanValue(@NotNull ConfigOption option) throws ClassCastException {
+ return (Boolean) configOptions.get(option);
+ }
+
+ /**
+ * Get the value of the specified {@link ConfigOption}
+ *
+ * @param option the {@link ConfigOption} to check
+ * @return the value of the {@link ConfigOption} as a string
+ * @throws ClassCastException if the option is not a string
+ */
+ public String getStringValue(@NotNull ConfigOption option) throws ClassCastException {
+ return (String) configOptions.get(option);
+ }
+
+ /**
+ * Get the value of the specified {@link ConfigOption}
+ *
+ * @param option the {@link ConfigOption} to check
+ * @return the value of the {@link ConfigOption} as a double
+ * @throws ClassCastException if the option is not a double
+ */
+ public double getDoubleValue(@NotNull ConfigOption option) throws ClassCastException {
+ return (Double) configOptions.get(option);
+ }
+
+ /**
+ * Get the value of the specified {@link ConfigOption}
+ *
+ * @param option the {@link ConfigOption} to check
+ * @return the value of the {@link ConfigOption} as a float
+ * @throws ClassCastException if the option is not a float
+ */
+ public double getFloatValue(@NotNull ConfigOption option) throws ClassCastException {
+ return (Float) configOptions.get(option);
+ }
+
+ /**
+ * Get the value of the specified {@link ConfigOption}
+ *
+ * @param option the {@link ConfigOption} to check
+ * @return the value of the {@link ConfigOption} as an integer
+ * @throws ClassCastException if the option is not an integer
+ */
+ public int getIntegerValue(@NotNull ConfigOption option) throws ClassCastException {
+ return (Integer) configOptions.get(option);
+ }
+
+ /**
+ * Get the value of the specified {@link ConfigOption}
+ *
+ * @param option the {@link ConfigOption} to check
+ * @return the value of the {@link ConfigOption} as a string {@link List}
+ * @throws ClassCastException if the option is not a string list
+ */
+ @SuppressWarnings("unchecked")
+ public List getStringListValue(@NotNull ConfigOption option) throws ClassCastException {
+ return (List) configOptions.get(option);
+ }
+
+
+ /**
+ * Load the settings from a BoostedYaml {@link YamlDocument} config file
+ *
+ * @param config The loaded {@link YamlDocument} config.yml file
+ * @return the loaded {@link Settings}
+ */
+ public static Settings load(@NotNull YamlDocument config) {
+ return new Settings(config);
+ }
+
+ /**
+ * Represents an option stored by a path in config.yml
+ */
+ public enum ConfigOption {
+ LANGUAGE("language", OptionType.STRING, "en-gb"),
+ CHECK_FOR_UPDATES("check_for_updates", OptionType.BOOLEAN, true),
+
+ CLUSTER_ID("cluster_id", OptionType.STRING, ""), //todo implement this
+
+ DATABASE_HOST("database.credentials.host", OptionType.STRING, "localhost"),
+ DATABASE_PORT("database.credentials.port", OptionType.INTEGER, 3306),
+ DATABASE_NAME("database.credentials.database", OptionType.STRING, "HuskSync"),
+ DATABASE_USERNAME("database.credentials.username", OptionType.STRING, "root"),
+ DATABASE_PASSWORD("database.credentials.password", OptionType.STRING, "pa55w0rd"),
+ DATABASE_CONNECTION_PARAMS("database.credentials.params", OptionType.STRING, "?autoReconnect=true&useSSL=false"),
+ DATABASE_CONNECTION_POOL_MAX_SIZE("database.connection_pool.maximum_pool_size", OptionType.INTEGER, 10),
+ DATABASE_CONNECTION_POOL_MIN_IDLE("database.connection_pool.minimum_idle", OptionType.INTEGER, 10),
+ DATABASE_CONNECTION_POOL_MAX_LIFETIME("database.connection_pool.maximum_lifetime", OptionType.INTEGER, 1800000),
+ DATABASE_CONNECTION_POOL_KEEPALIVE("database.connection_pool.keepalive_time", OptionType.INTEGER, 0),
+ DATABASE_CONNECTION_POOL_TIMEOUT("database.connection_pool.connection_timeout", OptionType.INTEGER, 5000),
+ DATABASE_PLAYERS_TABLE_NAME("database.table_names.players_table", OptionType.STRING, "husksync_players"),
+ DATABASE_DATA_TABLE_NAME("database.table_names.data_table", OptionType.STRING, "husksync_data"),
+
+ REDIS_HOST("redis.credentials.host", OptionType.STRING, "localhost"),
+ REDIS_PORT("redis.credentials.port", OptionType.INTEGER, 6379),
+ REDIS_PASSWORD("redis.credentials.password", OptionType.STRING, ""),
+ REDIS_USE_SSL("redis.use_ssl", OptionType.BOOLEAN, false),
+
+ SYNCHRONIZATION_MAX_USER_DATA_RECORDS("synchronization.max_user_data_records", OptionType.INTEGER, 5),
+ SYNCHRONIZATION_SAVE_ON_WORLD_SAVE("synchronization.save_on_world_save", OptionType.BOOLEAN, true),
+ SYNCHRONIZATION_SYNC_INVENTORIES("synchronization.features.inventories", OptionType.BOOLEAN, true),
+ SYNCHRONIZATION_SYNC_ENDER_CHESTS("synchronization.features.ender_chests", OptionType.BOOLEAN, true),
+ SYNCHRONIZATION_SYNC_HEALTH("synchronization.features.health", OptionType.BOOLEAN, true),
+ SYNCHRONIZATION_SYNC_MAX_HEALTH("synchronization.features.max_health", OptionType.BOOLEAN, true),
+ SYNCHRONIZATION_SYNC_HUNGER("synchronization.features.hunger", OptionType.BOOLEAN, true),
+ SYNCHRONIZATION_SYNC_EXPERIENCE("synchronization.features.experience", OptionType.BOOLEAN, true),
+ SYNCHRONIZATION_SYNC_POTION_EFFECTS("synchronization.features.potion_effects", OptionType.BOOLEAN, true),
+ SYNCHRONIZATION_SYNC_ADVANCEMENTS("synchronization.features.advancements", OptionType.BOOLEAN, true),
+ SYNCHRONIZATION_SYNC_GAME_MODE("synchronization.features.game_mode", OptionType.BOOLEAN, true),
+ SYNCHRONIZATION_SYNC_STATISTICS("synchronization.features.statistics", OptionType.BOOLEAN, true),
+ SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER("synchronization.features.persistent_data_container", OptionType.BOOLEAN, true),
+ SYNCHRONIZATION_SYNC_LOCATION("synchronization.features.location", OptionType.BOOLEAN, true);
+
+ /**
+ * The path in the config.yml file to the value
+ */
+ @NotNull
+ public final String configPath;
+
+ /**
+ * The {@link OptionType} of this option
+ */
+ @NotNull
+ public final OptionType optionType;
+
+ /**
+ * The default value of this option if not set in config
+ */
+ @Nullable
+ private final Object defaultValue;
+
+ ConfigOption(@NotNull String configPath, @NotNull OptionType optionType, @Nullable Object defaultValue) {
+ this.configPath = configPath;
+ this.optionType = optionType;
+ this.defaultValue = defaultValue;
+ }
+
+ ConfigOption(@NotNull String configPath, @NotNull OptionType optionType) {
+ this.configPath = configPath;
+ this.optionType = optionType;
+ this.defaultValue = null;
+ }
+
+ /**
+ * Get the value at the path specified (or return default if set), as a string
+ *
+ * @param config The {@link YamlDocument} config file
+ * @return the value defined in the config, as a string
+ */
+ public String getStringValue(@NotNull YamlDocument config) {
+ return defaultValue != null
+ ? config.getString(configPath, (String) defaultValue)
+ : config.getString(configPath);
+ }
+
+ /**
+ * Get the value at the path specified (or return default if set), as a boolean
+ *
+ * @param config The {@link YamlDocument} config file
+ * @return the value defined in the config, as a boolean
+ */
+ public boolean getBooleanValue(@NotNull YamlDocument config) {
+ return defaultValue != null
+ ? config.getBoolean(configPath, (Boolean) defaultValue)
+ : config.getBoolean(configPath);
+ }
+
+ /**
+ * Get the value at the path specified (or return default if set), as a double
+ *
+ * @param config The {@link YamlDocument} config file
+ * @return the value defined in the config, as a double
+ */
+ public double getDoubleValue(@NotNull YamlDocument config) {
+ return defaultValue != null
+ ? config.getDouble(configPath, (Double) defaultValue)
+ : config.getDouble(configPath);
+ }
+
+ /**
+ * Get the value at the path specified (or return default if set), as a float
+ *
+ * @param config The {@link YamlDocument} config file
+ * @return the value defined in the config, as a float
+ */
+ public float getFloatValue(@NotNull YamlDocument config) {
+ return defaultValue != null
+ ? config.getFloat(configPath, (Float) defaultValue)
+ : config.getFloat(configPath);
+ }
+
+ /**
+ * Get the value at the path specified (or return default if set), as an int
+ *
+ * @param config The {@link YamlDocument} config file
+ * @return the value defined in the config, as an int
+ */
+ public int getIntValue(@NotNull YamlDocument config) {
+ return defaultValue != null
+ ? config.getInt(configPath, (Integer) defaultValue)
+ : config.getInt(configPath);
+ }
+
+ /**
+ * Get the value at the path specified (or return default if set), as a string {@link List}
+ *
+ * @param config The {@link YamlDocument} config file
+ * @return the value defined in the config, as a string {@link List}
+ */
+ public List getStringListValue(@NotNull YamlDocument config) {
+ return config.getStringList(configPath, new ArrayList<>());
+ }
+
+ /**
+ * Represents the type of the object
+ */
+ public enum OptionType {
+ BOOLEAN,
+ STRING,
+ DOUBLE,
+ FLOAT,
+ INTEGER,
+ STRING_LIST
+ }
+ }
+
+}
diff --git a/common/src/main/java/net/william278/husksync/data/AdvancementData.java b/common/src/main/java/net/william278/husksync/data/AdvancementData.java
new file mode 100644
index 00000000..d7df3f39
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/data/AdvancementData.java
@@ -0,0 +1,28 @@
+package net.william278.husksync.data;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * A mapped piece of advancement data
+ */
+public class AdvancementData {
+
+ /**
+ * The advancement namespaced key
+ */
+ @SerializedName("key")
+ public String key;
+
+ /**
+ * A map of completed advancement criteria to when it was completed
+ */
+ @SerializedName("completed_criteria")
+ public Map completedCriteria;
+
+ public AdvancementData() {
+ }
+
+}
diff --git a/common/src/main/java/net/william278/husksync/data/InventoryData.java b/common/src/main/java/net/william278/husksync/data/InventoryData.java
new file mode 100644
index 00000000..0487c42c
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/data/InventoryData.java
@@ -0,0 +1,24 @@
+package net.william278.husksync.data;
+
+import com.google.gson.annotations.SerializedName;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Stores information about a player's inventory or ender chest
+ */
+public class InventoryData {
+
+ /**
+ * A base64 string of platform-serialized inventory data
+ */
+ @SerializedName("serialized_inventory")
+ public String serializedInventory;
+
+ public InventoryData(@NotNull final String serializedInventory) {
+ this.serializedInventory = serializedInventory;
+ }
+
+ public InventoryData() {
+ }
+
+}
diff --git a/common/src/main/java/net/william278/husksync/data/LocationData.java b/common/src/main/java/net/william278/husksync/data/LocationData.java
new file mode 100644
index 00000000..ee8d2a5f
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/data/LocationData.java
@@ -0,0 +1,72 @@
+package net.william278.husksync.data;
+
+import com.google.gson.annotations.SerializedName;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Stores information about a player's location
+ */
+public class LocationData {
+
+ /**
+ * Name of the world on the server
+ */
+ @SerializedName("world_name")
+ public String worldName;
+ /**
+ * Unique id of the world
+ */
+ @SerializedName("world_uuid")
+ public UUID worldUuid;
+ /**
+ * The environment type of the world (one of "NORMAL", "NETHER", "THE_END")
+ */
+ @SerializedName("world_environment")
+ public String worldEnvironment;
+
+ /**
+ * The x coordinate of the location
+ */
+ @SerializedName("x")
+ public double x;
+ /**
+ * The y coordinate of the location
+ */
+ @SerializedName("y")
+ public double y;
+ /**
+ * The z coordinate of the location
+ */
+ @SerializedName("z")
+ public double z;
+
+ /**
+ * The location's facing yaw angle
+ */
+ @SerializedName("yaw")
+ public float yaw;
+ /**
+ * The location's facing pitch angle
+ */
+ @SerializedName("pitch")
+ public float pitch;
+
+ public LocationData() {
+ }
+
+ public LocationData(@NotNull String worldName, @NotNull UUID worldUuid,
+ @NotNull String worldEnvironment,
+ double x, double y, double z,
+ float yaw, float pitch) {
+ this.worldName = worldName;
+ this.worldUuid = worldUuid;
+ this.worldEnvironment = worldEnvironment;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.yaw = yaw;
+ this.pitch = pitch;
+ }
+}
diff --git a/common/src/main/java/net/william278/husksync/data/PersistentDataContainerData.java b/common/src/main/java/net/william278/husksync/data/PersistentDataContainerData.java
new file mode 100644
index 00000000..994446ed
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/data/PersistentDataContainerData.java
@@ -0,0 +1,24 @@
+package net.william278.husksync.data;
+
+import com.google.gson.annotations.SerializedName;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Store's a user's persistent data container, holding a map of plugin-set persistent values
+ */
+public class PersistentDataContainerData {
+
+ /**
+ * A base64 string of platform-serialized PersistentDataContainer data
+ */
+ @SerializedName("serialized_persistent_data_container")
+ public String serializedPersistentDataContainer;
+
+ public PersistentDataContainerData(@NotNull final String serializedPersistentDataContainer) {
+ this.serializedPersistentDataContainer = serializedPersistentDataContainer;
+ }
+
+ public PersistentDataContainerData() {
+ }
+
+}
diff --git a/common/src/main/java/net/william278/husksync/data/PotionEffectData.java b/common/src/main/java/net/william278/husksync/data/PotionEffectData.java
new file mode 100644
index 00000000..a8ad390e
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/data/PotionEffectData.java
@@ -0,0 +1,21 @@
+package net.william278.husksync.data;
+
+import com.google.gson.annotations.SerializedName;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Stores potion effect data
+ */
+public class PotionEffectData {
+
+ @SerializedName("serialized_potion_effects")
+ public String serializedPotionEffects;
+
+ public PotionEffectData(@NotNull final String serializedInventory) {
+ this.serializedPotionEffects = serializedInventory;
+ }
+
+ public PotionEffectData() {
+ }
+
+}
diff --git a/common/src/main/java/net/william278/husksync/data/StatisticsData.java b/common/src/main/java/net/william278/husksync/data/StatisticsData.java
new file mode 100644
index 00000000..2320064a
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/data/StatisticsData.java
@@ -0,0 +1,50 @@
+package net.william278.husksync.data;
+
+import com.google.gson.annotations.SerializedName;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.HashMap;
+
+/**
+ * Stores information about a player's statistics
+ */
+public class StatisticsData {
+
+ /**
+ * Map of untyped statistic names to their values
+ */
+ @SerializedName("untyped_statistics")
+ public HashMap untypedStatistic;
+
+ /**
+ * Map of block type statistics to a map of material types to values
+ */
+ @SerializedName("block_statistics")
+ public HashMap> blockStatistics;
+
+ /**
+ * Map of item type statistics to a map of material types to values
+ */
+ @SerializedName("item_statistics")
+ public HashMap> itemStatistics;
+
+ /**
+ * Map of entity type statistics to a map of entity types to values
+ */
+ @SerializedName("entity_statistics")
+ public HashMap> entityStatistics;
+
+ public StatisticsData(@NotNull HashMap untypedStatistic,
+ @NotNull HashMap> blockStatistics,
+ @NotNull HashMap> itemStatistics,
+ @NotNull HashMap> entityStatistics) {
+ this.untypedStatistic = untypedStatistic;
+ this.blockStatistics = blockStatistics;
+ this.itemStatistics = itemStatistics;
+ this.entityStatistics = entityStatistics;
+ }
+
+ public StatisticsData() {
+ }
+
+}
diff --git a/common/src/main/java/net/william278/husksync/data/StatusData.java b/common/src/main/java/net/william278/husksync/data/StatusData.java
new file mode 100644
index 00000000..4a8ba2a8
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/data/StatusData.java
@@ -0,0 +1,103 @@
+package net.william278.husksync.data;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Stores status information about a player
+ */
+public class StatusData {
+
+ /**
+ * The player's health points
+ */
+ @SerializedName("health")
+ public double health;
+
+ /**
+ * The player's maximum health points
+ */
+ @SerializedName("max_health")
+ public double maxHealth;
+
+ /**
+ * The player's health scaling factor
+ */
+ @SerializedName("health_scale")
+ public double healthScale;
+
+ /**
+ * The player's hunger points
+ */
+ @SerializedName("hunger")
+ public int hunger;
+
+ /**
+ * The player's saturation points
+ */
+ @SerializedName("saturation")
+ public float saturation;
+
+ /**
+ * The player's saturation exhaustion points
+ */
+ @SerializedName("saturation_exhaustion")
+ public float saturationExhaustion;
+
+ /**
+ * The player's currently selected item slot
+ */
+ @SerializedName("selected_item_slot")
+ public int selectedItemSlot;
+
+ /**
+ * The player's total experience points
+ * (not to be confused with experience level - this is the "points" value shown on the death screen)
+ */
+ @SerializedName("total_experience")
+ public int totalExperience;
+
+ /**
+ * The player's experience level (shown on the exp bar)
+ */
+ @SerializedName("experience_level")
+ public int expLevel;
+
+ /**
+ * The player's progress to their next experience level
+ */
+ @SerializedName("experience_progress")
+ public float expProgress;
+
+ /**
+ * The player's game mode string (one of "survival", "creative", "adventure", "spectator")
+ */
+ @SerializedName("game_mode")
+ public String gameMode;
+
+ /**
+ * If the player is currently flying
+ */
+ @SerializedName("is_flying")
+ public boolean isFlying;
+
+ public StatusData(final double health, final double maxHealth, final double healthScale,
+ final int hunger, final float saturation, final float saturationExhaustion,
+ final int selectedItemSlot, final int totalExperience, final int expLevel,
+ final float expProgress, final String gameMode, final boolean isFlying) {
+ this.health = health;
+ this.maxHealth = maxHealth;
+ this.healthScale = healthScale;
+ this.hunger = hunger;
+ this.saturation = saturation;
+ this.saturationExhaustion = saturationExhaustion;
+ this.selectedItemSlot = selectedItemSlot;
+ this.totalExperience = totalExperience;
+ this.expLevel = expLevel;
+ this.expProgress = expProgress;
+ this.gameMode = gameMode;
+ this.isFlying = isFlying;
+ }
+
+ public StatusData() {
+ }
+}
diff --git a/common/src/main/java/net/william278/husksync/data/UserData.java b/common/src/main/java/net/william278/husksync/data/UserData.java
new file mode 100644
index 00000000..b602e7d7
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/data/UserData.java
@@ -0,0 +1,159 @@
+package net.william278.husksync.data;
+
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.annotations.SerializedName;
+import org.jetbrains.annotations.NotNull;
+
+import java.time.Instant;
+import java.util.HashSet;
+import java.util.UUID;
+
+/***
+ * Stores data about a user
+ */
+public class UserData implements Comparable {
+
+ /**
+ * The unique identifier for this user data version
+ */
+ protected UUID dataUuidVersion;
+
+ /**
+ * An epoch milliseconds timestamp of when this data was created
+ */
+ protected long creationTimestamp;
+
+ /**
+ * Stores the user's status data, including health, food, etc.
+ */
+ @SerializedName("status")
+ protected StatusData statusData;
+
+ /**
+ * Stores the user's inventory contents
+ */
+ @SerializedName("inventory")
+ protected InventoryData inventoryData;
+
+ /**
+ * Stores the user's ender chest contents
+ */
+ @SerializedName("ender_chest")
+ protected InventoryData enderChestData;
+
+ /**
+ * Store's the user's potion effects
+ */
+ @SerializedName("potion_effects")
+ protected PotionEffectData potionEffectData;
+
+ /**
+ * Stores the set of this user's advancements
+ */
+ @SerializedName("advancements")
+ protected HashSet advancementData;
+
+ /**
+ * Stores the user's set of statistics
+ */
+ @SerializedName("statistics")
+ protected StatisticsData statisticData;
+
+ /**
+ * Store's the user's world location and coordinates
+ */
+ @SerializedName("location")
+ protected LocationData locationData;
+
+ /**
+ * Stores the user's serialized persistent data container, which contains metadata keys applied by other plugins
+ */
+ @SerializedName("persistent_data_container")
+ protected PersistentDataContainerData persistentDataContainerData;
+
+ public UserData(@NotNull StatusData statusData, @NotNull InventoryData inventoryData,
+ @NotNull InventoryData enderChestData, @NotNull PotionEffectData potionEffectData,
+ @NotNull HashSet advancementData, @NotNull StatisticsData statisticData,
+ @NotNull LocationData locationData, @NotNull PersistentDataContainerData persistentDataContainerData) {
+ this.dataUuidVersion = UUID.randomUUID();
+ this.creationTimestamp = Instant.now().toEpochMilli();
+ this.statusData = statusData;
+ this.inventoryData = inventoryData;
+ this.enderChestData = enderChestData;
+ this.potionEffectData = potionEffectData;
+ this.advancementData = advancementData;
+ this.statisticData = statisticData;
+ this.locationData = locationData;
+ this.persistentDataContainerData = persistentDataContainerData;
+ }
+
+ protected UserData() {
+ }
+
+ /**
+ * Compare UserData by creation timestamp
+ *
+ * @param other the other UserData to be compared
+ * @return the comparison result; the more recent UserData is greater than the less recent UserData
+ */
+ @Override
+ public int compareTo(@NotNull UserData other) {
+ return Long.compare(this.creationTimestamp, other.creationTimestamp);
+ }
+
+ @NotNull
+ public static UserData fromJson(String json) throws JsonSyntaxException {
+ return new GsonBuilder().create().fromJson(json, UserData.class);
+ }
+
+ @NotNull
+ public String toJson() {
+ return new GsonBuilder().create().toJson(this);
+ }
+
+ public void setMetadata(@NotNull UUID dataUuidVersion, long creationTimestamp) {
+ this.dataUuidVersion = dataUuidVersion;
+ this.creationTimestamp = creationTimestamp;
+ }
+
+ public UUID getDataUuidVersion() {
+ return dataUuidVersion;
+ }
+
+ public long getCreationTimestamp() {
+ return creationTimestamp;
+ }
+
+ public StatusData getStatusData() {
+ return statusData;
+ }
+
+ public InventoryData getInventoryData() {
+ return inventoryData;
+ }
+
+ public InventoryData getEnderChestData() {
+ return enderChestData;
+ }
+
+ public PotionEffectData getPotionEffectData() {
+ return potionEffectData;
+ }
+
+ public HashSet getAdvancementData() {
+ return advancementData;
+ }
+
+ public StatisticsData getStatisticData() {
+ return statisticData;
+ }
+
+ public LocationData getLocationData() {
+ return locationData;
+ }
+
+ public PersistentDataContainerData getPersistentDataContainerData() {
+ return persistentDataContainerData;
+ }
+}
diff --git a/common/src/main/java/net/william278/husksync/database/Database.java b/common/src/main/java/net/william278/husksync/database/Database.java
new file mode 100644
index 00000000..707d9426
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/database/Database.java
@@ -0,0 +1,156 @@
+package net.william278.husksync.database;
+
+import net.william278.husksync.data.UserData;
+import net.william278.husksync.player.User;
+import net.william278.husksync.util.Logger;
+import net.william278.husksync.util.ResourceReader;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * An abstract representation of the plugin database, storing player data.
+ *
+ * Implemented by different database platforms - MySQL, SQLite, etc. - as configured by the administrator.
+ */
+public abstract class Database {
+
+ /**
+ * Name of the table that stores player information
+ */
+ protected final String playerTableName;
+
+ /**
+ * Name of the table that stores data
+ */
+ protected final String dataTableName;
+
+ /**
+ * The maximum number of user records to store in the database at once per user
+ */
+ protected final int maxUserDataRecords;
+
+ /**
+ * Logger instance used for database error logging
+ */
+ private final Logger logger;
+
+ /**
+ * Returns the {@link Logger} used to log database errors
+ *
+ * @return the {@link Logger} instance
+ */
+ protected Logger getLogger() {
+ return logger;
+ }
+
+ /**
+ * The {@link ResourceReader} used to read internal resource files by name
+ */
+ private final ResourceReader resourceReader;
+
+ protected Database(@NotNull String playerTableName, @NotNull String dataTableName, final int maxUserDataRecords,
+ @NotNull ResourceReader resourceReader, @NotNull Logger logger) {
+ this.playerTableName = playerTableName;
+ this.dataTableName = dataTableName;
+ this.maxUserDataRecords = maxUserDataRecords;
+ this.resourceReader = resourceReader;
+ this.logger = logger;
+ }
+
+ /**
+ * Loads SQL table creation schema statements from a resource file as a string array
+ *
+ * @param schemaFileName database script resource file to load from
+ * @return Array of string-formatted table creation schema statements
+ * @throws IOException if the resource could not be read
+ */
+ protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException {
+ return formatStatementTables(
+ new String(resourceReader.getResource(schemaFileName)
+ .readAllBytes(), StandardCharsets.UTF_8))
+ .split(";");
+ }
+
+ /**
+ * Format all table name placeholder strings in a SQL statement
+ *
+ * @param sql the SQL statement with un-formatted table name placeholders
+ * @return the formatted statement, with table placeholders replaced with the correct names
+ */
+ protected final String formatStatementTables(@NotNull String sql) {
+ return sql.replaceAll("%players_table%", playerTableName)
+ .replaceAll("%data_table%", dataTableName);
+ }
+
+ /**
+ * Initialize the database and ensure tables are present; create tables if they do not exist.
+ *
+ * @return A future returning void when complete
+ */
+ public abstract CompletableFuture initialize();
+
+ /**
+ * Ensure a {@link User} has an entry in the database and that their username is up-to-date
+ *
+ * @param user The {@link User} to ensure
+ * @return A future returning void when complete
+ */
+ public abstract CompletableFuture ensureUser(@NotNull User user);
+
+ /**
+ * Get a player by their Minecraft account {@link UUID}
+ *
+ * @param uuid Minecraft account {@link UUID} of the {@link User} to get
+ * @return A future returning an optional with the {@link User} present if they exist
+ */
+ public abstract CompletableFuture> getUser(@NotNull UUID uuid);
+
+ /**
+ * Get a user by their username (case-insensitive)
+ *
+ * @param username Username of the {@link User} to get (case-insensitive)
+ * @return A future returning an optional with the {@link User} present if they exist
+ */
+ public abstract CompletableFuture> getUserByName(@NotNull String username);
+
+ /**
+ * Get the current user data for a given user, if it exists.
+ *
+ * @param user the user to get data for
+ * @return an optional containing the user data, if it exists, or an empty optional if it does not
+ */
+ public abstract CompletableFuture> getCurrentUserData(@NotNull User user);
+
+ /**
+ * Get all UserData entries for a user from the database.
+ *
+ * @param user The user to get data for
+ * @return A future returning a list of a user's data
+ */
+ public abstract CompletableFuture> getUserData(@NotNull User user);
+
+ /**
+ * Prune user data records for a given user to the maximum value as configured
+ *
+ * @param user The user to prune data for
+ * @return A future returning void when complete
+ */
+ protected abstract CompletableFuture pruneUserDataRecords(@NotNull User user);
+
+ /**
+ * Add user data to the database
+ * This will remove the oldest data for the user if the amount of data exceeds the limit as configured
+ *
+ * @param user The user to add data for
+ * @param userData The data to add
+ * @return A future returning void when complete
+ */
+ public abstract CompletableFuture setUserData(@NotNull User user, @NotNull UserData userData);
+
+}
diff --git a/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java b/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java
new file mode 100644
index 00000000..ff3af9f7
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java
@@ -0,0 +1,289 @@
+package net.william278.husksync.database;
+
+import com.zaxxer.hikari.HikariDataSource;
+import net.william278.husksync.config.Settings;
+import net.william278.husksync.data.UserData;
+import net.william278.husksync.player.User;
+import net.william278.husksync.util.Logger;
+import net.william278.husksync.util.ResourceReader;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.sql.*;
+import java.time.Instant;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.logging.Level;
+
+public class MySqlDatabase extends Database {
+ /**
+ * MySQL server hostname
+ */
+ private final String mySqlHost;
+
+ /**
+ * MySQL server port
+ */
+ private final int mySqlPort;
+
+ /**
+ * Database to use on the MySQL server
+ */
+ private final String mySqlDatabaseName;
+ private final String mySqlUsername;
+ private final String mySqlPassword;
+ private final String mySqlConnectionParameters;
+
+ private final int hikariMaximumPoolSize;
+ private final int hikariMinimumIdle;
+ private final int hikariMaximumLifetime;
+ private final int hikariKeepAliveTime;
+ private final int hikariConnectionTimeOut;
+
+ private static final String DATA_POOL_NAME = "HuskHomesHikariPool";
+
+ private HikariDataSource dataSource;
+
+ public MySqlDatabase(@NotNull Settings settings, @NotNull ResourceReader resourceReader, @NotNull Logger logger) {
+ super(settings.getStringValue(Settings.ConfigOption.DATABASE_PLAYERS_TABLE_NAME),
+ settings.getStringValue(Settings.ConfigOption.DATABASE_DATA_TABLE_NAME),
+ settings.getIntegerValue(Settings.ConfigOption.SYNCHRONIZATION_MAX_USER_DATA_RECORDS),
+ resourceReader, logger);
+ mySqlHost = settings.getStringValue(Settings.ConfigOption.DATABASE_HOST);
+ mySqlPort = settings.getIntegerValue(Settings.ConfigOption.DATABASE_PORT);
+ mySqlDatabaseName = settings.getStringValue(Settings.ConfigOption.DATABASE_NAME);
+ mySqlUsername = settings.getStringValue(Settings.ConfigOption.DATABASE_USERNAME);
+ mySqlPassword = settings.getStringValue(Settings.ConfigOption.DATABASE_PASSWORD);
+ mySqlConnectionParameters = settings.getStringValue(Settings.ConfigOption.DATABASE_CONNECTION_PARAMS);
+ hikariMaximumPoolSize = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MAX_SIZE);
+ hikariMinimumIdle = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MIN_IDLE);
+ hikariMaximumLifetime = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MAX_LIFETIME);
+ hikariKeepAliveTime = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_KEEPALIVE);
+ hikariConnectionTimeOut = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_TIMEOUT);
+ }
+
+ /**
+ * Fetch the auto-closeable connection from the hikariDataSource
+ *
+ * @return The {@link Connection} to the MySQL database
+ * @throws SQLException if the connection fails for some reason
+ */
+ private Connection getConnection() throws SQLException {
+ return dataSource.getConnection();
+ }
+
+ @Override
+ public CompletableFuture initialize() {
+ return CompletableFuture.runAsync(() -> {
+ // Create jdbc driver connection url
+ final String jdbcUrl = "jdbc:mysql://" + mySqlHost + ":" + mySqlPort + "/" + mySqlDatabaseName + mySqlConnectionParameters;
+ dataSource = new HikariDataSource();
+ dataSource.setJdbcUrl(jdbcUrl);
+
+ // Authenticate
+ dataSource.setUsername(mySqlUsername);
+ dataSource.setPassword(mySqlPassword);
+
+ // Set various additional parameters
+ dataSource.setMaximumPoolSize(hikariMaximumPoolSize);
+ dataSource.setMinimumIdle(hikariMinimumIdle);
+ dataSource.setMaxLifetime(hikariMaximumLifetime);
+ dataSource.setKeepaliveTime(hikariKeepAliveTime);
+ dataSource.setConnectionTimeout(hikariConnectionTimeOut);
+ dataSource.setPoolName(DATA_POOL_NAME);
+
+ // Prepare database schema; make tables if they don't exist
+ try (Connection connection = dataSource.getConnection()) {
+ // Load database schema CREATE statements from schema file
+ final String[] databaseSchema = getSchemaStatements("database/mysql_schema.sql");
+ try (Statement statement = connection.createStatement()) {
+ for (String tableCreationStatement : databaseSchema) {
+ statement.execute(tableCreationStatement);
+ }
+ }
+ } catch (SQLException | IOException e) {
+ getLogger().log(Level.SEVERE, "An error occurred creating tables on the MySQL database: ", e);
+ }
+ });
+ }
+
+ @Override
+ public CompletableFuture ensureUser(@NotNull User user) {
+ return CompletableFuture.runAsync(() -> getUser(user.uuid).thenAccept(optionalUser ->
+ optionalUser.ifPresentOrElse(existingUser -> {
+ if (!existingUser.username.equals(user.username)) {
+ // Update a user's name if it has changed in the database
+ try (Connection connection = getConnection()) {
+ try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
+ UPDATE `%players_table%`
+ SET `username`=?
+ WHERE `uuid`=?"""))) {
+
+ statement.setString(1, user.username);
+ statement.setString(2, existingUser.uuid.toString());
+ statement.executeUpdate();
+ }
+ getLogger().log(Level.INFO, "Updated " + user.username + "'s name in the database (" + existingUser.username + " -> " + user.username + ")");
+ } catch (SQLException e) {
+ getLogger().log(Level.SEVERE, "Failed to update a user's name on the database", e);
+ }
+ }
+ },
+ () -> {
+ // Insert new player data into the database
+ try (Connection connection = getConnection()) {
+ try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
+ INSERT INTO `%players_table%` (`uuid`,`username`)
+ VALUES (?,?);"""))) {
+
+ statement.setString(1, user.uuid.toString());
+ statement.setString(2, user.username);
+ statement.executeUpdate();
+ }
+ } catch (SQLException e) {
+ getLogger().log(Level.SEVERE, "Failed to insert a user into the database", e);
+ }
+ })));
+ }
+
+ @Override
+ public CompletableFuture> getUser(@NotNull UUID uuid) {
+ return CompletableFuture.supplyAsync(() -> {
+ try (Connection connection = getConnection()) {
+ try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
+ SELECT `uuid`, `username`
+ FROM `%players_table%`
+ WHERE `uuid`=?"""))) {
+
+ statement.setString(1, uuid.toString());
+
+ final ResultSet resultSet = statement.executeQuery();
+ if (resultSet.next()) {
+ return Optional.of(new User(UUID.fromString(resultSet.getString("uuid")),
+ resultSet.getString("username")));
+ }
+ }
+ } catch (SQLException e) {
+ getLogger().log(Level.SEVERE, "Failed to fetch a user from uuid from the database", e);
+ }
+ return Optional.empty();
+ });
+ }
+
+ @Override
+ public CompletableFuture> getUserByName(@NotNull String username) {
+ return CompletableFuture.supplyAsync(() -> {
+ try (Connection connection = getConnection()) {
+ try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
+ SELECT `uuid`, `username`
+ FROM `%players_table%`
+ WHERE `username`=?"""))) {
+ statement.setString(1, username);
+
+ final ResultSet resultSet = statement.executeQuery();
+ if (resultSet.next()) {
+ return Optional.of(new User(UUID.fromString(resultSet.getString("uuid")),
+ resultSet.getString("username")));
+ }
+ }
+ } catch (SQLException e) {
+ getLogger().log(Level.SEVERE, "Failed to fetch a user by name from the database", e);
+ }
+ return Optional.empty();
+ });
+ }
+
+ @Override
+ public CompletableFuture> getCurrentUserData(@NotNull User user) {
+ return CompletableFuture.supplyAsync(() -> {
+ try (Connection connection = getConnection()) {
+ try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
+ SELECT `version_uuid`, `timestamp`, `data`
+ FROM `%data_table%`
+ WHERE `player_uuid`=?
+ ORDER BY `timestamp` DESC
+ LIMIT 1;"""))) {
+ statement.setString(1, user.uuid.toString());
+ final ResultSet resultSet = statement.executeQuery();
+ if (resultSet.next()) {
+ final UserData data = UserData.fromJson(resultSet.getString("data"));
+ data.setMetadata(UUID.fromString(resultSet.getString("version_uuid")),
+ resultSet.getTimestamp("timestamp").toInstant().toEpochMilli());
+ return Optional.of(data);
+ }
+ }
+ } catch (SQLException e) {
+ getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
+ }
+ return Optional.empty();
+ });
+ }
+
+ @Override
+ public CompletableFuture> getUserData(@NotNull User user) {
+ return CompletableFuture.supplyAsync(() -> {
+ final ArrayList retrievedData = new ArrayList<>();
+ try (Connection connection = getConnection()) {
+ try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
+ SELECT `version_uuid`, `timestamp`, `data`
+ FROM `%data_table%`
+ WHERE `player_uuid`=?
+ ORDER BY `timestamp` DESC;"""))) {
+ statement.setString(1, user.uuid.toString());
+ final ResultSet resultSet = statement.executeQuery();
+ while (resultSet.next()) {
+ final UserData data = UserData.fromJson(resultSet.getString("data"));
+ data.setMetadata(UUID.fromString(resultSet.getString("version_uuid")),
+ resultSet.getTimestamp("timestamp").toInstant().toEpochMilli());
+ retrievedData.add(data);
+ }
+ return retrievedData;
+ }
+ } catch (SQLException e) {
+ getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
+ }
+ return retrievedData;
+ });
+ }
+
+ @Override
+ protected CompletableFuture pruneUserDataRecords(@NotNull User user) {
+ return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(data -> {
+ if (data.size() > maxUserDataRecords) {
+ Collections.reverse(data);
+ data.subList(0, data.size() - maxUserDataRecords).forEach(dataToDelete -> {
+ try (Connection connection = getConnection()) {
+ try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
+ DELETE FROM `%data_table%`
+ WHERE `version_uuid`=?"""))) {
+ statement.setString(1, dataToDelete.getDataUuidVersion().toString());
+ statement.executeUpdate();
+ }
+ } catch (SQLException e) {
+ getLogger().log(Level.SEVERE, "Failed to prune user data from the database", e);
+ }
+ });
+ }
+ }));
+ }
+
+ @Override
+ public CompletableFuture setUserData(@NotNull User user, @NotNull UserData userData) {
+ return CompletableFuture.runAsync(() -> {
+ try (Connection connection = getConnection()) {
+ try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
+ INSERT INTO `%data_table%`
+ (`player_uuid`,`version_uuid`,`timestamp`,`data`)
+ VALUES (?,?,?,?);"""))) {
+ statement.setString(1, user.uuid.toString());
+ statement.setString(2, userData.getDataUuidVersion().toString());
+ statement.setTimestamp(3, Timestamp.from(Instant.ofEpochMilli(userData.getCreationTimestamp())));
+ statement.setString(4, userData.toJson());
+ statement.executeUpdate();
+ }
+ } catch (SQLException e) {
+ getLogger().log(Level.SEVERE, "Failed to set user data in the database", e);
+ }
+ }).thenRunAsync(() -> pruneUserDataRecords(user).join());
+ }
+}
diff --git a/common/src/main/java/net/william278/husksync/listener/EventListener.java b/common/src/main/java/net/william278/husksync/listener/EventListener.java
new file mode 100644
index 00000000..9fba36f7
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/listener/EventListener.java
@@ -0,0 +1,53 @@
+package net.william278.husksync.listener;
+
+import net.william278.husksync.HuskSync;
+import net.william278.husksync.player.OnlineUser;
+import net.william278.husksync.redis.RedisManager;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+public class EventListener {
+
+ private final HuskSync huskSync;
+ private final HashSet usersAwaitingSync;
+
+ protected EventListener(@NotNull HuskSync huskSync) {
+ this.huskSync = huskSync;
+ this.usersAwaitingSync = new HashSet<>();
+ }
+
+ public final void handlePlayerJoin(@NotNull OnlineUser user) {
+ usersAwaitingSync.add(user.uuid);
+ huskSync.getRedisManager().getUserData(user, RedisManager.RedisKeyType.SERVER_CHANGE).thenAccept(
+ cachedUserData -> cachedUserData.ifPresentOrElse(
+ userData -> user.setData(userData, huskSync.getSettings()).join(),
+ () -> huskSync.getDatabase().getCurrentUserData(user).thenAccept(
+ databaseUserData -> databaseUserData.ifPresent(
+ data -> user.setData(data, huskSync.getSettings()).join())).join())).thenRunAsync(
+ () -> {
+ huskSync.getLocales().getLocale("synchronisation_complete").ifPresent(user::sendActionBar);
+ usersAwaitingSync.remove(user.uuid);
+ huskSync.getDatabase().ensureUser(user).join();
+ });
+ }
+
+ public final void handlePlayerQuit(@NotNull OnlineUser user) {
+ user.getUserData().thenAccept(userData -> huskSync.getRedisManager()
+ .setPlayerData(user, userData, RedisManager.RedisKeyType.SERVER_CHANGE).thenRun(
+ () -> huskSync.getDatabase().setUserData(user, userData).join()));
+ }
+
+ public final void handleWorldSave(@NotNull List usersInWorld) {
+ CompletableFuture.runAsync(() -> usersInWorld.forEach(user ->
+ huskSync.getDatabase().setUserData(user, user.getUserData().join()).join()));
+ }
+
+ public final boolean cancelPlayerEvent(@NotNull OnlineUser user) {
+ return usersAwaitingSync.contains(user.uuid);
+ }
+
+}
diff --git a/common/src/main/java/net/william278/husksync/migrator/MPDBMigrator.java b/common/src/main/java/net/william278/husksync/migrator/MPDBMigrator.java
deleted file mode 100644
index cf16fc93..00000000
--- a/common/src/main/java/net/william278/husksync/migrator/MPDBMigrator.java
+++ /dev/null
@@ -1,312 +0,0 @@
-package net.william278.husksync.migrator;
-
-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;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.UUID;
-import java.util.logging.Level;
-
-/**
- * Class to handle migration of data from MySQLPlayerDataBridge
- *
- * The migrator accesses and decodes MPDB's format directly,
- * by communicating with a Spigot server
- */
-public class MPDBMigrator {
-
- public int migratedDataSent = 0;
- public int playersMigrated = 0;
-
- public HashMap incomingPlayerData;
-
- public MigrationSettings migrationSettings = new MigrationSettings();
- private Settings.SynchronisationCluster targetCluster;
- private Database sourceDatabase;
-
- private HashSet mpdbPlayerData;
-
- private final Logger logger;
-
- public MPDBMigrator(Logger logger) {
- this.logger = logger;
- }
-
- public boolean readyToMigrate(int networkPlayerCount, HashSet synchronisedServers) {
- if (networkPlayerCount > 0) {
- logger.log(Level.WARNING, "Failed to start migration because there are players online. " +
- "Your network has to be empty to migrate data for safety reasons.");
- return false;
- }
-
- int synchronisedServersWithMpdb = 0;
- for (Server server : synchronisedServers) {
- if (server.hasMySqlPlayerDataBridge()) {
- synchronisedServersWithMpdb++;
- }
- }
- if (synchronisedServersWithMpdb < 1) {
- logger.log(Level.WARNING, "Failed to start migration because at least one Spigot server with both HuskSync and MySqlPlayerDataBridge installed is not online. " +
- "Please start one Spigot server with HuskSync installed to begin migration.");
- return false;
- }
-
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- if (migrationSettings.targetCluster.equals(cluster.clusterId())) {
- targetCluster = cluster;
- break;
- }
- }
- if (targetCluster == null) {
- logger.log(Level.WARNING, "Failed to start migration because the target cluster could not be found. " +
- "Please ensure the target cluster is correct, configured in the proxy config file, then try again");
- return false;
- }
-
- migratedDataSent = 0;
- playersMigrated = 0;
- mpdbPlayerData = new HashSet<>();
- incomingPlayerData = new HashMap<>();
- final MigrationSettings settings = migrationSettings;
-
- // Get connection to source database
- sourceDatabase = new MigratorMySQL(logger, settings.sourceHost, settings.sourcePort,
- settings.sourceDatabase, settings.sourceUsername, settings.sourcePassword, targetCluster);
- sourceDatabase.load();
- if (sourceDatabase.isInactive()) {
- logger.log(Level.WARNING, "Failed to establish connection to the origin MySQL database. " +
- "Please check you have input the correct connection details and try again.");
- return false;
- }
-
- return true;
- }
-
- // Carry out the migration
- public void executeMigrationOperations(DataManager dataManager, HashSet synchronisedServers, RedisListener redisListener) {
- // Prepare the target database for insertion
- prepareTargetDatabase(dataManager);
-
- // Fetch inventory data from MPDB
- getInventoryData();
-
- // Fetch ender chest data from MPDB
- getEnderChestData();
-
- // Fetch experience data from MPDB
- getExperienceData();
-
- // Send the encoded data to the Bukkit servers for conversion
- sendEncodedData(synchronisedServers, redisListener);
- }
-
- // Clear the new database out of current data
- private void prepareTargetDatabase(DataManager dataManager) {
- logger.log(Level.INFO, "Preparing target database...");
- try (Connection connection = dataManager.getConnection(targetCluster.clusterId())) {
- try (PreparedStatement statement = connection.prepareStatement("DELETE FROM " + targetCluster.playerTableName() + ";")) {
- statement.executeUpdate();
- }
- try (PreparedStatement statement = connection.prepareStatement("DELETE FROM " + targetCluster.dataTableName() + ";")) {
- statement.executeUpdate();
- }
- } catch (SQLException e) {
- logger.log(Level.SEVERE, "An exception occurred preparing the target database", e);
- } finally {
- logger.log(Level.INFO, "Finished preparing target database!");
- }
- }
-
- private void getInventoryData() {
- logger.log(Level.INFO, "Getting inventory data from MySQLPlayerDataBridge...");
- try (Connection connection = sourceDatabase.getConnection()) {
- try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM " + migrationSettings.inventoryDataTable + ";")) {
- ResultSet resultSet = statement.executeQuery();
- while (resultSet.next()) {
- final UUID playerUUID = UUID.fromString(resultSet.getString("player_uuid"));
- final String playerName = resultSet.getString("player_name");
-
- MPDBPlayerData data = new MPDBPlayerData(playerUUID, playerName);
- data.inventoryData = resultSet.getString("inventory");
- data.armorData = resultSet.getString("armor");
-
- mpdbPlayerData.add(data);
- }
- }
- } catch (SQLException e) {
- logger.log(Level.SEVERE, "An exception occurred getting inventory data", e);
- } finally {
- logger.log(Level.INFO, "Finished getting inventory data from MySQLPlayerDataBridge");
- }
- }
-
- private void getEnderChestData() {
- logger.log(Level.INFO, "Getting ender chest data from MySQLPlayerDataBridge...");
- try (Connection connection = sourceDatabase.getConnection()) {
- try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM " + migrationSettings.enderChestDataTable + ";")) {
- ResultSet resultSet = statement.executeQuery();
- while (resultSet.next()) {
- final UUID playerUUID = UUID.fromString(resultSet.getString("player_uuid"));
-
- for (MPDBPlayerData data : mpdbPlayerData) {
- if (data.playerUUID.equals(playerUUID)) {
- data.enderChestData = resultSet.getString("enderchest");
- break;
- }
- }
- }
- }
- } catch (SQLException e) {
- logger.log(Level.SEVERE, "An exception occurred getting ender chest data", e);
- } finally {
- logger.log(Level.INFO, "Finished getting ender chest data from MySQLPlayerDataBridge");
- }
- }
-
- private void getExperienceData() {
- logger.log(Level.INFO, "Getting experience data from MySQLPlayerDataBridge...");
- try (Connection connection = sourceDatabase.getConnection()) {
- try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM " + migrationSettings.expDataTable + ";")) {
- ResultSet resultSet = statement.executeQuery();
- while (resultSet.next()) {
- final UUID playerUUID = UUID.fromString(resultSet.getString("player_uuid"));
-
- for (MPDBPlayerData data : mpdbPlayerData) {
- if (data.playerUUID.equals(playerUUID)) {
- data.expLevel = resultSet.getInt("exp_lvl");
- data.expProgress = resultSet.getFloat("exp");
- data.totalExperience = resultSet.getInt("total_exp");
- break;
- }
- }
- }
- }
- } catch (SQLException e) {
- logger.log(Level.SEVERE, "An exception occurred getting experience data", e);
- } finally {
- logger.log(Level.INFO, "Finished getting experience data from MySQLPlayerDataBridge");
- }
- }
-
- private void sendEncodedData(HashSet synchronisedServers, RedisListener redisListener) {
- for (Server processingServer : synchronisedServers) {
- if (processingServer.hasMySqlPlayerDataBridge()) {
- for (MPDBPlayerData data : mpdbPlayerData) {
- try {
- new RedisMessage(RedisMessage.MessageType.DECODE_MPDB_DATA,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null),
- processingServer.serverUUID().toString(),
- RedisMessage.serialize(data))
- .send();
- migratedDataSent++;
- } catch (IOException e) {
- logger.log(Level.SEVERE, "Failed to serialize encoded MPDB data", e);
- }
- }
- logger.log(Level.INFO, "Finished dispatching encoded data for " + migratedDataSent + " players; please wait for conversion to finish");
- }
- return;
- }
- }
-
- /**
- * Loads all incoming decoded MPDB data to the cache and database
- *
- * @param dataToLoad HashMap of the {@link PlayerData} to player Usernames that will be loaded
- */
- public void loadIncomingData(HashMap dataToLoad, DataManager dataManager) {
- int playersSaved = 0;
- logger.log(Level.INFO, "Saving data for " + playersMigrated + " players...");
-
- for (PlayerData playerData : dataToLoad.keySet()) {
- String playerName = dataToLoad.get(playerData);
-
- // Add the player to the MySQL table
- dataManager.ensurePlayerExists(playerData.getPlayerUUID(), playerName);
-
- // Update the data in the cache and SQL
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- dataManager.updatePlayerData(playerData, cluster);
- break;
- }
-
- playersSaved++;
- logger.log(Level.INFO, "Saved data for " + playersSaved + "/" + playersMigrated + " players");
- }
-
- // Mark as done when done
- logger.log(Level.INFO, """
- === MySQLPlayerDataBridge Migration Wizard ==========
-
- Migration complete!
-
- Successfully migrated data for %1%/%2% players.
-
- You should now uninstall MySQLPlayerDataBridge from
- the rest of the Spigot servers, then restart them.
- """.replaceAll("%1%", Integer.toString(playersMigrated))
- .replaceAll("%2%", Integer.toString(migratedDataSent)));
- sourceDatabase.close(); // Close source database
- }
-
- /**
- * Class used to hold settings for the MPDB migration
- */
- public static class MigrationSettings {
- public String sourceHost;
- public int sourcePort;
- public String sourceDatabase;
- public String sourceUsername;
- public String sourcePassword;
-
- public String inventoryDataTable;
- public String enderChestDataTable;
- public String expDataTable;
-
- public String targetCluster;
-
- public MigrationSettings() {
- sourceHost = "localhost";
- sourcePort = 3306;
- sourceDatabase = "mpdb";
- sourceUsername = "root";
- sourcePassword = "pa55w0rd";
-
- targetCluster = "main";
-
- inventoryDataTable = "mpdb_inventory";
- enderChestDataTable = "mpdb_enderchest";
- expDataTable = "mpdb_experience";
- }
- }
-
- /**
- * MySQL class used for importing data from MPDB
- */
- public static class MigratorMySQL extends MySQL {
- public MigratorMySQL(Logger logger, String host, int port, String database, String username, String password, Settings.SynchronisationCluster cluster) {
- super(cluster, logger);
- super.host = host;
- super.port = port;
- super.database = database;
- super.username = username;
- super.password = password;
- super.params = "?useSSL=false";
- super.dataPoolName = super.dataPoolName + "Migrator";
- }
- }
-
-}
diff --git a/common/src/main/java/net/william278/husksync/migrator/MPDBPlayerData.java b/common/src/main/java/net/william278/husksync/migrator/MPDBPlayerData.java
deleted file mode 100644
index e83228c8..00000000
--- a/common/src/main/java/net/william278/husksync/migrator/MPDBPlayerData.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package net.william278.husksync.migrator;
-
-import java.io.Serializable;
-import java.util.UUID;
-
-/**
- * A class that stores player data taken from MPDB's database, that can then be converted into HuskSync's format
- */
-public class MPDBPlayerData implements Serializable {
-
- /*
- * Player information
- */
- public final UUID playerUUID;
- public final String playerName;
-
- /*
- * Inventory, ender chest and armor data
- */
- public String inventoryData;
- public String armorData;
- public String enderChestData;
-
- /*
- * Experience data
- */
- public int expLevel;
- public float expProgress;
- public int totalExperience;
-
- public MPDBPlayerData(UUID playerUUID, String playerName) {
- this.playerUUID = playerUUID;
- this.playerName = playerName;
- }
-}
diff --git a/common/src/main/java/net/william278/husksync/player/OnlineUser.java b/common/src/main/java/net/william278/husksync/player/OnlineUser.java
new file mode 100644
index 00000000..c401cefd
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/player/OnlineUser.java
@@ -0,0 +1,119 @@
+package net.william278.husksync.player;
+
+import de.themoep.minedown.MineDown;
+import net.william278.husksync.config.Settings;
+import net.william278.husksync.data.*;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.HashSet;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Represents a logged-in {@link User}
+ */
+public abstract class OnlineUser extends User {
+
+ public OnlineUser(@NotNull UUID uuid, @NotNull String username) {
+ super(uuid, username);
+ }
+
+ /**
+ * Get the player's {@link StatusData}
+ *
+ * @return the player's {@link StatusData}
+ */
+ public abstract CompletableFuture getStatus();
+
+ /**
+ * Get the player's inventory {@link InventoryData} contents
+ *
+ * @return The player's inventory {@link InventoryData} contents
+ */
+ public abstract CompletableFuture getInventory();
+
+ /**
+ * Get the player's ender chest {@link InventoryData} contents
+ *
+ * @return The player's ender chest {@link InventoryData} contents
+ */
+ public abstract CompletableFuture getEnderChest();
+
+ /**
+ * Get the player's {@link PotionEffectData}
+ *
+ * @return The player's {@link PotionEffectData}
+ */
+ public abstract CompletableFuture getPotionEffects();
+
+ /**
+ * Get the player's set of {@link AdvancementData}
+ *
+ * @return the player's set of {@link AdvancementData}
+ */
+ public abstract CompletableFuture> getAdvancements();
+
+ /**
+ * Get the player's {@link StatisticsData}
+ *
+ * @return The player's {@link StatisticsData}
+ */
+ public abstract CompletableFuture getStatistics();
+
+ /**
+ * Get the player's {@link LocationData}
+ *
+ * @return the player's {@link LocationData}
+ */
+ public abstract CompletableFuture getLocation();
+
+ /**
+ * Get the player's {@link PersistentDataContainerData}
+ *
+ * @return The player's {@link PersistentDataContainerData} when fetched
+ */
+ public abstract CompletableFuture getPersistentDataContainer();
+
+ /**
+ * Set {@link UserData} to a player
+ *
+ * @param data The data to set
+ * @param settings Plugin settings, for determining what needs setting
+ * @return a future that will be completed when done
+ */
+ public abstract CompletableFuture setData(@NotNull UserData data, @NotNull Settings settings);
+
+ /**
+ * Dispatch a MineDown-formatted message to this player
+ *
+ * @param mineDown the parsed {@link MineDown} to send
+ */
+ public abstract void sendMessage(@NotNull MineDown mineDown);
+
+ /**
+ * Dispatch a MineDown-formatted action bar message to this player
+ *
+ * @param mineDown the parsed {@link MineDown} to send
+ */
+ public abstract void sendActionBar(@NotNull MineDown mineDown);
+
+ /**
+ * Returns if the player has the permission node
+ *
+ * @param node The permission node string
+ * @return {@code true} if the player has permission node; {@code false} otherwise
+ */
+ public abstract boolean hasPermission(@NotNull String node);
+
+ /**
+ * Get the player's current {@link UserData}
+ *
+ * @return the player's current {@link UserData}
+ */
+ public final CompletableFuture getUserData() {
+ return CompletableFuture.supplyAsync(() -> new UserData(getStatus().join(), getInventory().join(),
+ getEnderChest().join(), getPotionEffects().join(), getAdvancements().join(),
+ getStatistics().join(), getLocation().join(), getPersistentDataContainer().join()));
+ }
+
+}
diff --git a/common/src/main/java/net/william278/husksync/player/User.java b/common/src/main/java/net/william278/husksync/player/User.java
new file mode 100644
index 00000000..77e287cf
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/player/User.java
@@ -0,0 +1,28 @@
+package net.william278.husksync.player;
+
+import com.google.gson.annotations.SerializedName;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public class User {
+
+ @SerializedName("username")
+ public String username;
+
+ @SerializedName("uuid")
+ public UUID uuid;
+
+ public User(@NotNull UUID uuid, @NotNull String username) {
+ this.username = username;
+ this.uuid = uuid;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof User other) {
+ return this.uuid.equals(other.uuid);
+ }
+ return super.equals(obj);
+ }
+}
diff --git a/common/src/main/java/net/william278/husksync/proxy/command/HuskSyncCommand.java b/common/src/main/java/net/william278/husksync/proxy/command/HuskSyncCommand.java
deleted file mode 100644
index 65da55a7..00000000
--- a/common/src/main/java/net/william278/husksync/proxy/command/HuskSyncCommand.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package net.william278.husksync.proxy.command;
-
-public interface HuskSyncCommand {
-
- SubCommand[] SUB_COMMANDS = {new SubCommand("about", null),
- new SubCommand("status", "husksync.command.admin"),
- new SubCommand("reload", "husksync.command.admin"),
- new SubCommand("update", "husksync.command.admin"),
- new SubCommand("invsee", "husksync.command.inventory"),
- new SubCommand("echest", "husksync.command.ender_chest")};
-
- /**
- * A sub command, that may require a permission
- */
- record SubCommand(String command, String permission) { }
-
-}
diff --git a/common/src/main/java/net/william278/husksync/proxy/data/DataManager.java b/common/src/main/java/net/william278/husksync/proxy/data/DataManager.java
deleted file mode 100644
index e51df57c..00000000
--- a/common/src/main/java/net/william278/husksync/proxy/data/DataManager.java
+++ /dev/null
@@ -1,372 +0,0 @@
-package net.william278.husksync.proxy.data;
-
-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.util.*;
-import java.util.logging.Level;
-
-public class DataManager {
-
- /**
- * The player data cache for each cluster ID
- */
- public HashMap playerDataCache = new HashMap<>();
-
- /**
- * Map of the database assigned for each cluster
- */
- private final HashMap clusterDatabases;
-
- // Retrieve database connection for a cluster
- public Connection getConnection(String clusterId) throws SQLException {
- return clusterDatabases.get(clusterId).getConnection();
- }
-
- // Console logger for errors
- private final Logger logger;
-
- // Plugin data folder
- private final File dataFolder;
-
- // Flag variable identifying if the data manager failed to initialize
- public boolean hasFailedInitialization = false;
-
- public DataManager(Logger logger, File dataFolder) {
- this.logger = logger;
- this.dataFolder = dataFolder;
- clusterDatabases = new HashMap<>();
- initializeDatabases();
- }
-
- private void initializeDatabases() {
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- Database clusterDatabase = switch (Settings.dataStorageType) {
- case SQLITE -> new SQLite(cluster, dataFolder, logger);
- case MYSQL -> new MySQL(cluster, logger);
- };
- clusterDatabase.load();
- clusterDatabase.createTables();
- clusterDatabases.put(cluster.clusterId(), clusterDatabase);
- }
-
- // Abort loading if the database failed to initialize
- for (Database database : clusterDatabases.values()) {
- if (database.isInactive()) {
- hasFailedInitialization = true;
- return;
- }
- }
- }
-
- /**
- * Close the database connections
- */
- public void closeDatabases() {
- for (Database database : clusterDatabases.values()) {
- database.close();
- }
- }
-
- /**
- * Checks if the player is registered on the database.
- * If not, register them to the database
- * If they are, ensure that their player name is up-to-date on the database
- *
- * @param playerUUID The UUID of the player to register
- */
- public void ensurePlayerExists(UUID playerUUID, String playerName) {
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- if (!playerExists(playerUUID, cluster)) {
- createPlayerEntry(playerUUID, playerName, cluster);
- } else {
- updatePlayerName(playerUUID, playerName, cluster);
- }
- }
- }
-
- /**
- * Returns whether the player is registered in SQL (an entry in the PLAYER_TABLE)
- *
- * @param playerUUID The UUID of the player
- * @return {@code true} if the player is on the player table
- */
- private boolean playerExists(UUID playerUUID, Settings.SynchronisationCluster cluster) {
- try (Connection connection = getConnection(cluster.clusterId())) {
- try (PreparedStatement statement = connection.prepareStatement(
- "SELECT * FROM " + cluster.playerTableName() + " WHERE `uuid`=?;")) {
- statement.setString(1, playerUUID.toString());
- ResultSet resultSet = statement.executeQuery();
- return resultSet.next();
- }
- } catch (SQLException e) {
- logger.log(Level.SEVERE, "An SQL exception occurred", e);
- return false;
- }
- }
-
- private void createPlayerEntry(UUID playerUUID, String playerName, Settings.SynchronisationCluster cluster) {
- try (Connection connection = getConnection(cluster.clusterId())) {
- try (PreparedStatement statement = connection.prepareStatement(
- "INSERT INTO " + cluster.playerTableName() + " (`uuid`,`username`) VALUES(?,?);")) {
- statement.setString(1, playerUUID.toString());
- statement.setString(2, playerName);
- statement.executeUpdate();
- }
- } catch (SQLException e) {
- logger.log(Level.SEVERE, "An SQL exception occurred", e);
- }
- }
-
- public void updatePlayerName(UUID playerUUID, String playerName, Settings.SynchronisationCluster cluster) {
- try (Connection connection = getConnection(cluster.clusterId())) {
- try (PreparedStatement statement = connection.prepareStatement(
- "UPDATE " + cluster.playerTableName() + " SET `username`=? WHERE `uuid`=?;")) {
- statement.setString(1, playerName);
- statement.setString(2, playerUUID.toString());
- statement.executeUpdate();
- }
- } catch (SQLException e) {
- logger.log(Level.SEVERE, "An SQL exception occurred", e);
- }
- }
-
- /**
- * Returns a player's PlayerData by their username
- *
- * @param playerName The PlayerName of the data to get
- * @return Their {@link PlayerData}; or {@code null} if the player does not exist
- */
- public PlayerData getPlayerDataByName(String playerName, String clusterId) {
- PlayerData playerData = null;
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- if (cluster.clusterId().equals(clusterId)) {
- try (Connection connection = getConnection(clusterId)) {
- try (PreparedStatement statement = connection.prepareStatement(
- "SELECT * FROM " + cluster.playerTableName() + " WHERE `username`=? LIMIT 1;")) {
- statement.setString(1, playerName);
- ResultSet resultSet = statement.executeQuery();
- if (resultSet.next()) {
- final UUID uuid = UUID.fromString(resultSet.getString("uuid"));
-
- // Get the player data from the cache if it's there, otherwise pull from SQL
- playerData = playerDataCache.get(cluster).getPlayer(uuid);
- if (playerData == null) {
- playerData = Objects.requireNonNull(getPlayerData(uuid)).get(cluster);
- }
- break;
-
- }
- }
- } catch (SQLException e) {
- logger.log(Level.SEVERE, "An SQL exception occurred", e);
- }
- break;
- }
-
- }
- return playerData;
- }
-
- public Map getPlayerData(UUID playerUUID) {
- HashMap data = new HashMap<>();
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- try (Connection connection = getConnection(cluster.clusterId())) {
- try (PreparedStatement statement = connection.prepareStatement(
- "SELECT * FROM " + cluster.dataTableName() + " WHERE `player_id`=(SELECT `id` FROM " + cluster.playerTableName() + " WHERE `uuid`=?);")) {
- statement.setString(1, playerUUID.toString());
- ResultSet resultSet = statement.executeQuery();
- if (resultSet.next()) {
- final UUID dataVersionUUID = UUID.fromString(resultSet.getString("version_uuid"));
- 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");
- final double maxHealth = resultSet.getDouble("max_health");
- final double healthScale = resultSet.getDouble("health_scale");
- final int hunger = resultSet.getInt("hunger");
- final float saturation = resultSet.getFloat("saturation");
- final float saturationExhaustion = resultSet.getFloat("saturation_exhaustion");
- final int selectedSlot = resultSet.getInt("selected_slot");
- final String serializedStatusEffects = resultSet.getString("status_effects");
- final int totalExperience = resultSet.getInt("total_experience");
- final int expLevel = resultSet.getInt("exp_level");
- final float expProgress = resultSet.getFloat("exp_progress");
- final String gameMode = resultSet.getString("game_mode");
- final boolean isFlying = resultSet.getBoolean("is_flying");
- final String serializedAdvancementData = resultSet.getString("advancements");
- final String serializedLocationData = resultSet.getString("location");
- final String serializedStatisticData = resultSet.getString("statistics");
-
- 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));
- }
- }
- } catch (SQLException e) {
- logger.log(Level.SEVERE, "An SQL exception occurred", e);
- return null;
- }
- }
- return data;
- }
-
- public void updatePlayerData(PlayerData playerData, Settings.SynchronisationCluster cluster) {
- // Ignore if the Spigot server didn't properly sync the previous data
-
- // Add the new player data to the cache
- playerDataCache.get(cluster).updatePlayer(playerData);
-
- // SQL: If the player has cached data, update it, otherwise insert new data.
- if (playerHasCachedData(playerData.getPlayerUUID(), cluster)) {
- updatePlayerSQLData(playerData, cluster);
- } else {
- insertPlayerData(playerData, cluster);
- }
- }
-
- private void updatePlayerSQLData(PlayerData playerData, Settings.SynchronisationCluster cluster) {
- try (Connection connection = getConnection(cluster.clusterId())) {
- 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(System.currentTimeMillis()));
- statement.setString(3, playerData.getSerializedInventory());
- statement.setString(4, playerData.getSerializedEnderChest());
- statement.setDouble(5, playerData.getHealth()); // Health
- statement.setDouble(6, playerData.getMaxHealth()); // Max health
- statement.setDouble(7, playerData.getHealthScale()); // Health scale
- statement.setInt(8, playerData.getHunger()); // Hunger
- statement.setFloat(9, playerData.getSaturation()); // Saturation
- statement.setFloat(10, playerData.getSaturationExhaustion()); // Saturation exhaustion
- statement.setInt(11, playerData.getSelectedSlot()); // Current selected slot
- statement.setString(12, playerData.getSerializedEffectData()); // Status effects
- statement.setInt(13, playerData.getTotalExperience()); // Total Experience
- statement.setInt(14, playerData.getExpLevel()); // Exp level
- statement.setFloat(15, playerData.getExpProgress()); // Exp progress
- statement.setString(16, playerData.getGameMode()); // GameMode
- statement.setString(17, playerData.getSerializedStatistics()); // Statistics
- statement.setBoolean(18, playerData.isFlying()); // Is flying
- statement.setString(19, playerData.getSerializedAdvancements()); // Advancements
- statement.setString(20, playerData.getSerializedLocation()); // Location
-
- statement.setString(21, playerData.getPlayerUUID().toString());
- statement.executeUpdate();
- }
- } catch (SQLException e) {
- logger.log(Level.SEVERE, "An SQL exception occurred", e);
- }
- }
-
- private void insertPlayerData(PlayerData playerData, Settings.SynchronisationCluster cluster) {
- try (Connection connection = getConnection(cluster.clusterId())) {
- try (PreparedStatement statement = connection.prepareStatement(
- "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(System.currentTimeMillis()));
- statement.setString(4, playerData.getSerializedInventory());
- statement.setString(5, playerData.getSerializedEnderChest());
- statement.setDouble(6, playerData.getHealth()); // Health
- statement.setDouble(7, playerData.getMaxHealth()); // Max health
- statement.setDouble(8, playerData.getHealthScale()); // Health scale
- statement.setInt(9, playerData.getHunger()); // Hunger
- statement.setFloat(10, playerData.getSaturation()); // Saturation
- statement.setFloat(11, playerData.getSaturationExhaustion()); // Saturation exhaustion
- statement.setInt(12, playerData.getSelectedSlot()); // Current selected slot
- statement.setString(13, playerData.getSerializedEffectData()); // Status effects
- statement.setInt(14, playerData.getTotalExperience()); // Total Experience
- statement.setInt(15, playerData.getExpLevel()); // Exp level
- statement.setFloat(16, playerData.getExpProgress()); // Exp progress
- statement.setString(17, playerData.getGameMode()); // GameMode
- statement.setString(18, playerData.getSerializedStatistics()); // Statistics
- statement.setBoolean(19, playerData.isFlying()); // Is flying
- statement.setString(20, playerData.getSerializedAdvancements()); // Advancements
- statement.setString(21, playerData.getSerializedLocation()); // Location
-
- statement.executeUpdate();
- }
- } catch (SQLException e) {
- logger.log(Level.SEVERE, "An SQL exception occurred", e);
- }
- }
-
- /**
- * Returns whether the player has cached data saved in SQL (an entry in the DATA_TABLE)
- *
- * @param playerUUID The UUID of the player
- * @return {@code true} if the player has an entry in the data table
- */
- private boolean playerHasCachedData(UUID playerUUID, Settings.SynchronisationCluster cluster) {
- try (Connection connection = getConnection(cluster.clusterId())) {
- try (PreparedStatement statement = connection.prepareStatement(
- "SELECT * FROM " + cluster.dataTableName() + " WHERE `player_id`=(SELECT `id` FROM " + cluster.playerTableName() + " WHERE `uuid`=?);")) {
- statement.setString(1, playerUUID.toString());
- ResultSet resultSet = statement.executeQuery();
- return resultSet.next();
- }
- } catch (SQLException e) {
- logger.log(Level.SEVERE, "An SQL exception occurred", e);
- return false;
- }
- }
-
- /**
- * A cache of PlayerData
- */
- public static class PlayerDataCache {
- // The cached player data
- public HashSet 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().equals(newData.getPlayerUUID())) {
- oldData = data;
- break;
- }
- }
- 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().equals(playerUUID)) {
- return data;
- }
- }
- return null;
- }
-
- }
-}
diff --git a/common/src/main/java/net/william278/husksync/proxy/data/sql/Database.java b/common/src/main/java/net/william278/husksync/proxy/data/sql/Database.java
deleted file mode 100644
index 5b237011..00000000
--- a/common/src/main/java/net/william278/husksync/proxy/data/sql/Database.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package net.william278.husksync.proxy.data.sql;
-
-import net.william278.husksync.Settings;
-import net.william278.husksync.util.Logger;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-
-public abstract class Database {
-
- public String dataPoolName;
- public Settings.SynchronisationCluster cluster;
- public final Logger logger;
-
- public Database(Settings.SynchronisationCluster cluster, Logger logger) {
- this.cluster = cluster;
- this.dataPoolName = cluster != null ? "HuskSyncHikariPool-" + cluster.clusterId() : "HuskSyncMigratorPool";
- this.logger = logger;
- }
-
- public abstract Connection getConnection() throws SQLException;
-
- public boolean isInactive() {
- try {
- return getConnection() == null;
- } catch (SQLException e) {
- return true;
- }
- }
-
- public abstract void load();
-
- public abstract void createTables();
-
- public abstract void close();
-
- public final int hikariMaximumPoolSize = Settings.hikariMaximumPoolSize;
- public final int hikariMinimumIdle = Settings.hikariMinimumIdle;
- public final long hikariMaximumLifetime = Settings.hikariMaximumLifetime;
- public final long hikariKeepAliveTime = Settings.hikariKeepAliveTime;
- public final long hikariConnectionTimeOut = Settings.hikariConnectionTimeOut;
-}
diff --git a/common/src/main/java/net/william278/husksync/proxy/data/sql/MySQL.java b/common/src/main/java/net/william278/husksync/proxy/data/sql/MySQL.java
deleted file mode 100644
index 0d3571de..00000000
--- a/common/src/main/java/net/william278/husksync/proxy/data/sql/MySQL.java
+++ /dev/null
@@ -1,113 +0,0 @@
-package net.william278.husksync.proxy.data.sql;
-
-import com.zaxxer.hikari.HikariDataSource;
-import net.william278.husksync.Settings;
-import net.william278.husksync.util.Logger;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.logging.Level;
-
-public class MySQL extends Database {
-
- final String[] SQL_SETUP_STATEMENTS = {
- "CREATE TABLE IF NOT EXISTS " + cluster.playerTableName() + " (" +
- "`id` integer NOT NULL AUTO_INCREMENT," +
- "`uuid` char(36) NOT NULL UNIQUE," +
- "`username` varchar(16) NOT NULL," +
-
- "PRIMARY KEY (`id`)" +
- ");",
-
- "CREATE TABLE IF NOT EXISTS " + cluster.dataTableName() + " (" +
- "`player_id` integer NOT NULL," +
- "`version_uuid` char(36) NOT NULL UNIQUE," +
- "`timestamp` datetime NOT NULL," +
- "`inventory` longtext NOT NULL," +
- "`ender_chest` longtext NOT NULL," +
- "`health` double NOT NULL," +
- "`max_health` double NOT NULL," +
- "`health_scale` double NOT NULL," +
- "`hunger` integer NOT NULL," +
- "`saturation` float NOT NULL," +
- "`saturation_exhaustion` float NOT NULL," +
- "`selected_slot` integer NOT NULL," +
- "`status_effects` longtext NOT NULL," +
- "`total_experience` integer NOT NULL," +
- "`exp_level` integer NOT NULL," +
- "`exp_progress` float NOT NULL," +
- "`game_mode` tinytext NOT NULL," +
- "`statistics` longtext NOT NULL," +
- "`is_flying` boolean NOT NULL," +
- "`advancements` longtext NOT NULL," +
- "`location` text NOT NULL," +
-
- "PRIMARY KEY (`player_id`,`version_uuid`)," +
- "FOREIGN KEY (`player_id`) REFERENCES " + cluster.playerTableName() + " (`id`)" +
- ");"
-
- };
-
- public String host = Settings.mySQLHost;
- public int port = Settings.mySQLPort;
- public String database = Settings.mySQLDatabase;
- public String username = Settings.mySQLUsername;
- public String password = Settings.mySQLPassword;
- public String params = Settings.mySQLParams;
-
- private HikariDataSource dataSource;
-
- public MySQL(Settings.SynchronisationCluster cluster, Logger logger) {
- super(cluster, logger);
- }
-
- @Override
- public Connection getConnection() throws SQLException {
- return dataSource.getConnection();
- }
-
- @Override
- public void load() {
- // Create new HikariCP data source
- final String jdbcUrl = "jdbc:mysql://" + host + ":" + port + "/" + database + params;
- dataSource = new HikariDataSource();
- dataSource.setJdbcUrl(jdbcUrl);
-
- dataSource.setUsername(username);
- dataSource.setPassword(password);
-
- // Set data source driver path
- dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
-
- // Set various additional parameters
- dataSource.setMaximumPoolSize(hikariMaximumPoolSize);
- dataSource.setMinimumIdle(hikariMinimumIdle);
- dataSource.setMaxLifetime(hikariMaximumLifetime);
- dataSource.setKeepaliveTime(hikariKeepAliveTime);
- dataSource.setConnectionTimeout(hikariConnectionTimeOut);
- dataSource.setPoolName(dataPoolName);
- }
-
- @Override
- public void createTables() {
- // Create tables
- try (Connection connection = dataSource.getConnection()) {
- try (Statement statement = connection.createStatement()) {
- for (String tableCreationStatement : SQL_SETUP_STATEMENTS) {
- statement.execute(tableCreationStatement);
- }
- }
- } catch (SQLException e) {
- logger.log(Level.SEVERE, "An error occurred creating tables on the MySQL database: ", e);
- }
- }
-
- @Override
- public void close() {
- if (dataSource != null) {
- dataSource.close();
- }
- }
-
-}
diff --git a/common/src/main/java/net/william278/husksync/proxy/data/sql/SQLite.java b/common/src/main/java/net/william278/husksync/proxy/data/sql/SQLite.java
deleted file mode 100644
index ee0715c6..00000000
--- a/common/src/main/java/net/william278/husksync/proxy/data/sql/SQLite.java
+++ /dev/null
@@ -1,126 +0,0 @@
-package net.william278.husksync.proxy.data.sql;
-
-import com.zaxxer.hikari.HikariDataSource;
-import net.william278.husksync.Settings;
-import net.william278.husksync.util.Logger;
-
-import java.io.File;
-import java.io.IOException;
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.logging.Level;
-
-public class SQLite extends Database {
-
- final String[] SQL_SETUP_STATEMENTS = {
- "PRAGMA foreign_keys = ON;",
- "PRAGMA encoding = 'UTF-8';",
-
- "CREATE TABLE IF NOT EXISTS " + cluster.playerTableName() + " (" +
- "`id` integer PRIMARY KEY," +
- "`uuid` char(36) NOT NULL UNIQUE," +
- "`username` varchar(16) NOT NULL" +
- ");",
-
- "CREATE TABLE IF NOT EXISTS " + cluster.dataTableName() + " (" +
- "`player_id` integer NOT NULL REFERENCES " + cluster.playerTableName() + "(`id`)," +
- "`version_uuid` char(36) NOT NULL UNIQUE," +
- "`timestamp` datetime NOT NULL," +
- "`inventory` longtext NOT NULL," +
- "`ender_chest` longtext NOT NULL," +
- "`health` double NOT NULL," +
- "`max_health` double NOT NULL," +
- "`health_scale` double NOT NULL," +
- "`hunger` integer NOT NULL," +
- "`saturation` float NOT NULL," +
- "`saturation_exhaustion` float NOT NULL," +
- "`selected_slot` integer NOT NULL," +
- "`status_effects` longtext NOT NULL," +
- "`total_experience` integer NOT NULL," +
- "`exp_level` integer NOT NULL," +
- "`exp_progress` float NOT NULL," +
- "`game_mode` tinytext NOT NULL," +
- "`statistics` longtext NOT NULL," +
- "`is_flying` boolean NOT NULL," +
- "`advancements` longtext NOT NULL," +
- "`location` text NOT NULL," +
-
- "PRIMARY KEY (`player_id`,`version_uuid`)" +
- ");"
- };
-
- private String getDatabaseName() {
- return cluster.databaseName() + "Data";
- }
-
- private final File dataFolder;
-
- private HikariDataSource dataSource;
-
- public SQLite(Settings.SynchronisationCluster cluster, File dataFolder, Logger logger) {
- super(cluster, logger);
- this.dataFolder = dataFolder;
- }
-
- // Create the database file if it does not exist yet
- private void createDatabaseFileIfNotExist() {
- File databaseFile = new File(dataFolder, getDatabaseName() + ".db");
- if (!databaseFile.exists()) {
- try {
- if (!databaseFile.createNewFile()) {
- logger.log(Level.SEVERE, "Failed to write new file: " + getDatabaseName() + ".db (file already exists)");
- }
- } catch (IOException e) {
- logger.log(Level.SEVERE, "An error occurred writing a file: " + getDatabaseName() + ".db (" + e.getCause() + ")", e);
- }
- }
- }
-
- @Override
- public Connection getConnection() throws SQLException {
- return dataSource.getConnection();
- }
-
- @Override
- public void load() {
- // Make SQLite database file
- createDatabaseFileIfNotExist();
-
- // Create new HikariCP data source
- final String jdbcUrl = "jdbc:sqlite:" + dataFolder.getAbsolutePath() + File.separator + getDatabaseName() + ".db";
- dataSource = new HikariDataSource();
- dataSource.setDataSourceClassName("org.sqlite.SQLiteDataSource");
- dataSource.addDataSourceProperty("url", jdbcUrl);
-
- // Set various additional parameters
- dataSource.setMaximumPoolSize(hikariMaximumPoolSize);
- dataSource.setMinimumIdle(hikariMinimumIdle);
- dataSource.setMaxLifetime(hikariMaximumLifetime);
- dataSource.setKeepaliveTime(hikariKeepAliveTime);
- dataSource.setConnectionTimeout(hikariConnectionTimeOut);
- dataSource.setPoolName(dataPoolName);
- }
-
- @Override
- public void createTables() {
- // Create tables
- try (Connection connection = dataSource.getConnection()) {
- try (Statement statement = connection.createStatement()) {
- for (String tableCreationStatement : SQL_SETUP_STATEMENTS) {
- statement.execute(tableCreationStatement);
- }
- }
- } catch (SQLException e) {
- logger.log(Level.SEVERE, "An error occurred creating tables on the SQLite database", e);
- }
- }
-
- @Override
- public void close() {
- if (dataSource != null) {
- dataSource.close();
- }
- }
-
-}
diff --git a/common/src/main/java/net/william278/husksync/redis/RedisListener.java b/common/src/main/java/net/william278/husksync/redis/RedisListener.java
deleted file mode 100644
index a3d83c7d..00000000
--- a/common/src/main/java/net/william278/husksync/redis/RedisListener.java
+++ /dev/null
@@ -1,126 +0,0 @@
-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();
- }
-}
diff --git a/common/src/main/java/net/william278/husksync/redis/RedisManager.java b/common/src/main/java/net/william278/husksync/redis/RedisManager.java
new file mode 100644
index 00000000..9c054fb2
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/redis/RedisManager.java
@@ -0,0 +1,84 @@
+package net.william278.husksync.redis;
+
+import net.william278.husksync.config.Settings;
+import net.william278.husksync.data.UserData;
+import net.william278.husksync.player.User;
+import org.jetbrains.annotations.NotNull;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+public class RedisManager {
+
+ private static final String KEY_NAMESPACE = "husksync:";
+ private static String clusterId = "";
+ private final JedisPool jedisPool;
+
+ private RedisManager(@NotNull Settings settings) {
+ clusterId = settings.getStringValue(Settings.ConfigOption.CLUSTER_ID);
+ JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
+ jedisPoolConfig.setMaxIdle(0);
+ jedisPoolConfig.setTestOnBorrow(true);
+ jedisPoolConfig.setTestOnReturn(true);
+ if (settings.getStringValue(Settings.ConfigOption.REDIS_PASSWORD).isBlank()) {
+ jedisPool = new JedisPool(jedisPoolConfig,
+ settings.getStringValue(Settings.ConfigOption.REDIS_HOST),
+ settings.getIntegerValue(Settings.ConfigOption.REDIS_PORT),
+ 0,
+ settings.getBooleanValue(Settings.ConfigOption.REDIS_USE_SSL));
+ } else {
+ jedisPool = new JedisPool(jedisPoolConfig,
+ settings.getStringValue(Settings.ConfigOption.REDIS_HOST),
+ settings.getIntegerValue(Settings.ConfigOption.REDIS_PORT),
+ 0,
+ settings.getStringValue(Settings.ConfigOption.REDIS_PASSWORD),
+ settings.getBooleanValue(Settings.ConfigOption.REDIS_USE_SSL));
+ }
+ }
+
+ public CompletableFuture setPlayerData(@NotNull User user, @NotNull UserData userData,
+ @NotNull RedisKeyType redisKeyType) {
+ return CompletableFuture.runAsync(() -> {
+ try (Jedis jedis = jedisPool.getResource()) {
+ jedis.setex(redisKeyType.getKeyPrefix() + user.uuid.toString(),
+ redisKeyType.timeToLive, userData.toJson());
+ }
+ });
+ }
+
+ public CompletableFuture> getUserData(@NotNull User user, @NotNull RedisKeyType redisKeyType) {
+ return CompletableFuture.supplyAsync(() -> {
+ try (Jedis jedis = jedisPool.getResource()) {
+ final String json = jedis.get(redisKeyType.getKeyPrefix() + user.uuid.toString());
+ if (json == null) {
+ return Optional.empty();
+ }
+ return Optional.of(UserData.fromJson(json));
+ }
+ });
+ }
+
+ public static CompletableFuture initialize(@NotNull Settings settings) {
+ return CompletableFuture.supplyAsync(() -> new RedisManager(settings));
+ }
+
+ public enum RedisKeyType {
+ CACHE(60 * 60 * 24),
+ SERVER_CHANGE(2);
+
+ public final int timeToLive;
+
+ RedisKeyType(int timeToLive) {
+ this.timeToLive = timeToLive;
+ }
+
+ @NotNull
+ public String getKeyPrefix() {
+ return KEY_NAMESPACE.toLowerCase() + ":" + clusterId.toLowerCase() + ":" + name().toLowerCase() + ":";
+ }
+ }
+
+}
diff --git a/common/src/main/java/net/william278/husksync/redis/RedisMessage.java b/common/src/main/java/net/william278/husksync/redis/RedisMessage.java
deleted file mode 100644
index b9b0f360..00000000
--- a/common/src/main/java/net/william278/husksync/redis/RedisMessage.java
+++ /dev/null
@@ -1,200 +0,0 @@
-package net.william278.husksync.redis;
-
-import net.william278.husksync.PlayerData;
-import net.william278.husksync.Settings;
-import redis.clients.jedis.Jedis;
-
-import java.io.*;
-import java.util.Base64;
-import java.util.StringJoiner;
-import java.util.UUID;
-
-public class RedisMessage {
-
- public static String REDIS_CHANNEL = "HuskSync";
-
- public static String MESSAGE_META_SEPARATOR = "♦";
- public static String MESSAGE_DATA_SEPARATOR = "♣";
-
- private final String messageData;
- private final MessageType messageType;
- private final 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) throws IOException, ClassNotFoundException {
- String[] messageMetaElements = messageString.split(MESSAGE_META_SEPARATOR);
- messageType = MessageType.valueOf(messageMetaElements[0]);
- messageTarget = (MessageTarget) RedisMessage.deserialize(messageMetaElements[1]);
- messageData = messageMetaElements[2];
- }
-
- /**
- * Returns the full, formatted message string with type, target & data
- *
- * @return The fully formatted message
- */
- private String getFullMessage() throws IOException {
- return new StringJoiner(MESSAGE_META_SEPARATOR)
- .add(messageType.toString()).add(RedisMessage.serialize(messageTarget)).add(messageData)
- .toString();
- }
-
- /**
- * Send the redis message
- */
- public void send() throws IOException {
- 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 MessageType getMessageType() {
- return messageType;
- }
-
- public MessageTarget getMessageTarget() {
- return messageTarget;
- }
-
- /**
- * Defines the type of the message
- */
- public enum MessageType implements Serializable {
- /**
- * Sent by Bukkit servers to proxy when a user disconnects with that player's updated {@link PlayerData}.
- */
- PLAYER_DATA_UPDATE,
-
- /**
- * Sent by Bukkit servers to proxy to request {@link PlayerData} from the proxy if they are set as needing to request data on join.
- */
- PLAYER_DATA_REQUEST,
-
- /**
- * Sent by the Proxy to reply to a {@code MessageType.PLAYER_DATA_REQUEST}, contains the latest {@link PlayerData} for the requester.
- */
- PLAYER_DATA_SET,
-
- /**
- * Sent by Bukkit servers to proxy to request {@link PlayerData} from the proxy via the API.
- */
- API_DATA_REQUEST,
-
- /**
- * Sent by the Proxy to fulfill an {@code MessageType.API_DATA_REQUEST}, containing the latest {@link PlayerData} for the requested UUID.
- */
- API_DATA_RETURN,
-
- /**
- * Sent by the Proxy to cancel an {@code MessageType.API_DATA_REQUEST} if no data can be returned.
- */
- API_DATA_CANCEL,
-
- /**
- * Sent by the proxy to a Bukkit server to have them request data on join; contains no data otherwise.
- */
- REQUEST_DATA_ON_JOIN,
-
- /**
- * Sent by the proxy to ask the Bukkit server to send the full plugin information, contains information about the proxy brand and version.
- */
- SEND_PLUGIN_INFORMATION,
-
- /**
- * Sent by the proxy to show a player the contents of another player's inventory, contains their username and {@link PlayerData}.
- */
- OPEN_INVENTORY,
-
- /**
- * Sent by the proxy to show a player the contents of another player's ender chest, contains their username and {@link PlayerData}.
- */
- OPEN_ENDER_CHEST,
-
- /**
- * Sent by both the proxy and bukkit servers to confirm cross-server communication has been established.
- */
- CONNECTION_HANDSHAKE,
-
- /**
- * Sent by both the proxy and bukkit servers to terminate communications (if a bukkit / the proxy goes offline).
- */
- TERMINATE_HANDSHAKE,
-
- /**
- * Sent by a proxy to a bukkit server to decode MPDB data.
- */
- DECODE_MPDB_DATA,
-
- /**
- * Sent by a bukkit server back to the proxy with the correctly decoded MPDB data.
- */
- DECODED_MPDB_DATA_SET,
-
- /**
- * Sent by the proxy to a bukkit server to initiate a reload.
- */
- RELOAD_CONFIG
- }
-
- public enum RequestOnJoinUpdateType {
- ADD_REQUESTER,
- REMOVE_REQUESTER
- }
-
- /**
- * 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 {
- }
-
- /**
- * Deserialize an object from a Base64 string
- */
- public static Object deserialize(String s) throws IOException, ClassNotFoundException {
- byte[] data = Base64.getDecoder().decode(s);
- try (ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(data))) {
- return objectInputStream.readObject();
- }
- }
-
- /**
- * Serialize an object to a Base64 string
- */
- public static String serialize(Serializable o) throws IOException {
- ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
- try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {
- objectOutputStream.writeObject(o);
- }
- return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
- }
-}
\ No newline at end of file
diff --git a/common/src/main/java/net/william278/husksync/util/Logger.java b/common/src/main/java/net/william278/husksync/util/Logger.java
index 3e076f65..2d663f4f 100644
--- a/common/src/main/java/net/william278/husksync/util/Logger.java
+++ b/common/src/main/java/net/william278/husksync/util/Logger.java
@@ -3,7 +3,7 @@ package net.william278.husksync.util;
import java.util.logging.Level;
/**
- * Logger interface to allow for implementation of different logger platforms used by Bungee and Velocity
+ * An abstract, cross-platform representation of a logger
*/
public interface Logger {
@@ -16,4 +16,5 @@ public interface Logger {
void severe(String message);
void config(String message);
-}
\ No newline at end of file
+
+}
diff --git a/common/src/main/java/net/william278/husksync/util/MessageManager.java b/common/src/main/java/net/william278/husksync/util/MessageManager.java
deleted file mode 100644
index ef953659..00000000
--- a/common/src/main/java/net/william278/husksync/util/MessageManager.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package net.william278.husksync.util;
-
-import java.util.HashMap;
-
-public class MessageManager {
-
- private static HashMap messages = new HashMap<>();
-
- public static void setMessages(HashMap newMessages) {
- messages = new HashMap<>(newMessages);
- }
-
- public static String getMessage(String messageId) {
- return messages.get(messageId);
- }
-
- 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 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)\n")
- .append("[• Cached player data:](white) [%2%](#00fb9a)");
-
-}
\ No newline at end of file
diff --git a/common/src/main/java/net/william278/husksync/util/ResourceReader.java b/common/src/main/java/net/william278/husksync/util/ResourceReader.java
new file mode 100644
index 00000000..7acbe70a
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/util/ResourceReader.java
@@ -0,0 +1,28 @@
+package net.william278.husksync.util;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.InputStream;
+
+/**
+ * Abstract representation of a reader that reads internal resource files by name
+ */
+public interface ResourceReader {
+
+ /**
+ * Gets the resource with given filename and reads it as an {@link InputStream}
+ *
+ * @param fileName Name of the resource file to read
+ * @return The resource, read as an {@link InputStream}
+ */
+ @NotNull InputStream getResource(String fileName);
+
+ /**
+ * Gets the plugin data folder where plugin configuration and data are kept
+ *
+ * @return the plugin data directory
+ */
+ @NotNull File getDataFolder();
+
+}
diff --git a/common/src/main/java/net/william278/husksync/util/ThrowSupplier.java b/common/src/main/java/net/william278/husksync/util/ThrowSupplier.java
deleted file mode 100644
index 5825b2de..00000000
--- a/common/src/main/java/net/william278/husksync/util/ThrowSupplier.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package net.william278.husksync.util;
-
-public interface ThrowSupplier {
- T get() throws Exception;
-
- static A get(ThrowSupplier supplier) {
- try {
- return supplier.get();
- } catch (Exception e) {
- throw new RuntimeException(e.getMessage(), e);
- }
- }
-}
diff --git a/common/src/main/java/net/william278/husksync/util/UpdateChecker.java b/common/src/main/java/net/william278/husksync/util/UpdateChecker.java
index 54ec946c..f7f488cf 100644
--- a/common/src/main/java/net/william278/husksync/util/UpdateChecker.java
+++ b/common/src/main/java/net/william278/husksync/util/UpdateChecker.java
@@ -1,53 +1,59 @@
package net.william278.husksync.util;
+import org.jetbrains.annotations.NotNull;
+
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
+import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
-public abstract class UpdateChecker {
+public class UpdateChecker {
private final static int SPIGOT_PROJECT_ID = 97144;
+ private final Logger logger;
private final VersionUtils.Version currentVersion;
- private VersionUtils.Version latestVersion;
- public UpdateChecker(String currentVersion) {
+ public UpdateChecker(@NotNull String currentVersion, @NotNull Logger logger) {
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 = 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 = new VersionUtils.Version();
- } catch (Exception e) {
- log(Level.WARNING, "Failed to check for updates: An exception occurred.");
- this.latestVersion = new VersionUtils.Version();
- }
+ this.logger = logger;
}
- public boolean isUpToDate() {
- return this.currentVersion.compareTo(latestVersion) >= 0;
+ public CompletableFuture fetchLatestVersion() {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ final URL url = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + SPIGOT_PROJECT_ID);
+ URLConnection urlConnection = url.openConnection();
+ return VersionUtils.Version.of(new BufferedReader(new InputStreamReader(urlConnection.getInputStream())).readLine());
+ } catch (Exception e) {
+ logger.log(Level.WARNING, "Failed to fetch the latest plugin version", e);
+ }
+ return new VersionUtils.Version();
+ });
}
- public String getLatestVersion() {
- return latestVersion.toString();
+ public boolean isUpdateAvailable(@NotNull VersionUtils.Version latestVersion) {
+ return latestVersion.compareTo(currentVersion) > 0;
}
- public String getCurrentVersion() {
- return currentVersion.toString();
+ public VersionUtils.Version getCurrentVersion() {
+ return currentVersion;
}
- public abstract void log(Level level, String message);
+ public CompletableFuture isUpToDate() {
+ return fetchLatestVersion().thenApply(this::isUpdateAvailable);
+ }
public void logToConsole() {
- if (!isUpToDate()) {
- log(Level.WARNING, "A new version of HuskSync is available: Version "
- + latestVersion + " (Currently running: " + currentVersion + ")");
- }
+ fetchLatestVersion().thenAccept(latestVersion -> {
+ if (isUpdateAvailable(latestVersion)) {
+ logger.log(Level.WARNING, "A new version of HuskSync is available: v" + latestVersion);
+ } else {
+ logger.log(Level.INFO, "HuskSync is up-to-date! (Running: v" + currentVersion + ")");
+ }
+ });
}
-}
+}
\ No newline at end of file
diff --git a/common/src/main/java/net/william278/husksync/util/VersionUtils.java b/common/src/main/java/net/william278/husksync/util/VersionUtils.java
index 1bbcd858..2081ed2c 100644
--- a/common/src/main/java/net/william278/husksync/util/VersionUtils.java
+++ b/common/src/main/java/net/william278/husksync/util/VersionUtils.java
@@ -58,4 +58,4 @@ public class VersionUtils {
}
}
-}
+}
\ No newline at end of file
diff --git a/common/src/main/resources/config.yml b/common/src/main/resources/config.yml
new file mode 100644
index 00000000..70c75274
--- /dev/null
+++ b/common/src/main/resources/config.yml
@@ -0,0 +1,51 @@
+# ------------------------------
+# | HuskSync Config |
+# | Developed by William278 |
+# ------------------------------
+# Documentation available at: https://william278.net/docs/husksync/Setup
+
+language: 'en-gb'
+check_for_updates: true
+cluster_id: ''
+
+database:
+ credentials:
+ host: 'localhost'
+ port: 3306
+ database: 'HuskSync'
+ username: 'root'
+ password: 'pa55w0rd'
+ params: '?autoReconnect=true&useSSL=false'
+ connection_pool:
+ maximum_pool_size: 10
+ minimum_idle: 10
+ maximum_lifetime: 1800000
+ keepalive_time: 0
+ connection_timeout: 5000
+ table_names:
+ players_table: 'husksync_players'
+ data_table: 'husksync_data'
+
+redis:
+ credentials:
+ host: 'localhost'
+ port: 6379
+ password: ''
+ use_ssl: false
+
+synchronization:
+ max_user_data_records: 5
+ save_on_world_save: true
+ features:
+ inventories: true
+ ender_chests: true
+ health: true
+ max_health: true
+ hunger: true
+ experience: true
+ potion_effects: true
+ advancements: true
+ game_mode: true
+ statistics: true
+ persistent_data_container: true
+ location: false
\ No newline at end of file
diff --git a/common/src/main/resources/database/mysql_scehma.sql b/common/src/main/resources/database/mysql_scehma.sql
new file mode 100644
index 00000000..f8936879
--- /dev/null
+++ b/common/src/main/resources/database/mysql_scehma.sql
@@ -0,0 +1,20 @@
+# Create the players table if it does not exist
+CREATE TABLE IF NOT EXISTS `%players_table%`
+(
+ `uuid` char(36) NOT NULL UNIQUE,
+ `username` varchar(16) NOT NULL,
+
+ PRIMARY KEY (`uuid`)
+);
+
+# Create the player data table if it does not exist
+CREATE TABLE IF NOT EXISTS `%data_table%`
+(
+ `version_uuid` char(36) NOT NULL,
+ `player_uuid` char(36) NOT NULL,
+ `timestamp` datetime NOT NULL,
+ `data` json NOT NULL,
+
+ PRIMARY KEY (`version_uuid`),
+ FOREIGN KEY (`player_uuid`) REFERENCES `%players_table%` (`uuid`) ON DELETE CASCADE
+);
\ No newline at end of file
diff --git a/common/src/main/resources/languages/de-de.yml b/common/src/main/resources/languages/de-de.yml
deleted file mode 100644
index 1813606b..00000000
--- a/common/src/main/resources/languages/de-de.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-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)'
diff --git a/common/src/main/resources/languages/es-es.yml b/common/src/main/resources/languages/es-es.yml
deleted file mode 100644
index fd644330..00000000
--- a/common/src/main/resources/languages/es-es.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-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)'
diff --git a/common/src/main/resources/languages/ja-jp.yml b/common/src/main/resources/languages/ja-jp.yml
deleted file mode 100644
index 370a1567..00000000
--- a/common/src/main/resources/languages/ja-jp.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-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)'
diff --git a/common/src/main/resources/languages/uk-ua.yml b/common/src/main/resources/languages/uk-ua.yml
deleted file mode 100644
index 58c5e838..00000000
--- a/common/src/main/resources/languages/uk-ua.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-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)'
\ No newline at end of file
diff --git a/common/src/main/resources/languages/zh-cn.yml b/common/src/main/resources/languages/zh-cn.yml
deleted file mode 100644
index 529f91aa..00000000
--- a/common/src/main/resources/languages/zh-cn.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-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)'
diff --git a/common/src/main/resources/languages/zh-tw.yml b/common/src/main/resources/languages/zh-tw.yml
deleted file mode 100644
index 0aa3afaa..00000000
--- a/common/src/main/resources/languages/zh-tw.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-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)'
\ No newline at end of file
diff --git a/common/src/main/resources/languages/en-gb.yml b/common/src/main/resources/locales/en-gb.yml
similarity index 100%
rename from common/src/main/resources/languages/en-gb.yml
rename to common/src/main/resources/locales/en-gb.yml
diff --git a/common/src/main/resources/proxy-config.yml b/common/src/main/resources/proxy-config.yml
deleted file mode 100644
index 6d442d22..00000000
--- a/common/src/main/resources/proxy-config.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-language: 'en-gb'
-redis_settings:
- host: 'localhost'
- port: 6379
- password: ''
- use_ssl: false
-data_storage_settings:
- database_type: 'sqlite'
- mysql_settings:
- host: 'localhost'
- port: 3306
- database: 'HuskSync'
- username: 'root'
- password: 'pa55w0rd'
- params: '?autoReconnect=true&useSSL=false'
- hikari_pool_settings:
- maximum_pool_size: 10
- minimum_idle: 10
- maximum_lifetime: 1800000
- keepalive_time: 0
- connection_timeout: 5000
-bounce_back_synchronization: true
-clusters:
- main:
- player_table: 'husksync_players'
- data_table: 'husksync_data'
-check_for_updates: true
-config_file_version: 1.2
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 86c87c62..5b3490b3 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,10 @@
+org.gradle.jvmargs='-Dfile.encoding=UTF-8'
+
+org.gradle.daemon=true
javaVersion=16
-plugin_version=1.4
-plugin_archive=husksync
\ No newline at end of file
+
+plugin_version=1.5
+plugin_archive=husksync
+
+jedis_version=4.2.3
+sqlite_driver_version=3.36.0.3
\ No newline at end of file
diff --git a/plugin/build.gradle b/plugin/build.gradle
index 32f4d6c4..021a17b0 100644
--- a/plugin/build.gradle
+++ b/plugin/build.gradle
@@ -1,20 +1,13 @@
-//file:noinspection GroovyAssignabilityCheck
plugins {
id 'maven-publish'
}
dependencies {
implementation project(path: ':bukkit', configuration: 'shadow')
- implementation project(path: ':api', configuration: 'shadow')
- implementation project(path: ':bungeecord', configuration: 'shadow')
- implementation project(path: ':velocity', configuration: 'shadow')
+ //implementation project(path: ':api', configuration: 'shadow')
}
shadowJar {
- dependencies {
- exclude dependency(':jedis')
- exclude dependency(':commons-pool2')
- }
}
publishing {
@@ -23,7 +16,6 @@ publishing {
groupId = 'net.william278'
artifactId = 'husksync-plugin'
version = "$rootProject.version"
-
artifact shadowJar
}
}
diff --git a/settings.gradle b/settings.gradle
index 80bab06e..914df6f3 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -8,7 +8,5 @@ rootProject.name = 'HuskSync'
include 'common'
include 'bukkit'
-include 'bungeecord'
-include 'velocity'
include 'api'
include 'plugin'
\ No newline at end of file
diff --git a/velocity/src/main/java/net/william278/husksync/HuskSyncVelocity.java b/velocity/src/main/java/net/william278/husksync/HuskSyncVelocity.java
deleted file mode 100644
index 56accde0..00000000
--- a/velocity/src/main/java/net/william278/husksync/HuskSyncVelocity.java
+++ /dev/null
@@ -1,220 +0,0 @@
-package net.william278.husksync;
-
-import com.google.inject.Inject;
-import com.velocitypowered.api.command.CommandManager;
-import com.velocitypowered.api.command.CommandMeta;
-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 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;
-import org.slf4j.Logger;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.HashSet;
-import java.util.Objects;
-import java.util.logging.Level;
-
-@Plugin(id = "husksync")
-public class HuskSyncVelocity {
-
- // Plugin version
- public static String VERSION = null;
-
- // Velocity bStats ID (different from Bukkit and BungeeCord)
- private static final int METRICS_ID = 13489;
- private final Metrics.Factory metricsFactory;
-
- private static HuskSyncVelocity instance;
-
- public static HuskSyncVelocity getInstance() {
- return instance;
- }
-
- // Whether the plugin is ready to accept redis messages
- public static boolean readyForRedis = false;
-
- // Whether the plugin is in the process of disabling and should skip responding to handshake confirmations
- public static boolean isDisabling = false;
-
- /**
- * Set of all the {@link Server}s that have completed the synchronisation handshake with HuskSync on the proxy
- */
- public static HashSet synchronisedServers;
-
- public static DataManager dataManager;
-
- public static VelocityRedisListener redisListener;
-
- public static MPDBMigrator mpdbMigrator;
-
- private final Logger logger;
- private final ProxyServer server;
- private final Path dataDirectory;
-
- // Get the data folder
- public File getDataFolder() {
- return dataDirectory.toFile();
- }
-
- // Get the proxy server
- public ProxyServer getProxyServer() {
- return server;
- }
-
- // Velocity logger handling
- private VelocityLogger velocityLogger;
-
- public VelocityLogger getVelocityLogger() {
- return velocityLogger;
- }
-
- @Inject
- 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
- public void onProxyInitialization(ProxyInitializeEvent event) {
- // Set instance
- instance = this;
-
- // Load dependencies
- fetchDependencies();
-
- // Setup logger
- velocityLogger = new VelocityLogger(logger);
-
- // Prepare synchronised servers tracker
- synchronisedServers = new HashSet<>();
-
- // Load config
- ConfigManager.loadConfig();
-
- // Load settings from config
- ConfigLoader.loadSettings(Objects.requireNonNull(ConfigManager.getConfig()));
-
- // Load messages
- ConfigManager.loadMessages();
-
- // Load locales from messages
- ConfigLoader.loadMessageStrings(Objects.requireNonNull(ConfigManager.getMessages()));
-
- // Do update checker
- if (Settings.automaticUpdateChecks) {
- new VelocityUpdateChecker(VERSION).logToConsole();
- }
-
- // Setup data manager
- dataManager = new DataManager(getVelocityLogger(), getDataFolder());
-
- // Ensure the data manager initialized correctly
- if (dataManager.hasFailedInitialization) {
- getVelocityLogger().severe("Failed to initialize the HuskSync database(s).\n" +
- "HuskSync will now abort loading itself (Velocity) v" + VERSION);
- }
-
- // Setup player data cache
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- dataManager.playerDataCache.put(cluster, new DataManager.PlayerDataCache());
- }
-
- // Initialize the redis listener
- redisListener = new VelocityRedisListener();
-
- // Register listener
- server.getEventManager().register(this, new VelocityEventListener());
-
- // Register command
- CommandManager commandManager = getProxyServer().getCommandManager();
- CommandMeta meta = commandManager.metaBuilder("husksync")
- .aliases("hs")
- .build();
- commandManager.register(meta, new VelocityCommand());
-
- // Prepare the migrator for use if needed
- mpdbMigrator = new MPDBMigrator(getVelocityLogger());
-
- // Initialize bStats metrics
- try {
- metricsFactory.make(this, METRICS_ID);
- } catch (Exception e) {
- getVelocityLogger().info("Skipped metrics initialization");
- }
-
- // Log to console
- getVelocityLogger().info("Enabled HuskSync (Velocity) v" + VERSION);
-
- // Mark as ready for redis message processing
- readyForRedis = true;
- }
-
- @Subscribe
- public void onProxyShutdown(ProxyShutdownEvent event) {
- // Plugin shutdown logic
- isDisabling = true;
-
- // Send terminating handshake message
- for (Server server : synchronisedServers) {
- try {
- new RedisMessage(RedisMessage.MessageType.TERMINATE_HANDSHAKE,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, server.clusterId()),
- server.serverUUID().toString(),
- "Velocity").send();
- } catch (IOException e) {
- getVelocityLogger().log(Level.SEVERE, "Failed to serialize Redis message for handshake termination", e);
- }
- }
-
- // Close database connections
- dataManager.closeDatabases();
-
- // Log to console
- getVelocityLogger().info("Disabled HuskSync (Velocity) v" + VERSION);
- }
-
- // Load dependencies
- private void fetchDependencies() {
- VelocityLibraryManager manager = new VelocityLibraryManager<>(logger, dataDirectory, getProxyServer().getPluginManager(), getInstance(), "lib");
-
- Library mySqlLib = Library.builder()
- .groupId("mysql")
- .artifactId("mysql-connector-java")
- .version("8.0.29")
- .build();
-
- Library sqLiteLib = Library.builder()
- .groupId("org.xerial")
- .artifactId("sqlite-jdbc")
- .version("3.36.0.3")
- .build();
-
- manager.addMavenCentral();
- manager.loadLibrary(mySqlLib);
- manager.loadLibrary(sqLiteLib);
- }
-}
diff --git a/velocity/src/main/java/net/william278/husksync/velocity/command/VelocityCommand.java b/velocity/src/main/java/net/william278/husksync/velocity/command/VelocityCommand.java
deleted file mode 100644
index 9efe62d8..00000000
--- a/velocity/src/main/java/net/william278/husksync/velocity/command/VelocityCommand.java
+++ /dev/null
@@ -1,423 +0,0 @@
-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 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.*;
-import java.util.logging.Level;
-import java.util.stream.Collectors;
-
-public class VelocityCommand implements SimpleCommand, HuskSyncCommand {
-
- private static final HuskSyncVelocity plugin = HuskSyncVelocity.getInstance();
-
- @Override
- public void execute(Invocation invocation) {
- final String[] args = invocation.arguments();
- final CommandSource sender = invocation.source();
- if (sender instanceof Player player) {
- if (HuskSyncVelocity.synchronisedServers.size() == 0) {
- player.sendMessage(new MineDown(MessageManager.getMessage("error_no_servers_proxied")).toComponent());
- return;
- }
- if (args.length >= 1) {
- switch (args[0].toLowerCase(Locale.ROOT)) {
- case "about", "info" -> sendAboutInformation(player);
- case "update" -> {
- if (!player.hasPermission("husksync.command.inventory")) {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
- return;
- }
- sender.sendMessage(new MineDown("[Checking for HuskSync updates...](gray)").toComponent());
- plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
- // Check Bukkit servers needing updates
- int updatesNeeded = 0;
- String bukkitBrand = "Spigot";
- String bukkitVersion = "1.0";
- for (Server server : HuskSyncVelocity.synchronisedServers) {
- VelocityUpdateChecker updateChecker = new VelocityUpdateChecker(server.huskSyncVersion());
- if (!updateChecker.isUpToDate()) {
- updatesNeeded++;
- bukkitBrand = server.serverBrand();
- bukkitVersion = server.huskSyncVersion();
- }
- }
-
- // Check Velocity servers needing updates and send message
- VelocityUpdateChecker proxyUpdateChecker = new VelocityUpdateChecker(HuskSyncVelocity.VERSION);
- if (proxyUpdateChecker.isUpToDate() && updatesNeeded == 0) {
- sender.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date, running Version " + proxyUpdateChecker.getLatestVersion() + "](#00fb9a)").toComponent());
- } else {
- sender.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| Your server(s) are not up-to-date:](#00fb9a)").toComponent());
- if (!proxyUpdateChecker.isUpToDate()) {
- sender.sendMessage(new MineDown("[•](white) [HuskSync on the Velocity proxy is outdated (Latest: " + proxyUpdateChecker.getLatestVersion() + ", Running: " + proxyUpdateChecker.getCurrentVersion() + ")](#00fb9a)").toComponent());
- }
- if (updatesNeeded > 0) {
- sender.sendMessage(new MineDown("[•](white) [HuskSync on " + updatesNeeded + " connected " + bukkitBrand + " server(s) are outdated (Latest: " + proxyUpdateChecker.getLatestVersion() + ", Running: " + bukkitVersion + ")](#00fb9a)").toComponent());
- }
- sender.sendMessage(new MineDown("[•](white) [Download links:](#00fb9a) [[⏩ Spigot]](gray open_url=https://www.spigotmc.org/resources/husktowns.92672/updates) [•](#262626) [[⏩ Polymart]](gray open_url=https://polymart.org/resource/husktowns.1056/updates)").toComponent());
- }
- }).schedule();
- }
- case "invsee", "openinv", "inventory" -> {
- if (!player.hasPermission("husksync.command.inventory")) {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
- return;
- }
- String clusterId;
- if (Settings.clusters.size() > 1) {
- if (args.length == 3) {
- clusterId = args[2];
- } else {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
- return;
- }
- } else {
- clusterId = "main";
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- clusterId = cluster.clusterId();
- break;
- }
- }
- if (args.length == 2 || args.length == 3) {
- String playerName = args[1];
- openInventory(player, playerName, clusterId);
- } else {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax").replaceAll("%1%",
- "/husksync invsee ")).toComponent());
- }
- }
- case "echest", "enderchest" -> {
- if (!player.hasPermission("husksync.command.ender_chest")) {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
- return;
- }
- String clusterId;
- if (Settings.clusters.size() > 1) {
- if (args.length == 3) {
- clusterId = args[2];
- } else {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
- return;
- }
- } else {
- clusterId = "main";
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- clusterId = cluster.clusterId();
- break;
- }
- }
- if (args.length == 2 || args.length == 3) {
- String playerName = args[1];
- openEnderChest(player, playerName, clusterId);
- } else {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax")
- .replaceAll("%1%", "/husksync echest ")).toComponent());
- }
- }
- case "migrate" -> {
- if (!player.hasPermission("husksync.command.admin")) {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
- return;
- }
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_console_command_only")
- .replaceAll("%1%", "Velocity")).toComponent());
- }
- case "status" -> {
- if (!player.hasPermission("husksync.command.admin")) {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
- return;
- }
- int playerDataSize = 0;
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- playerDataSize += HuskSyncVelocity.dataManager.playerDataCache.get(cluster).playerData.size();
- }
- sender.sendMessage(new MineDown(MessageManager.PLUGIN_STATUS.toString()
- .replaceAll("%1%", String.valueOf(HuskSyncVelocity.synchronisedServers.size()))
- .replaceAll("%2%", String.valueOf(playerDataSize))).toComponent());
- }
- case "reload" -> {
- if (!player.hasPermission("husksync.command.admin")) {
- sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
- return;
- }
- ConfigManager.loadConfig();
- ConfigLoader.loadSettings(Objects.requireNonNull(ConfigManager.getConfig()));
-
- ConfigManager.loadMessages();
- ConfigLoader.loadMessageStrings(Objects.requireNonNull(ConfigManager.getMessages()));
-
- // Send reload request to all bukkit servers
- try {
- new RedisMessage(RedisMessage.MessageType.RELOAD_CONFIG,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null),
- "reload")
- .send();
- } catch (IOException e) {
- plugin.getVelocityLogger().log(Level.WARNING, "Failed to serialize reload notification message data");
- }
-
- sender.sendMessage(new MineDown(MessageManager.getMessage("reload_complete")).toComponent());
- }
- default -> sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax").replaceAll("%1%",
- "/husksync ")).toComponent());
- }
- } else {
- sendAboutInformation(player);
- }
- } else {
- // Database migration wizard
- if (args.length >= 1) {
- if (args[0].equalsIgnoreCase("migrate")) {
- MPDBMigrator migrator = HuskSyncVelocity.mpdbMigrator;
- if (args.length == 1) {
- sender.sendMessage(new MineDown(
- """
- === MySQLPlayerDataBridge Migration Wizard ==========
- This will migrate data from the MySQLPlayerDataBridge
- plugin to HuskSync.
-
- Data that will be migrated:
- - Inventories
- - Ender Chests
- - Experience points
-
- Other non-vital data, such as current health, hunger
- & potion effects will not be migrated to ensure that
- migration does not take an excessive amount of time.
-
- To do this, you need to have MySqlPlayerDataBridge
- and HuskSync installed on one Spigot server as well
- as HuskSync installed on the proxy (which you have)
-
- >To proceed, type: husksync migrate setup""").toComponent());
- } else {
- switch (args[1].toLowerCase()) {
- case "setup" -> sender.sendMessage(new MineDown(
- """
- === MySQLPlayerDataBridge Migration Wizard ==========
- The following database settings will be used.
- Please make sure they match the correct settings to
- access your MySQLPlayerDataBridge Data
-
- sourceHost: %1%
- sourcePort: %2%
- sourceDatabase: %3%
- sourceUsername: %4%
- sourcePassword: %5%
-
- sourceInventoryTableName: %6%
- sourceEnderChestTableName: %7%
- sourceExperienceTableName: %8%
-
- targetCluster: %9%
-
- To change a setting, type:
- husksync migrate setting
-
- Please ensure no players are logged in to the network
- and that at least one Spigot server is online with
- both HuskSync AND MySqlPlayerDataBridge installed AND
- that the server has been configured with the correct
- Redis credentials.
-
- Warning: Data will be saved to your configured data
- source, which is currently a %10% database.
- Please make sure you are happy with this, or stop
- the proxy server and edit this in config.yml
-
- Warning: Migration will overwrite any current data
- saved by HuskSync. It will not, however, delete any
- data from the source MySQLPlayerDataBridge database.
-
- >When done, type: husksync migrate start"""
- .replaceAll("%1%", migrator.migrationSettings.sourceHost)
- .replaceAll("%2%", String.valueOf(migrator.migrationSettings.sourcePort))
- .replaceAll("%3%", migrator.migrationSettings.sourceDatabase)
- .replaceAll("%4%", migrator.migrationSettings.sourceUsername)
- .replaceAll("%5%", migrator.migrationSettings.sourcePassword)
- .replaceAll("%6%", migrator.migrationSettings.inventoryDataTable)
- .replaceAll("%7%", migrator.migrationSettings.enderChestDataTable)
- .replaceAll("%8%", migrator.migrationSettings.expDataTable)
- .replaceAll("%9%", migrator.migrationSettings.targetCluster)
- .replaceAll("%10%", Settings.dataStorageType.toString())
- ).toComponent());
- case "setting" -> {
- if (args.length == 4) {
- String value = args[3];
- switch (args[2]) {
- case "sourceHost", "host" -> migrator.migrationSettings.sourceHost = value;
- case "sourcePort", "port" -> {
- try {
- migrator.migrationSettings.sourcePort = Integer.parseInt(value);
- } catch (NumberFormatException e) {
- sender.sendMessage(new MineDown("Error: Invalid value; port must be a number").toComponent());
- return;
- }
- }
- case "sourceDatabase", "database" -> migrator.migrationSettings.sourceDatabase = value;
- case "sourceUsername", "username" -> migrator.migrationSettings.sourceUsername = value;
- case "sourcePassword", "password" -> migrator.migrationSettings.sourcePassword = value;
- case "sourceInventoryTableName", "inventoryTableName", "inventoryTable" -> migrator.migrationSettings.inventoryDataTable = value;
- case "sourceEnderChestTableName", "enderChestTableName", "enderChestTable" -> migrator.migrationSettings.enderChestDataTable = value;
- case "sourceExperienceTableName", "experienceTableName", "experienceTable" -> migrator.migrationSettings.expDataTable = value;
- case "targetCluster", "cluster" -> migrator.migrationSettings.targetCluster = value;
- default -> {
- sender.sendMessage(new MineDown("Error: Invalid setting; please use \"husksync migrate setup\" to view a list").toComponent());
- return;
- }
- }
- sender.sendMessage(new MineDown("Successfully updated setting: \"" + args[2] + "\" --> \"" + value + "\"").toComponent());
- } else {
- sender.sendMessage(new MineDown("Error: Invalid usage. Syntax: husksync migrate setting ").toComponent());
- }
- }
- case "start" -> {
- sender.sendMessage(new MineDown("Starting MySQLPlayerDataBridge migration!...").toComponent());
-
- // If the migrator is ready, execute the migration asynchronously
- if (HuskSyncVelocity.mpdbMigrator.readyToMigrate(plugin.getProxyServer().getPlayerCount(),
- HuskSyncVelocity.synchronisedServers)) {
- plugin.getProxyServer().getScheduler().buildTask(plugin, () ->
- HuskSyncVelocity.mpdbMigrator.executeMigrationOperations(HuskSyncVelocity.dataManager,
- HuskSyncVelocity.synchronisedServers, HuskSyncVelocity.redisListener)).schedule();
- }
- }
- default -> sender.sendMessage(new MineDown("Error: Invalid argument for migration. Use \"husksync migrate\" to start the process").toComponent());
- }
- }
- return;
- }
- }
- sender.sendMessage(new MineDown("Error: Invalid syntax. Usage: husksync migrate ").toComponent());
- }
- }
-
- // 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_inventory")).toComponent());
- return;
- }
- if (plugin.getProxyServer().getPlayer(targetPlayerName).isPresent()) {
- viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_inventory_online")).toComponent());
- return;
- }
- plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- if (!cluster.clusterId().equals(clusterId)) continue;
- PlayerData playerData = HuskSyncVelocity.dataManager.getPlayerDataByName(targetPlayerName, cluster.clusterId());
- if (playerData == null) {
- viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent());
- return;
- }
- try {
- new RedisMessage(RedisMessage.MessageType.OPEN_INVENTORY,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId(), null),
- targetPlayerName, RedisMessage.serialize(playerData))
- .send();
- viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_inventory_of").replaceAll("%1%",
- targetPlayerName)).toComponent());
- } catch (IOException e) {
- plugin.getVelocityLogger().log(Level.WARNING, "Failed to serialize inventory-see player data", e);
- }
- return;
- }
- viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
- }).schedule();
- }
-
- // View the ender chest of a player specified by their name
- public void openEnderChest(Player viewer, String targetPlayerName, String clusterId) {
- if (viewer.getUsername().equalsIgnoreCase(targetPlayerName)) {
- viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_ender_chest")).toComponent());
- return;
- }
- if (plugin.getProxyServer().getPlayer(targetPlayerName).isPresent()) {
- viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_ender_chest_online")).toComponent());
- return;
- }
- plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- if (!cluster.clusterId().equals(clusterId)) continue;
- PlayerData playerData = HuskSyncVelocity.dataManager.getPlayerDataByName(targetPlayerName, cluster.clusterId());
- if (playerData == null) {
- viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent());
- return;
- }
- try {
- new RedisMessage(RedisMessage.MessageType.OPEN_ENDER_CHEST,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId(), null),
- targetPlayerName, RedisMessage.serialize(playerData))
- .send();
- viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_ender_chest_of").replaceAll("%1%",
- targetPlayerName)).toComponent());
- } catch (IOException e) {
- plugin.getVelocityLogger().log(Level.WARNING, "Failed to serialize inventory-see player data", e);
- }
- return;
- }
- viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
- }).schedule();
- }
-
- /**
- * Send information about the plugin
- *
- * @param player The player to send it to
- */
- private void sendAboutInformation(Player player) {
- try {
- new RedisMessage(RedisMessage.MessageType.SEND_PLUGIN_INFORMATION,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, player.getUniqueId(), null),
- "Velocity", HuskSyncVelocity.VERSION).send();
- } catch (IOException e) {
- plugin.getVelocityLogger().log(Level.WARNING, "Failed to serialize plugin information to send", e);
- }
- }
-
- @Override
- public List suggest(Invocation invocation) {
- final CommandSource sender = invocation.source();
- final String[] args = invocation.arguments();
-
- if (sender instanceof Player player) {
- if (args.length == 1) {
- final ArrayList subCommands = new ArrayList<>();
- for (SubCommand subCommand : SUB_COMMANDS) {
- if (subCommand.permission() != null) {
- if (!player.hasPermission(subCommand.permission())) {
- continue;
- }
- }
- subCommands.add(subCommand.command());
- }
- // Return list of subcommands
- if (args[0].length() == 0) {
- return subCommands;
- }
-
- // Automatically filter the sub commands' order in tab completion by what the player has typed
- return subCommands.stream().filter(val -> val.startsWith(args[0]))
- .sorted().collect(Collectors.toList());
- } else {
- return Collections.emptyList();
- }
- }
- return Collections.emptyList();
- }
-}
\ No newline at end of file
diff --git a/velocity/src/main/java/net/william278/husksync/velocity/config/ConfigLoader.java b/velocity/src/main/java/net/william278/husksync/velocity/config/ConfigLoader.java
deleted file mode 100644
index 12f170fa..00000000
--- a/velocity/src/main/java/net/william278/husksync/velocity/config/ConfigLoader.java
+++ /dev/null
@@ -1,101 +0,0 @@
-package net.william278.husksync.velocity.config;
-
-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;
-
-public class ConfigLoader {
-
- private static ConfigurationNode copyDefaults(ConfigurationNode configRoot) {
- // Get the config version and update if needed
- String configVersion = getConfigString(configRoot, "1.0", "config_file_version");
- if (configVersion.contains("-dev")) {
- configVersion = configVersion.replaceAll("-dev", "");
- }
- if (!configVersion.equals(HuskSyncVelocity.VERSION)) {
- if (configVersion.equalsIgnoreCase("1.0")) {
- configRoot.getNode("check_for_updates").setValue(true);
- }
- if (configVersion.equalsIgnoreCase("1.0") || configVersion.equalsIgnoreCase("1.0.1") || configVersion.equalsIgnoreCase("1.0.2") || configVersion.equalsIgnoreCase("1.0.3")) {
- configRoot.getNode("clusters", "main", "player_table").setValue("husksync_players");
- configRoot.getNode("clusters", "main", "data_table").setValue("husksync_data");
- }
- configRoot.getNode("config_file_version").setValue(HuskSyncVelocity.VERSION);
- }
- // Save the config back
- ConfigManager.saveConfig(configRoot);
- return configRoot;
- }
-
- private static String getConfigString(ConfigurationNode rootNode, String defaultValue, String... nodePath) {
- 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;
- }
-
- private static int getConfigInt(ConfigurationNode rootNode, int defaultValue, String... nodePath) {
- 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;
- }
-
- public static void loadSettings(ConfigurationNode loadedConfig) throws IllegalArgumentException {
- ConfigurationNode config = copyDefaults(loadedConfig);
-
- Settings.language = getConfigString(config, "en-gb", "language");
-
- Settings.serverType = Settings.ServerType.PROXY;
- Settings.automaticUpdateChecks = getConfigBoolean(config, true, "check_for_updates");
- 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) {
- Settings.mySQLHost = getConfigString(config, "localhost", "data_storage_settings", "mysql_settings", "host");
- Settings.mySQLPort = getConfigInt(config, 3306, "data_storage_settings", "mysql_settings", "port");
- Settings.mySQLDatabase = getConfigString(config, "HuskSync", "data_storage_settings", "mysql_settings", "database");
- Settings.mySQLUsername = getConfigString(config, "root", "data_storage_settings", "mysql_settings", "username");
- Settings.mySQLPassword = getConfigString(config, "pa55w0rd", "data_storage_settings", "mysql_settings", "password");
- Settings.mySQLParams = getConfigString(config, "?autoReconnect=true&useSSL=false", "data_storage_settings", "mysql_settings", "params");
- }
-
- Settings.hikariMaximumPoolSize = getConfigInt(config, 10, "data_storage_settings", "hikari_pool_settings", "maximum_pool_size");
- Settings.hikariMinimumIdle = getConfigInt(config, 10, "data_storage_settings", "hikari_pool_settings", "minimum_idle");
- Settings.hikariMaximumLifetime = getConfigLong(config, 1800000, "data_storage_settings", "hikari_pool_settings", "maximum_lifetime");
- 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";
- for (ConfigurationNode cluster : clusterSection.getChildrenMap().values()) {
- final String clusterId = (String) cluster.getKey();
- final String playerTableName = getConfigString(config, "husksync_players", "clusters", clusterId, "player_table");
- final String dataTableName = getConfigString(config, "husksync_data", "clusters", clusterId, "data_table");
- final String databaseName = getConfigString(config, settingDatabaseName, "clusters", clusterId, "database");
- Settings.clusters.add(new Settings.SynchronisationCluster(clusterId, databaseName, playerTableName, dataTableName));
- }
- }
-
- public static void loadMessageStrings(ConfigurationNode config) {
- final HashMap messages = new HashMap<>();
- for (ConfigurationNode message : config.getChildrenMap().values()) {
- final String messageId = (String) message.getKey();
- messages.put(messageId, getConfigString(config, "", messageId));
- }
- MessageManager.setMessages(messages);
- }
-
-}
diff --git a/velocity/src/main/java/net/william278/husksync/velocity/config/ConfigManager.java b/velocity/src/main/java/net/william278/husksync/velocity/config/ConfigManager.java
deleted file mode 100644
index 215e8aa7..00000000
--- a/velocity/src/main/java/net/william278/husksync/velocity/config/ConfigManager.java
+++ /dev/null
@@ -1,96 +0,0 @@
-package net.william278.husksync.velocity.config;
-
-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;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.util.Objects;
-import java.util.logging.Level;
-
-public class ConfigManager {
-
- private static final HuskSyncVelocity plugin = HuskSyncVelocity.getInstance();
-
- public static void loadConfig() {
- try {
- if (!plugin.getDataFolder().exists()) {
- if (plugin.getDataFolder().mkdir()) {
- plugin.getVelocityLogger().info("Created HuskSync data folder");
- }
- }
- File configFile = new File(plugin.getDataFolder(), "config.yml");
- if (!configFile.exists()) {
- Files.copy(Objects.requireNonNull(HuskSyncVelocity.class.getClassLoader().getResourceAsStream("proxy-config.yml")), configFile.toPath());
- plugin.getVelocityLogger().info("Created HuskSync config file");
- }
- } catch (Exception e) {
- plugin.getVelocityLogger().log(Level.CONFIG, "An exception occurred loading the configuration file", e);
- }
- }
-
- public static void saveConfig(ConfigurationNode rootNode) {
- try {
- getConfigLoader().save(rootNode);
- } catch (IOException e) {
- plugin.getVelocityLogger().log(Level.CONFIG, "An exception occurred loading the configuration file", e);
- }
- }
-
- public static void loadMessages() {
- try {
- if (!plugin.getDataFolder().exists()) {
- if (plugin.getDataFolder().mkdir()) {
- plugin.getVelocityLogger().info("Created HuskSync data folder");
- }
- }
- File messagesFile = new File(plugin.getDataFolder(), "messages_" + Settings.language + ".yml");
- if (!messagesFile.exists()) {
- Files.copy(Objects.requireNonNull(HuskSyncVelocity.class.getClassLoader().getResourceAsStream("languages/" + Settings.language + ".yml")),
- messagesFile.toPath());
- plugin.getVelocityLogger().info("Created HuskSync messages file");
- }
- } catch (IOException e) {
- plugin.getVelocityLogger().log(Level.CONFIG, "An exception occurred loading the messages file", e);
- }
- }
-
- private static YAMLConfigurationLoader getConfigLoader() {
- File configFile = new File(plugin.getDataFolder(), "config.yml");
- return YAMLConfigurationLoader.builder()
- .setPath(configFile.toPath())
- .setFlowStyle(DumperOptions.FlowStyle.BLOCK)
- .setIndent(2)
- .build();
- }
-
- public static ConfigurationNode getConfig() {
- try {
- return getConfigLoader().load();
- } catch (IOException e) {
- plugin.getVelocityLogger().log(Level.CONFIG, "An IOException has occurred loading the plugin config.");
- return null;
- }
- }
-
- public static ConfigurationNode getMessages() {
- try {
- File configFile = new File(plugin.getDataFolder(), "messages_" + Settings.language + ".yml");
- return YAMLConfigurationLoader.builder()
- .setPath(configFile.toPath())
- .setFlowStyle(DumperOptions.FlowStyle.BLOCK)
- .setIndent(2)
- .build()
- .load();
- } catch (IOException e) {
- plugin.getVelocityLogger().log(Level.CONFIG, "An IOException has occurred loading the plugin messages.");
- return null;
- }
- }
-
-}
-
diff --git a/velocity/src/main/java/net/william278/husksync/velocity/listener/VelocityEventListener.java b/velocity/src/main/java/net/william278/husksync/velocity/listener/VelocityEventListener.java
deleted file mode 100644
index f374aaa7..00000000
--- a/velocity/src/main/java/net/william278/husksync/velocity/listener/VelocityEventListener.java
+++ /dev/null
@@ -1,47 +0,0 @@
-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 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;
-import java.util.logging.Level;
-
-public class VelocityEventListener {
-
- private static final HuskSyncVelocity plugin = HuskSyncVelocity.getInstance();
-
- @Subscribe(order = PostOrder.FIRST)
- public void onPostLogin(PostLoginEvent event) {
- final Player player = event.getPlayer();
- plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
- // Ensure the player has data on SQL and that it is up-to-date
- HuskSyncVelocity.dataManager.ensurePlayerExists(player.getUniqueId(), player.getUsername());
-
- // Get the player's data from SQL
- final Map data = HuskSyncVelocity.dataManager.getPlayerData(player.getUniqueId());
-
- // Update the player's data from SQL onto the cache
- assert data != null;
- for (Settings.SynchronisationCluster cluster : data.keySet()) {
- HuskSyncVelocity.dataManager.playerDataCache.get(cluster).updatePlayer(data.get(cluster));
- }
-
- // Send a message asking the bukkit to request data on join
- try {
- new RedisMessage(RedisMessage.MessageType.REQUEST_DATA_ON_JOIN,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null),
- RedisMessage.RequestOnJoinUpdateType.ADD_REQUESTER.toString(), player.getUniqueId().toString()).send();
- } catch (IOException e) {
- plugin.getVelocityLogger().log(Level.SEVERE, "Failed to serialize request data on join message data");
- e.printStackTrace();
- }
- }).schedule();
- }
-}
diff --git a/velocity/src/main/java/net/william278/husksync/velocity/listener/VelocityRedisListener.java b/velocity/src/main/java/net/william278/husksync/velocity/listener/VelocityRedisListener.java
deleted file mode 100644
index e37eec47..00000000
--- a/velocity/src/main/java/net/william278/husksync/velocity/listener/VelocityRedisListener.java
+++ /dev/null
@@ -1,231 +0,0 @@
-package net.william278.husksync.velocity.listener;
-
-import com.velocitypowered.api.proxy.Player;
-import de.themoep.minedown.adventure.MineDown;
-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;
-import java.util.Optional;
-import java.util.UUID;
-import java.util.logging.Level;
-
-public class VelocityRedisListener extends RedisListener {
-
- private static final HuskSyncVelocity plugin = HuskSyncVelocity.getInstance();
-
- // Initialize the listener on the bungee
- public VelocityRedisListener() {
- super();
- listen();
- }
-
- private PlayerData getPlayerCachedData(UUID uuid, String clusterId) {
- PlayerData data = null;
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- if (cluster.clusterId().equals(clusterId)) {
- // Get the player data from the cache
- PlayerData cachedData = HuskSyncVelocity.dataManager.playerDataCache.get(cluster).getPlayer(uuid);
- if (cachedData != null) {
- return cachedData;
- }
-
- data = Objects.requireNonNull(HuskSyncVelocity.dataManager.getPlayerData(uuid)).get(cluster); // Get their player data from MySQL
- HuskSyncVelocity.dataManager.playerDataCache.get(cluster).updatePlayer(data); // Update the cache
- break;
- }
- }
- return data; // Return the data
- }
-
- /**
- * Handle an incoming {@link RedisMessage}
- *
- * @param message The {@link RedisMessage} to handle
- */
- @Override
- public void handleMessage(RedisMessage message) {
- // Ignore messages destined for Bukkit servers
- if (message.getMessageTarget().targetServerType() != Settings.ServerType.PROXY) {
- return;
- }
- // Only process redis messages when ready
- if (!HuskSyncVelocity.readyForRedis) {
- return;
- }
-
- switch (message.getMessageType()) {
- case PLAYER_DATA_REQUEST -> plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
- // Get the UUID of the requesting player
- final UUID requestingPlayerUUID = UUID.fromString(message.getMessageData());
- try {
- // Send the reply, serializing the message data
- new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, requestingPlayerUUID, message.getMessageTarget().targetClusterId()),
- RedisMessage.serialize(getPlayerCachedData(requestingPlayerUUID, message.getMessageTarget().targetClusterId())))
- .send();
-
- // Send an update to all bukkit servers removing the player from the requester cache
- new RedisMessage(RedisMessage.MessageType.REQUEST_DATA_ON_JOIN,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
- RedisMessage.RequestOnJoinUpdateType.REMOVE_REQUESTER.toString(), requestingPlayerUUID.toString())
- .send();
-
- // Send synchronisation complete message
- Optional 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.getMessageDataElements()[0];
- final boolean bounceBack = Boolean.parseBoolean(message.getMessageDataElements()[1]);
- try {
- playerData = (PlayerData) RedisMessage.deserialize(serializedPlayerData);
- } catch (IOException | ClassNotFoundException e) {
- log(Level.SEVERE, "Failed to deserialize PlayerData when handling a player update request");
- e.printStackTrace();
- return;
- }
-
- // Update the data in the cache and SQL
- for (Settings.SynchronisationCluster cluster : Settings.clusters) {
- if (cluster.clusterId().equals(message.getMessageTarget().targetClusterId())) {
- HuskSyncVelocity.dataManager.updatePlayerData(playerData, cluster);
- break;
- }
- }
-
- // Reply with the player data if they are still online (switching server)
- if (Settings.bounceBackSynchronisation && bounceBack) {
- Optional 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();
- }
- });
- }
- }).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
- final UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
- final boolean hasMySqlPlayerDataBridge = Boolean.parseBoolean(message.getMessageDataElements()[1]);
- final String bukkitBrand = message.getMessageDataElements()[2];
- final String huskSyncVersion = message.getMessageDataElements()[3];
- try {
- new RedisMessage(RedisMessage.MessageType.CONNECTION_HANDSHAKE,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
- serverUUID.toString(), "Velocity")
- .send();
- HuskSyncVelocity.synchronisedServers.add(
- new Server(serverUUID, hasMySqlPlayerDataBridge,
- huskSyncVersion, bukkitBrand, message.getMessageTarget().targetClusterId()));
- log(Level.INFO, "Completed handshake with " + bukkitBrand + " server (" + serverUUID + ")");
- } catch (IOException e) {
- log(Level.SEVERE, "Failed to serialize handshake message data");
- e.printStackTrace();
- }
- }
- case TERMINATE_HANDSHAKE -> {
- // Terminate the handshake with a Bukkit server
- final UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
- final String bukkitBrand = message.getMessageDataElements()[1];
-
- // Remove a server from the synchronised server list
- Server serverToRemove = null;
- for (Server server : HuskSyncVelocity.synchronisedServers) {
- if (server.serverUUID().equals(serverUUID)) {
- serverToRemove = server;
- break;
- }
- }
- HuskSyncVelocity.synchronisedServers.remove(serverToRemove);
- log(Level.INFO, "Terminated the handshake with " + bukkitBrand + " server (" + serverUUID + ")");
- }
- case DECODED_MPDB_DATA_SET -> {
- // Deserialize the PlayerData received
- PlayerData playerData;
- final String serializedPlayerData = message.getMessageDataElements()[0];
- final String playerName = message.getMessageDataElements()[1];
- try {
- playerData = (PlayerData) RedisMessage.deserialize(serializedPlayerData);
- } catch (IOException | ClassNotFoundException e) {
- log(Level.SEVERE, "Failed to deserialize PlayerData when handling incoming decoded MPDB data");
- e.printStackTrace();
- return;
- }
-
- // Get the MPDB migrator
- MPDBMigrator migrator = HuskSyncVelocity.mpdbMigrator;
-
- // Add the incoming data to the data to be saved
- migrator.incomingPlayerData.put(playerData, playerName);
-
- // Increment players migrated
- migrator.playersMigrated++;
- plugin.getVelocityLogger().log(Level.INFO, "Migrated " + migrator.playersMigrated + "/" + migrator.migratedDataSent + " players.");
-
- // When all the data has been received, save it
- if (migrator.migratedDataSent == migrator.playersMigrated) {
- migrator.loadIncomingData(migrator.incomingPlayerData, HuskSyncVelocity.dataManager);
- }
- }
- 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]);
-
- try {
- final PlayerData data = getPlayerCachedData(playerUUID, message.getMessageTarget().targetClusterId());
-
- if (data == null) {
- new RedisMessage(RedisMessage.MessageType.API_DATA_CANCEL,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
- requestUUID.toString())
- .send();
- } else {
- // Send the reply alongside the request UUID, serializing the requested message data
- new RedisMessage(RedisMessage.MessageType.API_DATA_RETURN,
- new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
- requestUUID.toString(),
- RedisMessage.serialize(data))
- .send();
- }
- } catch (IOException e) {
- plugin.getVelocityLogger().log(Level.SEVERE, "Failed to serialize PlayerData requested via the API");
- }
- }).schedule();
- }
- }
-
- /**
- * Log to console
- *
- * @param level The {@link Level} to log
- * @param message Message to log
- */
- @Override
- public void log(Level level, String message) {
- plugin.getVelocityLogger().log(level, message);
- }
-}
\ No newline at end of file
diff --git a/velocity/src/main/java/net/william278/husksync/velocity/util/VelocityLogger.java b/velocity/src/main/java/net/william278/husksync/velocity/util/VelocityLogger.java
deleted file mode 100644
index 4c7022e8..00000000
--- a/velocity/src/main/java/net/william278/husksync/velocity/util/VelocityLogger.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package net.william278.husksync.velocity.util;
-
-import net.william278.husksync.util.Logger;
-
-import java.util.logging.Level;
-
-public record VelocityLogger(org.slf4j.Logger parent) implements Logger {
-
- @Override
- public void log(Level level, String message, Exception e) {
- logMessage(level, message);
- e.printStackTrace();
- }
-
- @Override
- public void log(Level level, String message) {
- logMessage(level, message);
- }
-
- @Override
- public void info(String message) {
- logMessage(Level.INFO, message);
- }
-
- @Override
- public void severe(String message) {
- logMessage(Level.SEVERE, message);
- }
-
- @Override
- public void config(String message) {
- logMessage(Level.CONFIG, message);
- }
-
- // Logs the message using SLF4J
- private void logMessage(Level level, String message) {
- switch (level.intValue()) {
- case 1000 -> parent.error(message); // Severe
- case 900 -> parent.warn(message); // Warning
- case 70 -> parent.warn("[Config] " + message);
- default -> parent.info(message);
- }
- }
-}
\ No newline at end of file
diff --git a/velocity/src/main/java/net/william278/husksync/velocity/util/VelocityUpdateChecker.java b/velocity/src/main/java/net/william278/husksync/velocity/util/VelocityUpdateChecker.java
deleted file mode 100644
index 3799bba1..00000000
--- a/velocity/src/main/java/net/william278/husksync/velocity/util/VelocityUpdateChecker.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package net.william278.husksync.velocity.util;
-
-import net.william278.husksync.HuskSyncVelocity;
-import net.william278.husksync.util.UpdateChecker;
-
-import java.util.logging.Level;
-
-public class VelocityUpdateChecker extends UpdateChecker {
-
- private static final HuskSyncVelocity plugin = HuskSyncVelocity.getInstance();
-
- public VelocityUpdateChecker(String versionToCheck) {
- super(versionToCheck);
- }
-
- @Override
- public void log(Level level, String message) {
- plugin.getVelocityLogger().log(level, message);
- }
-}
diff --git a/velocity/src/main/resources/velocity-plugin.json b/velocity/src/main/resources/velocity-plugin.json
deleted file mode 100644
index ee4c3bab..00000000
--- a/velocity/src/main/resources/velocity-plugin.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "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"
-}
\ No newline at end of file