diff --git a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java index 4dd79800..d95d0b20 100644 --- a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java +++ b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java @@ -12,6 +12,9 @@ import net.william278.husksync.command.HuskSyncCommand; import net.william278.husksync.command.Permission; import net.william278.husksync.config.Locales; import net.william278.husksync.config.Settings; +import net.william278.husksync.data.CompressedDataAdapter; +import net.william278.husksync.data.DataAdapter; +import net.william278.husksync.data.JsonDataAdapter; import net.william278.husksync.database.Database; import net.william278.husksync.database.MySqlDatabase; import net.william278.husksync.listener.BukkitEventListener; @@ -46,6 +49,8 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { private EventListener eventListener; + private DataAdapter dataAdapter; + private Settings settings; private Locales locales; @@ -91,9 +96,18 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { } return loadedSettings; }).join(); + }).thenApply(succeeded -> { + if (succeeded) { + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_COMPRESS_DATA)) { + dataAdapter = new CompressedDataAdapter(); + } else { + dataAdapter = new JsonDataAdapter(); + } + } + return succeeded; }).thenApply(succeeded -> { // Establish connection to the database - this.database = new MySqlDatabase(settings, resourceReader, logger); + this.database = new MySqlDatabase(settings, resourceReader, logger, dataAdapter); if (succeeded) { getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the database..."); final CompletableFuture databaseConnectFuture = new CompletableFuture<>(); @@ -101,7 +115,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { final boolean initialized = this.database.initialize(); if (!initialized) { getLoggingAdapter().log(Level.SEVERE, "Failed to establish a connection to the database. " - + "Please check the supplied database credentials in the config file"); + + "Please check the supplied database credentials in the config file"); databaseConnectFuture.completeAsync(() -> false); return; } @@ -113,13 +127,13 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { return false; }).thenApply(succeeded -> { // Establish connection to the Redis server - this.redisManager = new RedisManager(settings); + this.redisManager = new RedisManager(settings, dataAdapter); if (succeeded) { getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the Redis server..."); return this.redisManager.initialize().thenApply(initialized -> { if (!initialized) { getLoggingAdapter().log(Level.SEVERE, "Failed to establish a connection to the Redis server. " - + "Please check the supplied Redis credentials in the config file"); + + "Please check the supplied Redis credentials in the config file"); return false; } getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the Redis server"); @@ -167,7 +181,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { // Handle failed initialization if (!succeeded) { getLoggingAdapter().log(Level.SEVERE, "Failed to initialize HuskSync. " + - "The plugin will now be disabled"); + "The plugin will now be disabled"); getServer().getPluginManager().disablePlugin(this); } else { getLoggingAdapter().log(Level.INFO, "Successfully enabled HuskSync v" + getVersion()); @@ -207,6 +221,11 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { return redisManager; } + @Override + public @NotNull DataAdapter getDataAdapter() { + return dataAdapter; + } + @Override public @NotNull Settings getSettings() { return settings; diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java index f1603ac9..0419fe56 100644 --- a/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java +++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java @@ -43,7 +43,7 @@ public class BukkitSerializer { // 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"); + throw new DataDeserializationException("Failed to serialize item stack data", e); } }); } @@ -78,7 +78,7 @@ public class BukkitSerializer { return inventoryContents; } } catch (IOException | ClassNotFoundException e) { - throw new RuntimeException("Failed to deserialize item stack data"); + throw new DataDeserializationException("Failed to deserialize item stack data", e); } }); } @@ -132,7 +132,7 @@ public class BukkitSerializer { // 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"); + throw new DataDeserializationException("Failed to serialize potion effect data", e); } }); } @@ -167,7 +167,7 @@ public class BukkitSerializer { return potionEffects; } } catch (IOException | ClassNotFoundException e) { - throw new RuntimeException("Failed to deserialize potion effects", e); + throw new DataDeserializationException("Failed to deserialize potion effects", e); } }); } diff --git a/common/src/main/java/net/william278/husksync/HuskSync.java b/common/src/main/java/net/william278/husksync/HuskSync.java index 1c20308d..a689a749 100644 --- a/common/src/main/java/net/william278/husksync/HuskSync.java +++ b/common/src/main/java/net/william278/husksync/HuskSync.java @@ -2,6 +2,7 @@ package net.william278.husksync; import net.william278.husksync.config.Locales; import net.william278.husksync.config.Settings; +import net.william278.husksync.data.DataAdapter; import net.william278.husksync.database.Database; import net.william278.husksync.player.OnlineUser; import net.william278.husksync.redis.RedisManager; @@ -23,6 +24,8 @@ public interface HuskSync { @NotNull RedisManager getRedisManager(); + @NotNull DataAdapter getDataAdapter(); + @NotNull Settings getSettings(); @NotNull Locales getLocales(); diff --git a/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java b/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java index f7a9c3a7..73c2db55 100644 --- a/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java +++ b/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java @@ -7,11 +7,15 @@ import net.william278.husksync.player.OnlineUser; import net.william278.husksync.util.UpdateChecker; import org.jetbrains.annotations.NotNull; +import java.util.Arrays; import java.util.List; import java.util.logging.Level; +import java.util.stream.Collectors; public class HuskSyncCommand extends CommandBase implements TabCompletable, ConsoleExecutable { + private final String[] COMMAND_ARGUMENTS = {"update", "about", "reload"}; + public HuskSyncCommand(@NotNull HuskSync implementor) { super("husksync", Permission.COMMAND_HUSKSYNC, implementor); } @@ -32,8 +36,8 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons 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)")); + "[•](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 + "](#00fb9a)")); } @@ -56,11 +60,12 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons @Override public void onConsoleExecute(@NotNull String[] args) { if (args.length < 1) { - plugin.getLoggingAdapter().log(Level.INFO, "Console usage: /husksync "); + plugin.getLoggingAdapter().log(Level.INFO, "Console usage: \"husksync \""); return; } switch (args[0].toLowerCase()) { - case "update", "version" -> new UpdateChecker(plugin.getVersion(), plugin.getLoggingAdapter()).logToConsole(); + case "update", "version" -> + new UpdateChecker(plugin.getVersion(), plugin.getLoggingAdapter()).logToConsole(); case "info", "about" -> plugin.getLoggingAdapter().log(Level.INFO, plugin.getLocales().stripMineDown( Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getVersion()))); case "reload" -> { @@ -71,13 +76,15 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons //todo - MPDB migrator } default -> - plugin.getLoggingAdapter().log(Level.INFO, "Invalid syntax. Console usage: /husksync "); + plugin.getLoggingAdapter().log(Level.INFO, "Invalid syntax. Console usage: \"husksync \""); } } @Override public List onTabComplete(@NotNull OnlineUser player, @NotNull String[] args) { - return null; + return Arrays.stream(COMMAND_ARGUMENTS) + .filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : "")) + .sorted().collect(Collectors.toList()); } private void displayPluginInformation(@NotNull OnlineUser player) { diff --git a/common/src/main/java/net/william278/husksync/config/Settings.java b/common/src/main/java/net/william278/husksync/config/Settings.java index 0ad81a83..2f8e6c7d 100644 --- a/common/src/main/java/net/william278/husksync/config/Settings.java +++ b/common/src/main/java/net/william278/husksync/config/Settings.java @@ -141,6 +141,7 @@ public class Settings { 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_COMPRESS_DATA("synchronization.compress_data", 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), diff --git a/common/src/main/java/net/william278/husksync/data/AdvancementData.java b/common/src/main/java/net/william278/husksync/data/AdvancementData.java index 67860a04..c0abd2cd 100644 --- a/common/src/main/java/net/william278/husksync/data/AdvancementData.java +++ b/common/src/main/java/net/william278/husksync/data/AdvancementData.java @@ -23,11 +23,12 @@ public class AdvancementData { @SerializedName("completed_criteria") public Map completedCriteria; - public AdvancementData() { - } - public AdvancementData(@NotNull String key, @NotNull Map awardedCriteria) { this.key = key; this.completedCriteria = awardedCriteria; } + + @SuppressWarnings("unused") + protected AdvancementData() { + } } diff --git a/common/src/main/java/net/william278/husksync/data/CompressedDataAdapter.java b/common/src/main/java/net/william278/husksync/data/CompressedDataAdapter.java new file mode 100644 index 00000000..3beccc79 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/data/CompressedDataAdapter.java @@ -0,0 +1,27 @@ +package net.william278.husksync.data; + +import org.jetbrains.annotations.NotNull; +import org.xerial.snappy.Snappy; + +import java.io.IOException; + +public class CompressedDataAdapter extends JsonDataAdapter { + + @Override + public byte[] toBytes(@NotNull UserData data) throws DataAdaptionException { + try { + return Snappy.compress(super.toBytes(data)); + } catch (IOException e) { + throw new DataAdaptionException("Failed to compress data", e); + } + } + + @Override + public @NotNull UserData fromBytes(byte[] data) throws DataAdaptionException { + try { + return super.fromBytes(Snappy.uncompress(data)); + } catch (IOException e) { + throw new DataAdaptionException("Failed to decompress data", e); + } + } +} diff --git a/common/src/main/java/net/william278/husksync/data/DataAdapter.java b/common/src/main/java/net/william278/husksync/data/DataAdapter.java new file mode 100644 index 00000000..1ba2fa33 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/data/DataAdapter.java @@ -0,0 +1,29 @@ +package net.william278.husksync.data; + +import org.jetbrains.annotations.NotNull; + +/** + * An adapter that adapts {@link UserData} to and from a portable byte array. + */ +public interface DataAdapter { + + /** + * Converts {@link UserData} to a byte array. + * + * @param data The {@link UserData} to adapt. + * @return The byte array. + * @throws DataAdaptionException If an error occurred during adaptation. + */ + byte[] toBytes(@NotNull UserData data) throws DataAdaptionException; + + /** + * Converts a byte array to {@link UserData}. + * + * @param data The byte array to adapt. + * @return The {@link UserData}. + * @throws DataAdaptionException If an error occurred during adaptation, such as if the byte array is invalid. + */ + @NotNull + UserData fromBytes(final byte[] data) throws DataAdaptionException; + +} diff --git a/common/src/main/java/net/william278/husksync/data/DataAdaptionException.java b/common/src/main/java/net/william278/husksync/data/DataAdaptionException.java new file mode 100644 index 00000000..b38364bc --- /dev/null +++ b/common/src/main/java/net/william278/husksync/data/DataAdaptionException.java @@ -0,0 +1,11 @@ +package net.william278.husksync.data; + +/** + * Indicates an error occurred during {@link UserData} adaptation to and from (compressed) json. + */ +public class DataAdaptionException extends RuntimeException { + protected DataAdaptionException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/common/src/main/java/net/william278/husksync/data/DataDeserializationException.java b/common/src/main/java/net/william278/husksync/data/DataDeserializationException.java new file mode 100644 index 00000000..6812b37f --- /dev/null +++ b/common/src/main/java/net/william278/husksync/data/DataDeserializationException.java @@ -0,0 +1,13 @@ +package net.william278.husksync.data; + +/** + * Indicates an error occurred during base-64 serialization and deserialization of data. + *

+ * For example, an exception deserializing {@link InventoryData} item stack or {@link PotionEffectData} potion effect arrays + */ +public class DataDeserializationException extends RuntimeException { + protected DataDeserializationException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/common/src/main/java/net/william278/husksync/data/InventoryData.java b/common/src/main/java/net/william278/husksync/data/InventoryData.java index 0487c42c..f0ac1b2e 100644 --- a/common/src/main/java/net/william278/husksync/data/InventoryData.java +++ b/common/src/main/java/net/william278/husksync/data/InventoryData.java @@ -18,7 +18,8 @@ public class InventoryData { this.serializedInventory = serializedInventory; } - public InventoryData() { + @SuppressWarnings("unused") + protected InventoryData() { } } diff --git a/common/src/main/java/net/william278/husksync/data/JsonDataAdapter.java b/common/src/main/java/net/william278/husksync/data/JsonDataAdapter.java new file mode 100644 index 00000000..f3b1e6b2 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/data/JsonDataAdapter.java @@ -0,0 +1,24 @@ +package net.william278.husksync.data; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import org.jetbrains.annotations.NotNull; + +import java.nio.charset.StandardCharsets; + +public class JsonDataAdapter implements DataAdapter { + + @Override + public byte[] toBytes(@NotNull UserData data) throws DataAdaptionException { + return new GsonBuilder().create().toJson(data).getBytes(StandardCharsets.UTF_8); + } + + @Override + public @NotNull UserData fromBytes(byte[] data) throws DataAdaptionException { + try { + return new GsonBuilder().create().fromJson(new String(data, StandardCharsets.UTF_8), UserData.class); + } catch (JsonSyntaxException e) { + throw new DataAdaptionException("Failed to parse JSON data", e); + } + } +} diff --git a/common/src/main/java/net/william278/husksync/data/LocationData.java b/common/src/main/java/net/william278/husksync/data/LocationData.java index ee8d2a5f..e567e49d 100644 --- a/common/src/main/java/net/william278/husksync/data/LocationData.java +++ b/common/src/main/java/net/william278/husksync/data/LocationData.java @@ -53,9 +53,6 @@ public class LocationData { @SerializedName("pitch") public float pitch; - public LocationData() { - } - public LocationData(@NotNull String worldName, @NotNull UUID worldUuid, @NotNull String worldEnvironment, double x, double y, double z, @@ -69,4 +66,8 @@ public class LocationData { this.yaw = yaw; this.pitch = pitch; } + + @SuppressWarnings("unused") + protected LocationData() { + } } diff --git a/common/src/main/java/net/william278/husksync/data/PersistentDataContainerData.java b/common/src/main/java/net/william278/husksync/data/PersistentDataContainerData.java index c1c9cc57..ae3e50cc 100644 --- a/common/src/main/java/net/william278/husksync/data/PersistentDataContainerData.java +++ b/common/src/main/java/net/william278/husksync/data/PersistentDataContainerData.java @@ -20,7 +20,8 @@ public class PersistentDataContainerData { this.persistentDataMap = persistentDataMap; } - public PersistentDataContainerData() { + @SuppressWarnings("unused") + protected 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 index a8ad390e..cf8662a6 100644 --- a/common/src/main/java/net/william278/husksync/data/PotionEffectData.java +++ b/common/src/main/java/net/william278/husksync/data/PotionEffectData.java @@ -15,7 +15,8 @@ public class PotionEffectData { this.serializedPotionEffects = serializedInventory; } - public PotionEffectData() { + @SuppressWarnings("unused") + protected 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 index 62a2d409..c0d17d30 100644 --- a/common/src/main/java/net/william278/husksync/data/StatisticsData.java +++ b/common/src/main/java/net/william278/husksync/data/StatisticsData.java @@ -45,7 +45,8 @@ public class StatisticsData { this.entityStatistics = entityStatistics; } - public StatisticsData() { + @SuppressWarnings("unused") + protected 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 index 7606ca7e..d6b7f9c0 100644 --- a/common/src/main/java/net/william278/husksync/data/StatusData.java +++ b/common/src/main/java/net/william278/husksync/data/StatusData.java @@ -98,6 +98,7 @@ public class StatusData { this.isFlying = isFlying; } - public StatusData() { + @SuppressWarnings("unused") + protected 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 index 93fc1007..564dad04 100644 --- a/common/src/main/java/net/william278/husksync/data/UserData.java +++ b/common/src/main/java/net/william278/husksync/data/UserData.java @@ -1,7 +1,5 @@ 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; @@ -74,6 +72,8 @@ public class UserData { this.persistentDataContainerData = persistentDataContainerData; } + // Empty constructor to facilitate json serialization + @SuppressWarnings("unused") protected UserData() { } @@ -109,14 +109,4 @@ public class UserData { return persistentDataContainerData; } - @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); - } - } diff --git a/common/src/main/java/net/william278/husksync/database/Database.java b/common/src/main/java/net/william278/husksync/database/Database.java index e7bc4d2f..25b4b07d 100644 --- a/common/src/main/java/net/william278/husksync/database/Database.java +++ b/common/src/main/java/net/william278/husksync/database/Database.java @@ -1,5 +1,6 @@ package net.william278.husksync.database; +import net.william278.husksync.data.DataAdapter; import net.william278.husksync.data.UserData; import net.william278.husksync.data.VersionedUserData; import net.william278.husksync.player.User; @@ -36,6 +37,20 @@ public abstract class Database { */ protected final int maxUserDataRecords; + /** + * {@link DataAdapter} implementation used for adapting {@link UserData} to and from JSON + */ + private final DataAdapter dataAdapter; + + /** + * Returns the {@link DataAdapter} used to adapt {@link UserData} to and from JSON + * + * @return instance of the {@link DataAdapter} implementation + */ + protected DataAdapter getDataAdapter() { + return dataAdapter; + } + /** * Logger instance used for database error logging */ @@ -56,11 +71,12 @@ public abstract class Database { private final ResourceReader resourceReader; protected Database(@NotNull String playerTableName, @NotNull String dataTableName, final int maxUserDataRecords, - @NotNull ResourceReader resourceReader, @NotNull Logger logger) { + @NotNull ResourceReader resourceReader, @NotNull DataAdapter dataAdapter, @NotNull Logger logger) { this.playerTableName = playerTableName; this.dataTableName = dataTableName; this.maxUserDataRecords = maxUserDataRecords; this.resourceReader = resourceReader; + this.dataAdapter = dataAdapter; this.logger = logger; } diff --git a/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java b/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java index 106671aa..1dd9c0d8 100644 --- a/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java +++ b/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java @@ -2,6 +2,8 @@ package net.william278.husksync.database; import com.zaxxer.hikari.HikariDataSource; import net.william278.husksync.config.Settings; +import net.william278.husksync.data.DataAdapter; +import net.william278.husksync.data.DataAdaptionException; import net.william278.husksync.data.UserData; import net.william278.husksync.data.VersionedUserData; import net.william278.husksync.player.User; @@ -52,22 +54,23 @@ public class MySqlDatabase extends Database { */ private HikariDataSource connectionPool; - public MySqlDatabase(@NotNull Settings settings, @NotNull ResourceReader resourceReader, @NotNull Logger logger) { + public MySqlDatabase(@NotNull Settings settings, @NotNull ResourceReader resourceReader, @NotNull Logger logger, + @NotNull DataAdapter dataAdapter) { 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); + resourceReader, dataAdapter, logger); + this.mySqlHost = settings.getStringValue(Settings.ConfigOption.DATABASE_HOST); + this.mySqlPort = settings.getIntegerValue(Settings.ConfigOption.DATABASE_PORT); + this.mySqlDatabaseName = settings.getStringValue(Settings.ConfigOption.DATABASE_NAME); + this.mySqlUsername = settings.getStringValue(Settings.ConfigOption.DATABASE_USERNAME); + this.mySqlPassword = settings.getStringValue(Settings.ConfigOption.DATABASE_PASSWORD); + this.mySqlConnectionParameters = settings.getStringValue(Settings.ConfigOption.DATABASE_CONNECTION_PARAMS); + this.hikariMaximumPoolSize = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MAX_SIZE); + this.hikariMinimumIdle = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MIN_IDLE); + this.hikariMaximumLifetime = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MAX_LIFETIME); + this.hikariKeepAliveTime = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_KEEPALIVE); + this.hikariConnectionTimeOut = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_TIMEOUT); } /** @@ -219,16 +222,15 @@ public class MySqlDatabase extends Database { final ResultSet resultSet = statement.executeQuery(); if (resultSet.next()) { final Blob blob = resultSet.getBlob("data"); - final byte[] compressedDataJson = blob.getBytes(1, (int) blob.length()); + final byte[] dataByteArray = blob.getBytes(1, (int) blob.length()); blob.free(); return Optional.of(new VersionedUserData( UUID.fromString(resultSet.getString("version_uuid")), Date.from(resultSet.getTimestamp("timestamp").toInstant()), - UserData.fromJson(new String(Snappy.uncompress(compressedDataJson), - StandardCharsets.UTF_8)))); + getDataAdapter().fromBytes(dataByteArray))); } } - } catch (SQLException | IOException e) { + } catch (SQLException | DataAdaptionException e) { getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e); } return Optional.empty(); @@ -249,18 +251,17 @@ public class MySqlDatabase extends Database { final ResultSet resultSet = statement.executeQuery(); while (resultSet.next()) { final Blob blob = resultSet.getBlob("data"); - final byte[] compressedDataJson = blob.getBytes(1, (int) blob.length()); + final byte[] dataByteArray = blob.getBytes(1, (int) blob.length()); blob.free(); final VersionedUserData data = new VersionedUserData( UUID.fromString(resultSet.getString("version_uuid")), Date.from(resultSet.getTimestamp("timestamp").toInstant()), - UserData.fromJson(new String(Snappy.uncompress(compressedDataJson), - StandardCharsets.UTF_8))); + getDataAdapter().fromBytes(dataByteArray)); retrievedData.add(data); } return retrievedData; } - } catch (SQLException | IOException e) { + } catch (SQLException | DataAdaptionException e) { getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e); } return retrievedData; @@ -297,11 +298,11 @@ public class MySqlDatabase extends Database { (`player_uuid`,`version_uuid`,`timestamp`,`data`) VALUES (?,UUID(),NOW(),?);"""))) { statement.setString(1, user.uuid.toString()); - statement.setBlob(2, new ByteArrayInputStream(Snappy - .compress(userData.toJson().getBytes(StandardCharsets.UTF_8)))); + statement.setBlob(2, new ByteArrayInputStream( + getDataAdapter().toBytes(userData))); statement.executeUpdate(); } - } catch (SQLException | IOException e) { + } catch (SQLException | DataAdaptionException e) { getLogger().log(Level.SEVERE, "Failed to set user data in the database", e); } }).thenRun(() -> 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 index 84ddea52..21e4268f 100644 --- a/common/src/main/java/net/william278/husksync/listener/EventListener.java +++ b/common/src/main/java/net/william278/husksync/listener/EventListener.java @@ -1,9 +1,8 @@ package net.william278.husksync.listener; import net.william278.husksync.HuskSync; +import net.william278.husksync.config.Settings; import net.william278.husksync.player.OnlineUser; -import net.william278.husksync.player.User; -import net.william278.husksync.redis.RedisManager; import org.jetbrains.annotations.NotNull; import java.util.HashSet; @@ -94,7 +93,7 @@ public abstract class EventListener { } public final void handleWorldSave(@NotNull List usersInWorld) { - if (disabling) { + if (disabling || !huskSync.getSettings().getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SAVE_ON_WORLD_SAVE)) { return; } CompletableFuture.runAsync(() -> usersInWorld.forEach(user -> diff --git a/common/src/main/java/net/william278/husksync/redis/RedisManager.java b/common/src/main/java/net/william278/husksync/redis/RedisManager.java index 50d4c116..02ce812e 100644 --- a/common/src/main/java/net/william278/husksync/redis/RedisManager.java +++ b/common/src/main/java/net/william278/husksync/redis/RedisManager.java @@ -1,6 +1,7 @@ package net.william278.husksync.redis; import net.william278.husksync.config.Settings; +import net.william278.husksync.data.DataAdapter; import net.william278.husksync.data.UserData; import net.william278.husksync.player.User; import org.jetbrains.annotations.NotNull; @@ -26,6 +27,7 @@ public class RedisManager { private static String clusterId = ""; private final JedisPoolConfig jedisPoolConfig; + private final DataAdapter dataAdapter; private final String redisHost; private final int redisPort; @@ -34,8 +36,9 @@ public class RedisManager { private JedisPool jedisPool; - public RedisManager(@NotNull Settings settings) { + public RedisManager(@NotNull Settings settings, @NotNull DataAdapter dataAdapter) { clusterId = settings.getStringValue(Settings.ConfigOption.CLUSTER_ID); + this.dataAdapter = dataAdapter; this.redisHost = settings.getStringValue(Settings.ConfigOption.REDIS_HOST); this.redisPort = settings.getIntegerValue(Settings.ConfigOption.REDIS_PORT); this.redisPassword = settings.getStringValue(Settings.ConfigOption.REDIS_PASSWORD); @@ -72,9 +75,8 @@ public class RedisManager { /** * Set a user's data to the Redis server * - * @param user the user to set data for - * @param userData the user's data to set - * @param redisKeyType the type of key to set the data with. This determines the time to live for the data. + * @param user the user to set data for + * @param userData the user's data to set * @return a future returning void when complete */ public CompletableFuture setUserData(@NotNull User user, @NotNull UserData userData) { @@ -82,11 +84,9 @@ public class RedisManager { return CompletableFuture.runAsync(() -> { try (Jedis jedis = jedisPool.getResource()) { // Set the user's data as a compressed byte array of the json using Snappy - jedis.setex(getKey(RedisKeyType.DATA_UPDATE, user.uuid), RedisKeyType.DATA_UPDATE.timeToLive, - Snappy.compress(userData.toJson().getBytes(StandardCharsets.UTF_8))); - System.out.println("Set key at " + new Date().getTime()); - } catch (IOException e) { - throw new RuntimeException(e); + jedis.setex(getKey(RedisKeyType.DATA_UPDATE, user.uuid), + RedisKeyType.DATA_UPDATE.timeToLive, + dataAdapter.toBytes(userData)); } }); } catch (Exception e) { @@ -107,8 +107,7 @@ public class RedisManager { /** * Fetch a user's data from the Redis server and consume the key if found * - * @param user The user to fetch data for - * @param redisKeyType The type of key to fetch + * @param user The user to fetch data for * @return The user's data, if it's present on the database. Otherwise, an empty optional. */ public CompletableFuture> getUserData(@NotNull User user) { @@ -116,18 +115,15 @@ public class RedisManager { try (Jedis jedis = jedisPool.getResource()) { final byte[] key = getKey(RedisKeyType.DATA_UPDATE, user.uuid); System.out.println("Reading key at " + new Date().getTime()); - final byte[] compressedJson = jedis.get(key); - if (compressedJson == null) { + final byte[] dataByteArray = jedis.get(key); + if (dataByteArray == null) { return Optional.empty(); } // Consume the key (delete from redis) jedis.del(key); // Use Snappy to decompress the json - return Optional.of(UserData.fromJson(new String(Snappy.uncompress(compressedJson), - StandardCharsets.UTF_8))); - } catch (IOException e) { - throw new RuntimeException(e); + return Optional.of(dataAdapter.fromBytes(dataByteArray)); } }); } diff --git a/common/src/main/resources/config.yml b/common/src/main/resources/config.yml index 0a851709..161736a4 100644 --- a/common/src/main/resources/config.yml +++ b/common/src/main/resources/config.yml @@ -36,6 +36,7 @@ redis: synchronization: max_user_data_records: 5 save_on_world_save: true + compress_data: true features: inventories: true ender_chests: true