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