From 1c9d74f925ab705e766675a18ac54fb42e5a4106 Mon Sep 17 00:00:00 2001 From: William Date: Thu, 7 Jul 2022 01:52:19 +0100 Subject: [PATCH] Events & API work, save DataSaveCauses as part of versioning --- .../william278/husksync/api/HuskSyncAPI.java | 177 ++++++++++++++++++ build.gradle | 9 + .../william278/husksync/BukkitHuskSync.java | 35 +++- .../husksync/data/BukkitInventoryMap.java | 125 +++++++++++++ .../husksync/data/BukkitSerializer.java | 31 ++- .../event/BukkitDataSavePlayerEvent.java | 60 ++++++ .../husksync/event/BukkitEvent.java | 25 +++ .../husksync/event/BukkitEventCannon.java | 33 ++++ .../husksync/event/BukkitPlayerEvent.java | 34 ++++ .../husksync/event/BukkitPreSyncEvent.java | 51 +++++ .../event/BukkitSyncCompletePlayerEvent.java | 26 +++ .../listener/BukkitEventListener.java | 7 +- .../husksync/player/BukkitPlayer.java | 117 +++++++----- .../husksync/util/BukkitLogger.java | 16 +- bukkit/src/main/resources/plugin.yml | 6 +- .../net/william278/husksync/HuskSync.java | 3 + .../husksync/command/EchestCommand.java | 69 +++---- .../husksync/command/InvseeCommand.java | 69 +++---- .../william278/husksync/config/Settings.java | 2 + .../william278/husksync/data/DataAdapter.java | 15 +- .../data/DataDeserializationException.java | 2 +- .../husksync/data/DataSaveCause.java | 53 ++++++ .../husksync/data/InventoryData.java | 25 --- .../william278/husksync/data/ItemData.java | 25 +++ .../husksync/data/JsonDataAdapter.java | 7 +- .../husksync/data/PotionEffectData.java | 4 +- .../husksync/data/StatusDataFlag.java | 51 +++++ .../william278/husksync/data/UserData.java | 30 ++- .../husksync/data/VersionedUserData.java | 9 +- .../husksync/database/Database.java | 24 ++- .../husksync/database/MySqlDatabase.java | 50 ++--- .../husksync/editor/DataEditor.java | 17 +- .../husksync/editor/InventoryEditorMenu.java | 24 +-- .../husksync/event/CancellableEvent.java | 9 + .../husksync/event/DataSaveEvent.java | 20 ++ .../net/william278/husksync/event/Event.java | 9 + .../husksync/event/EventCannon.java | 23 +++ .../husksync/event/PlayerEvent.java | 9 + .../husksync/event/PreSyncEvent.java | 13 ++ .../husksync/event/SyncCompleteEvent.java | 5 + .../husksync/listener/EventListener.java | 90 ++++++--- .../husksync/player/OnlineUser.java | 111 +++++------ .../husksync/redis/RedisManager.java | 29 ++- .../net/william278/husksync/util/Logger.java | 26 ++- common/src/main/resources/config.yml | 2 + .../main/resources/database/mysql_schema.sql | 9 +- .../husksync/data/DataAdaptionTests.java | 75 ++++++++ .../husksync/player/DummyPlayer.java | 156 +++++++++++++++ 48 files changed, 1477 insertions(+), 340 deletions(-) create mode 100644 bukkit/src/main/java/net/william278/husksync/data/BukkitInventoryMap.java create mode 100644 bukkit/src/main/java/net/william278/husksync/event/BukkitDataSavePlayerEvent.java create mode 100644 bukkit/src/main/java/net/william278/husksync/event/BukkitEvent.java create mode 100644 bukkit/src/main/java/net/william278/husksync/event/BukkitEventCannon.java create mode 100644 bukkit/src/main/java/net/william278/husksync/event/BukkitPlayerEvent.java create mode 100644 bukkit/src/main/java/net/william278/husksync/event/BukkitPreSyncEvent.java create mode 100644 bukkit/src/main/java/net/william278/husksync/event/BukkitSyncCompletePlayerEvent.java create mode 100644 common/src/main/java/net/william278/husksync/data/DataSaveCause.java delete mode 100644 common/src/main/java/net/william278/husksync/data/InventoryData.java create mode 100644 common/src/main/java/net/william278/husksync/data/ItemData.java create mode 100644 common/src/main/java/net/william278/husksync/data/StatusDataFlag.java create mode 100644 common/src/main/java/net/william278/husksync/event/CancellableEvent.java create mode 100644 common/src/main/java/net/william278/husksync/event/DataSaveEvent.java create mode 100644 common/src/main/java/net/william278/husksync/event/Event.java create mode 100644 common/src/main/java/net/william278/husksync/event/EventCannon.java create mode 100644 common/src/main/java/net/william278/husksync/event/PlayerEvent.java create mode 100644 common/src/main/java/net/william278/husksync/event/PreSyncEvent.java create mode 100644 common/src/main/java/net/william278/husksync/event/SyncCompleteEvent.java create mode 100644 common/src/test/java/net/william278/husksync/data/DataAdaptionTests.java create mode 100644 common/src/test/java/net/william278/husksync/player/DummyPlayer.java diff --git a/api/src/main/java/net/william278/husksync/api/HuskSyncAPI.java b/api/src/main/java/net/william278/husksync/api/HuskSyncAPI.java index ea441fe0..83f24d5e 100644 --- a/api/src/main/java/net/william278/husksync/api/HuskSyncAPI.java +++ b/api/src/main/java/net/william278/husksync/api/HuskSyncAPI.java @@ -1,4 +1,181 @@ package net.william278.husksync.api; +import net.william278.husksync.BukkitHuskSync; +import net.william278.husksync.data.*; +import net.william278.husksync.player.BukkitPlayer; +import net.william278.husksync.player.OnlineUser; +import net.william278.husksync.player.User; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.potion.PotionEffect; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * The HuskSync API for the Bukkit platform, providing methods to access and modify player {@link UserData} held by {@link User}s. + *

+ * Retrieve an instance of the API class via {@link #getInstance()}. + */ +@SuppressWarnings("unused") public class HuskSyncAPI { + + /** + * (Internal use only) - Instance of the API class. + */ + private static final HuskSyncAPI INSTANCE = new HuskSyncAPI(); + /** + * (Internal use only) - Instance of the implementing plugin. + */ + private static final BukkitHuskSync PLUGIN = BukkitHuskSync.getInstance(); + + /** + * (Internal use only) - Constructor. + */ + private HuskSyncAPI() { + } + + /** + * Entrypoint to the HuskSync API - returns an instance of the API + * + * @return instance of the HuskSync API + */ + public static @NotNull HuskSyncAPI getInstance() { + return INSTANCE; + } + + /** + * Returns a {@link User} instance for the given bukkit {@link Player}. + * + * @param player the bukkit player to get the {@link User} instance for + * @return the {@link User} instance for the given bukkit player + */ + @NotNull + public OnlineUser getUser(@NotNull Player player) { + return BukkitPlayer.adapt(player); + } + + /** + * Returns a {@link User} by the given player's account {@link UUID}, if they exist. + * + * @param uuid the unique id of the player to get the {@link User} instance for + * @return future returning the {@link User} instance for the given player's unique id if they exist, otherwise an empty {@link Optional} + * @apiNote The player does not have to be online + */ + public CompletableFuture> getUser(@NotNull UUID uuid) { + return PLUGIN.getDatabase().getUser(uuid); + } + + /** + * Returns a {@link User} by the given player's username (case-insensitive), if they exist. + * + * @param username the username of the {@link User} instance for + * @return future returning the {@link User} instance for the given player's username if they exist, otherwise an empty {@link Optional} + * @apiNote The player does not have to be online, though their username has to be the username + * they had when they last joined the server. + */ + public CompletableFuture> getUser(@NotNull String username) { + return PLUGIN.getDatabase().getUserByName(username); + } + + /** + * Returns a {@link User}'s current {@link UserData} + * + * @param user the {@link User} to get the {@link UserData} for + * @return future returning the {@link UserData} for the given {@link User} if they exist, otherwise an empty {@link Optional} + * @apiNote If the user is not online on the implementing bukkit server, + * the {@link UserData} returned will be their last database-saved UserData.

+ * If the user happens to be online on another server on the network, + * then the {@link UserData} returned here may not be reflective of their actual current UserData. + */ + public CompletableFuture> getUserData(@NotNull User user) { + return CompletableFuture.supplyAsync(() -> { + if (user instanceof OnlineUser) { + return Optional.of(((OnlineUser) user).getUserData().join()); + } else { + return PLUGIN.getDatabase().getCurrentUserData(user).join().map(VersionedUserData::userData); + } + }); + } + + /** + * Returns the saved {@link VersionedUserData} records for the given {@link User} + * + * @param user the {@link User} to get the {@link VersionedUserData} for + * @return future returning a list {@link VersionedUserData} for the given {@link User} if they exist, + * otherwise an empty {@link Optional} + * @apiNote The length of the list of VersionedUserData will correspond to the configured + * {@code max_user_data_records} config option + */ + public CompletableFuture> getSavedUserData(@NotNull User user) { + return CompletableFuture.supplyAsync(() -> PLUGIN.getDatabase().getUserData(user).join()); + } + + /** + * Returns the JSON string representation of the given {@link UserData} + * + * @param userData the {@link UserData} to get the JSON string representation of + * @param prettyPrint whether to pretty print the JSON string + * @return the JSON string representation of the given {@link UserData} + */ + @NotNull + public String getUserDataJson(@NotNull UserData userData, boolean prettyPrint) { + return PLUGIN.getDataAdapter().toJson(userData, prettyPrint); + } + + /** + * Returns a {@link BukkitInventoryMap} for the given {@link User}, containing their current inventory item data + * + * @param user the {@link User} to get the {@link BukkitInventoryMap} for + * @return future returning the {@link BukkitInventoryMap} for the given {@link User} if they exist, + * otherwise an empty {@link Optional} + */ + public CompletableFuture> getPlayerInventory(@NotNull User user) { + return CompletableFuture.supplyAsync(() -> getUserData(user).join() + .map(userData -> BukkitSerializer.deserializeInventory(userData + .getInventoryData().serializedItems).join())); + } + + /** + * Returns the {@link ItemStack}s array contents of the given {@link User}'s Ender Chest data + * + * @param user the {@link User} to get the Ender Chest contents of + * @return future returning the {@link ItemStack} array of Ender Chest items for the user if they exist, + * otherwise an empty {@link Optional} + */ + public CompletableFuture> getPlayerEnderChest(@NotNull User user) { + return CompletableFuture.supplyAsync(() -> getUserData(user).join() + .map(userData -> BukkitSerializer.deserializeItemStackArray(userData + .getEnderChestData().serializedItems).join())); + } + + /** + * Deserialize a Base-64 encoded inventory array string into a {@link ItemStack} array. + * + * @param serializedItemStackArray The Base-64 encoded inventory array string. + * @return The deserialized {@link ItemStack} array. + * @throws DataDeserializationException If an error occurs during deserialization. + */ + public CompletableFuture deserializeItemStackArray(@NotNull String serializedItemStackArray) + throws DataDeserializationException { + return CompletableFuture.supplyAsync(() -> BukkitSerializer + .deserializeItemStackArray(serializedItemStackArray).join()); + } + + /** + * Deserialize a Base-64 encoded potion effect array string into a {@link PotionEffect} array. + * + * @param serializedPotionEffectArray The Base-64 encoded potion effect array string. + * @return The deserialized {@link PotionEffect} array. + * @throws DataDeserializationException If an error occurs during deserialization. + */ + public CompletableFuture deserializePotionEffectArray(@NotNull String serializedPotionEffectArray) + throws DataDeserializationException { + return CompletableFuture.supplyAsync(() -> BukkitSerializer + .deserializePotionEffects(serializedPotionEffectArray).join()); + } + } diff --git a/build.gradle b/build.gradle index 5cccac28..d51f705a 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,15 @@ allprojects { maven { url 'https://jitpack.io' } } + dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.8.2' + } + + test { + useJUnitPlatform() + } + processResources { filter ReplaceTokens as Class, beginToken: '${', endToken: '}', tokens: rootProject.ext.properties diff --git a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java index 376c2aa6..7c5b86ab 100644 --- a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java +++ b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java @@ -15,6 +15,8 @@ import net.william278.husksync.data.JsonDataAdapter; import net.william278.husksync.database.Database; import net.william278.husksync.database.MySqlDatabase; import net.william278.husksync.editor.DataEditor; +import net.william278.husksync.event.BukkitEventCannon; +import net.william278.husksync.event.EventCannon; import net.william278.husksync.listener.BukkitEventListener; import net.william278.husksync.listener.EventListener; import net.william278.husksync.player.BukkitPlayer; @@ -51,12 +53,18 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { private DataEditor dataEditor; + private EventCannon eventCannon; private Settings settings; private Locales locales; private static BukkitHuskSync instance; + /** + * (Internal use only) Returns the instance of the implementing Bukkit plugin + * + * @return the instance of the Bukkit plugin + */ public static BukkitHuskSync getInstance() { return instance; } @@ -90,6 +98,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { getLoggingAdapter().log(Level.INFO, "Loading plugin configuration settings & locales..."); return reload().thenApply(loadedSettings -> { if (loadedSettings) { + logger.showDebugLogs(settings.getBooleanValue(Settings.ConfigOption.DEBUG_LOGGING)); getLoggingAdapter().log(Level.INFO, "Successfully loaded plugin configuration settings & locales"); } else { getLoggingAdapter().log(Level.SEVERE, "Failed to load plugin configuration settings and/or locales"); @@ -106,6 +115,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { } } return succeeded; + }).thenApply(succeeded -> { + // Prepare event cannon + if (succeeded) { + eventCannon = new BukkitEventCannon(); + } + return succeeded; }).thenApply(succeeded -> { // Prepare data editor if (succeeded) { @@ -114,15 +129,14 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { return succeeded; }).thenApply(succeeded -> { // Establish connection to the database - this.database = new MySqlDatabase(settings, resourceReader, logger, dataAdapter); if (succeeded) { + this.database = new MySqlDatabase(settings, resourceReader, logger, dataAdapter, eventCannon); getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the database..."); final CompletableFuture databaseConnectFuture = new CompletableFuture<>(); Bukkit.getScheduler().runTask(this, () -> { 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"); + getLoggingAdapter().log(Level.SEVERE, "Failed to establish a connection to the database. " + "Please check the supplied database credentials in the config file"); databaseConnectFuture.completeAsync(() -> false); return; } @@ -134,13 +148,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { return false; }).thenApply(succeeded -> { // Establish connection to the Redis server - this.redisManager = new RedisManager(settings, dataAdapter); if (succeeded) { + this.redisManager = new RedisManager(settings, dataAdapter, logger); 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"); + getLoggingAdapter().log(Level.SEVERE, "Failed to establish a connection to the Redis server. " + "Please check the supplied Redis credentials in the config file"); return false; } getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the Redis server"); @@ -178,7 +191,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { return succeeded; }).thenApply(succeeded -> { // Check for updates - if (settings.getBooleanValue(Settings.ConfigOption.CHECK_FOR_UPDATES) && succeeded) { + if (succeeded && settings.getBooleanValue(Settings.ConfigOption.CHECK_FOR_UPDATES)) { getLoggingAdapter().log(Level.INFO, "Checking for updates..."); new UpdateChecker(getVersion(), getLoggingAdapter()).logToConsole(); } @@ -186,8 +199,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { }).thenAccept(succeeded -> { // Handle failed initialization if (!succeeded) { - getLoggingAdapter().log(Level.SEVERE, "Failed to initialize HuskSync. " + - "The plugin will now be disabled"); + getLoggingAdapter().log(Level.SEVERE, "Failed to initialize HuskSync. " + "The plugin will now be disabled"); getServer().getPluginManager().disablePlugin(this); } else { getLoggingAdapter().log(Level.INFO, "Successfully enabled HuskSync v" + getVersion()); @@ -237,6 +249,11 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { return dataEditor; } + @Override + public @NotNull EventCannon getEventCannon() { + return eventCannon; + } + @Override public @NotNull Settings getSettings() { return settings; diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitInventoryMap.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitInventoryMap.java new file mode 100644 index 00000000..b336b563 --- /dev/null +++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitInventoryMap.java @@ -0,0 +1,125 @@ +package net.william278.husksync.data; + +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +/** + * A mapped player inventory, providing methods to easily access a player's inventory. + */ +public class BukkitInventoryMap { + + private ItemStack[] contents; + + /** + * Creates a new mapped inventory from the given contents. + * + * @param contents the contents of the inventory + */ + protected BukkitInventoryMap(ItemStack[] contents) { + this.contents = contents; + } + + /** + * Gets the contents of the inventory. + * + * @return the contents of the inventory + */ + public ItemStack[] getContents() { + return contents; + } + + /** + * Set the contents of the inventory. + * + * @param contents the contents of the inventory + */ + public void setContents(ItemStack[] contents) { + this.contents = contents; + } + + /** + * Gets the size of the inventory. + * + * @return the size of the inventory + */ + public int getSize() { + return contents.length; + } + + /** + * Gets the item at the given index. + * + * @param index the index of the item to get + * @return the item at the given index + */ + public Optional getItemAt(int index) { + if (contents.length >= index) { + if (contents[index] == null) { + return Optional.empty(); + } + return Optional.of(contents[index]); + } + return Optional.empty(); + } + + /** + * Sets the item at the given index. + * + * @param itemStack the item to set at the given index + * @param index the index of the item to set + * @throws IllegalArgumentException if the index is out of bounds + */ + public void setItemAt(@NotNull ItemStack itemStack, int index) throws IllegalArgumentException { + contents[index] = itemStack; + } + + /** + * Returns the main inventory contents. + * + * @return the main inventory contents + */ + public ItemStack[] getInventory() { + final ItemStack[] inventory = new ItemStack[36]; + System.arraycopy(contents, 0, inventory, 0, Math.min(contents.length, inventory.length)); + return inventory; + } + + public ItemStack[] getHotbar() { + final ItemStack[] armor = new ItemStack[9]; + for (int i = 0; i <= 9; i++) { + armor[i] = getItemAt(i).orElse(null); + } + return armor; + } + + public Optional getOffHand() { + return getItemAt(40); + } + + public Optional getHelmet() { + return getItemAt(39); + } + + public Optional getChestplate() { + return getItemAt(38); + } + + public Optional getLeggings() { + return getItemAt(37); + } + + public Optional getBoots() { + return getItemAt(36); + } + + public ItemStack[] getArmor() { + final ItemStack[] armor = new ItemStack[4]; + for (int i = 36; i < 40; i++) { + armor[i - 36] = getItemAt(i).orElse(null); + } + return armor; + } + +} 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 6dcd4856..5ae9f8d4 100644 --- a/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java +++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java @@ -4,6 +4,7 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.potion.PotionEffect; import org.bukkit.util.io.BukkitObjectInputStream; import org.bukkit.util.io.BukkitObjectOutputStream; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; @@ -21,7 +22,8 @@ public class BukkitSerializer { * @param inventoryContents The contents of the inventory * @return The serialized inventory contents */ - public static CompletableFuture serializeInventory(ItemStack[] inventoryContents) throws DataDeserializationException { + public static CompletableFuture serializeItemStackArray(ItemStack[] inventoryContents) + throws DataDeserializationException { return CompletableFuture.supplyAsync(() -> { // Return an empty string if there is no inventory item data to serialize if (inventoryContents.length == 0) { @@ -49,20 +51,35 @@ public class BukkitSerializer { } /** - * Returns an array of ItemStacks from serialized inventory data. Note: empty slots will be represented by {@code null} + * Returns a {@link BukkitInventoryMap} from a serialized array of ItemStacks representing the contents of a player's inventory. * - * @param inventoryData The serialized {@link ItemStack[]} array - * @return The inventory contents as an array of {@link ItemStack}s + * @param serializedPlayerInventory The serialized {@link ItemStack[]} inventory array + * @return The deserialized ItemStacks, mapped for convenience as a {@link BukkitInventoryMap} + * @throws DataDeserializationException If the serialized item stack array could not be deserialized */ - public static CompletableFuture deserializeInventory(String inventoryData) throws DataDeserializationException { + public static CompletableFuture deserializeInventory(@NotNull String serializedPlayerInventory) + throws DataDeserializationException { + return CompletableFuture.supplyAsync(() -> new BukkitInventoryMap(deserializeItemStackArray(serializedPlayerInventory).join())); + } + + /** + * Returns an array of ItemStacks from serialized inventory data. + * + * @param serializeItemStackArray The serialized {@link ItemStack[]} array + * @return The deserialized array of {@link ItemStack}s + * @throws DataDeserializationException If the serialized item stack array could not be deserialized + * @implNote Empty slots will be represented by {@code null} + */ + public static CompletableFuture deserializeItemStackArray(String serializeItemStackArray) + throws DataDeserializationException { return CompletableFuture.supplyAsync(() -> { // Return empty array if there is no inventory data (set the player as having an empty inventory) - if (inventoryData.isEmpty()) { + if (serializeItemStackArray.isEmpty()) { return new ItemStack[0]; } // Create a byte input stream to read the serialized data - try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(inventoryData))) { + try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(serializeItemStackArray))) { 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()]; diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitDataSavePlayerEvent.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitDataSavePlayerEvent.java new file mode 100644 index 00000000..3eb12c51 --- /dev/null +++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitDataSavePlayerEvent.java @@ -0,0 +1,60 @@ +package net.william278.husksync.event; + +import net.william278.husksync.data.DataSaveCause; +import net.william278.husksync.data.UserData; +import net.william278.husksync.player.User; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +public class BukkitDataSavePlayerEvent extends BukkitEvent implements DataSaveEvent, Cancellable { + private static final HandlerList HANDLER_LIST = new HandlerList(); + private boolean cancelled = false; + private UserData userData; + private final User user; + private final DataSaveCause saveCause; + + protected BukkitDataSavePlayerEvent(@NotNull User user, @NotNull UserData userData, + @NotNull DataSaveCause saveCause) { + this.user = user; + this.userData = userData; + this.saveCause = saveCause; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + @NotNull + @Override + public User getUser() { + return user; + } + + @Override + public @NotNull UserData getUserData() { + return userData; + } + + @Override + public void setUserData(@NotNull UserData userData) { + this.userData = userData; + } + + @Override + public @NotNull DataSaveCause getSaveCause() { + return saveCause; + } + + @NotNull + @Override + public HandlerList getHandlers() { + return HANDLER_LIST; + } +} diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitEvent.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitEvent.java new file mode 100644 index 00000000..1160449c --- /dev/null +++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitEvent.java @@ -0,0 +1,25 @@ +package net.william278.husksync.event; + +import net.william278.husksync.BukkitHuskSync; +import net.william278.husksync.player.BukkitPlayer; +import net.william278.husksync.player.OnlineUser; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +public abstract class BukkitEvent extends Event implements net.william278.husksync.event.Event { + + @Override + public CompletableFuture fire() { + final CompletableFuture eventFireFuture = new CompletableFuture<>(); + Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> { + Bukkit.getServer().getPluginManager().callEvent(this); + eventFireFuture.complete(this); + }); + return eventFireFuture; + } + +} diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitEventCannon.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitEventCannon.java new file mode 100644 index 00000000..eacdc30e --- /dev/null +++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitEventCannon.java @@ -0,0 +1,33 @@ +package net.william278.husksync.event; + +import net.william278.husksync.data.DataSaveCause; +import net.william278.husksync.data.UserData; +import net.william278.husksync.player.BukkitPlayer; +import net.william278.husksync.player.OnlineUser; +import net.william278.husksync.player.User; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +public class BukkitEventCannon extends EventCannon { + + public BukkitEventCannon() { + } + + @Override + public CompletableFuture firePreSyncEvent(@NotNull OnlineUser user, @NotNull UserData userData) { + return new BukkitPreSyncEvent(((BukkitPlayer) user).getPlayer(), userData).fire(); + } + + @Override + public CompletableFuture fireDataSaveEvent(@NotNull User user, @NotNull UserData userData, + @NotNull DataSaveCause saveCause) { + return new BukkitDataSavePlayerEvent(user, userData, saveCause).fire(); + } + + @Override + public void fireSyncCompleteEvent(@NotNull OnlineUser user) { + new BukkitSyncCompletePlayerEvent(((BukkitPlayer) user).getPlayer()).fire(); + } + +} diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitPlayerEvent.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitPlayerEvent.java new file mode 100644 index 00000000..f889939f --- /dev/null +++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitPlayerEvent.java @@ -0,0 +1,34 @@ +package net.william278.husksync.event; + +import net.william278.husksync.BukkitHuskSync; +import net.william278.husksync.player.BukkitPlayer; +import net.william278.husksync.player.OnlineUser; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +public abstract class BukkitPlayerEvent extends org.bukkit.event.player.PlayerEvent implements PlayerEvent { + + + public BukkitPlayerEvent(@NotNull Player who) { + super(who); + } + + @Override + public OnlineUser getUser() { + return BukkitPlayer.adapt(player); + } + + @Override + public CompletableFuture fire() { + final CompletableFuture eventFireFuture = new CompletableFuture<>(); + Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> { + Bukkit.getServer().getPluginManager().callEvent(this); + eventFireFuture.complete(this); + }); + return eventFireFuture; + } + +} diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitPreSyncEvent.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitPreSyncEvent.java new file mode 100644 index 00000000..4289fc0d --- /dev/null +++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitPreSyncEvent.java @@ -0,0 +1,51 @@ +package net.william278.husksync.event; + +import net.william278.husksync.data.UserData; +import net.william278.husksync.player.BukkitPlayer; +import net.william278.husksync.player.OnlineUser; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEvent, Cancellable { + private static final HandlerList HANDLER_LIST = new HandlerList(); + private boolean cancelled = false; + private UserData userData; + + protected BukkitPreSyncEvent(@NotNull Player player, @NotNull UserData userData) { + super(player); + this.userData = userData; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + @Override + public OnlineUser getUser() { + return BukkitPlayer.adapt(player); + } + + @Override + public @NotNull UserData getUserData() { + return userData; + } + + @Override + public void setUserData(@NotNull UserData userData) { + this.userData = userData; + } + + @NotNull + @Override + public HandlerList getHandlers() { + return HANDLER_LIST; + } +} diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitSyncCompletePlayerEvent.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitSyncCompletePlayerEvent.java new file mode 100644 index 00000000..7e4181c5 --- /dev/null +++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitSyncCompletePlayerEvent.java @@ -0,0 +1,26 @@ +package net.william278.husksync.event; + +import net.william278.husksync.player.BukkitPlayer; +import net.william278.husksync.player.OnlineUser; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +public class BukkitSyncCompletePlayerEvent extends BukkitPlayerEvent implements SyncCompleteEvent { + private static final HandlerList HANDLER_LIST = new HandlerList(); + + protected BukkitSyncCompletePlayerEvent(@NotNull Player player) { + super(player); + } + + @Override + public OnlineUser getUser() { + return BukkitPlayer.adapt(player); + } + + @NotNull + @Override + public HandlerList getHandlers() { + return HANDLER_LIST; + } +} diff --git a/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java b/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java index 83e48a85..e691df8a 100644 --- a/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java +++ b/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java @@ -3,7 +3,7 @@ package net.william278.husksync.listener; import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.data.BukkitSerializer; import net.william278.husksync.data.DataDeserializationException; -import net.william278.husksync.data.InventoryData; +import net.william278.husksync.data.ItemData; import net.william278.husksync.player.BukkitPlayer; import net.william278.husksync.player.OnlineUser; import org.bukkit.Bukkit; @@ -41,7 +41,6 @@ public class BukkitEventListener extends EventListener implements Listener { @EventHandler public void onPlayerQuit(@NotNull PlayerQuitEvent event) { super.handlePlayerQuit(BukkitPlayer.adapt(event.getPlayer())); - BukkitPlayer.remove(event.getPlayer()); } @EventHandler(ignoreCancelled = true) @@ -56,8 +55,8 @@ public class BukkitEventListener extends EventListener implements Listener { final OnlineUser user = BukkitPlayer.adapt(player); if (huskSync.getDataEditor().isEditingInventoryData(user)) { try { - BukkitSerializer.serializeInventory(event.getInventory().getContents()).thenAccept( - serializedInventory -> super.handleMenuClose(user, new InventoryData(serializedInventory))); + BukkitSerializer.serializeItemStackArray(event.getInventory().getContents()).thenAccept( + serializedInventory -> super.handleMenuClose(user, new ItemData(serializedInventory))); } catch (DataDeserializationException e) { huskSync.getLoggingAdapter().log(Level.SEVERE, "Failed to serialize inventory data during menu close", e); diff --git a/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java b/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java index 23e3486a..e6d2d80c 100644 --- a/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java +++ b/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java @@ -31,7 +31,6 @@ import java.util.concurrent.atomic.AtomicReference; */ public class BukkitPlayer extends OnlineUser { - private static final HashMap cachedPlayers = new HashMap<>(); private final Player player; private BukkitPlayer(@NotNull Player player) { @@ -40,16 +39,11 @@ public class BukkitPlayer extends OnlineUser { } public static BukkitPlayer adapt(@NotNull Player player) { - if (cachedPlayers.containsKey(player.getUniqueId())) { - return cachedPlayers.get(player.getUniqueId()); - } - final BukkitPlayer bukkitPlayer = new BukkitPlayer(player); - cachedPlayers.put(player.getUniqueId(), bukkitPlayer); - return bukkitPlayer; + return new BukkitPlayer(player); } - public static void remove(@NotNull Player player) { - cachedPlayers.remove(player.getUniqueId()); + public Player getPlayer() { + return player; } @Override @@ -73,21 +67,18 @@ public class BukkitPlayer extends OnlineUser { @Override public CompletableFuture setStatus(@NotNull StatusData statusData, - final boolean setHealth, final boolean setMaxHealth, - final boolean setHunger, final boolean setExperience, - final boolean setGameMode, final boolean setFlying, - final boolean setSelectedItemSlot) { + @NotNull List statusDataFlags) { return CompletableFuture.runAsync(() -> { double currentMaxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)) .getBaseValue(); - if (setMaxHealth) { + if (statusDataFlags.contains(StatusDataFlag.SET_MAX_HEALTH)) { if (statusData.maxHealth != 0d) { Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)) .setBaseValue(statusData.maxHealth); currentMaxHealth = statusData.maxHealth; } } - if (setHealth) { + if (statusDataFlags.contains(StatusDataFlag.SET_HEALTH)) { final double currentHealth = player.getHealth(); if (statusData.health != currentHealth) { player.setHealth(currentHealth > currentMaxHealth ? currentMaxHealth : statusData.health); @@ -100,24 +91,24 @@ public class BukkitPlayer extends OnlineUser { } player.setHealthScaled(statusData.healthScale != 0D); } - if (setHunger) { + if (statusDataFlags.contains(StatusDataFlag.SET_HUNGER)) { player.setFoodLevel(statusData.hunger); player.setSaturation(statusData.saturation); player.setExhaustion(statusData.saturationExhaustion); } - if (setSelectedItemSlot) { + if (statusDataFlags.contains(StatusDataFlag.SET_SELECTED_ITEM_SLOT)) { player.getInventory().setHeldItemSlot(statusData.selectedItemSlot); } - if (setExperience) { + if (statusDataFlags.contains(StatusDataFlag.SET_EXPERIENCE)) { player.setTotalExperience(statusData.totalExperience); player.setLevel(statusData.expLevel); player.setExp(statusData.expProgress); } - if (setGameMode) { + if (statusDataFlags.contains(StatusDataFlag.SET_GAME_MODE)) { Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> player.setGameMode(GameMode.valueOf(statusData.gameMode))); } - if (setFlying) { + if (statusDataFlags.contains(StatusDataFlag.SET_FLYING)) { Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> { if (statusData.isFlying) { player.setAllowFlight(true); @@ -130,29 +121,39 @@ public class BukkitPlayer extends OnlineUser { } @Override - public CompletableFuture getInventory() { - return BukkitSerializer.serializeInventory(player.getInventory().getContents()) - .thenApply(InventoryData::new); + public CompletableFuture getInventory() { + return BukkitSerializer.serializeItemStackArray(player.getInventory().getContents()) + .thenApply(ItemData::new); } @Override - public CompletableFuture setInventory(@NotNull InventoryData inventoryData) { - return BukkitSerializer.deserializeInventory(inventoryData.serializedInventory).thenAccept(contents -> - Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), - () -> player.getInventory().setContents(contents))); + public CompletableFuture setInventory(@NotNull ItemData itemData) { + return BukkitSerializer.deserializeInventory(itemData.serializedItems).thenApplyAsync(contents -> { + final CompletableFuture inventorySetFuture = new CompletableFuture<>(); + Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> { + player.getInventory().setContents(contents.getContents()); + inventorySetFuture.complete(null); + }); + return inventorySetFuture.join(); + }); } @Override - public CompletableFuture getEnderChest() { - return BukkitSerializer.serializeInventory(player.getEnderChest().getContents()) - .thenApply(InventoryData::new); + public CompletableFuture getEnderChest() { + return BukkitSerializer.serializeItemStackArray(player.getEnderChest().getContents()) + .thenApply(ItemData::new); } @Override - public CompletableFuture setEnderChest(@NotNull InventoryData enderChestData) { - return BukkitSerializer.deserializeInventory(enderChestData.serializedInventory).thenAccept(contents -> - Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), - () -> player.getEnderChest().setContents(contents))); + public CompletableFuture setEnderChest(@NotNull ItemData enderChestData) { + return BukkitSerializer.deserializeItemStackArray(enderChestData.serializedItems).thenApplyAsync(contents -> { + final CompletableFuture enderChestSetFuture = new CompletableFuture<>(); + Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> { + player.getEnderChest().setContents(contents); + enderChestSetFuture.complete(null); + }); + return enderChestSetFuture.join(); + }); } @Override @@ -163,15 +164,20 @@ public class BukkitPlayer extends OnlineUser { @Override public CompletableFuture setPotionEffects(@NotNull PotionEffectData potionEffectData) { - return BukkitSerializer.deserializePotionEffects(potionEffectData.serializedPotionEffects).thenAccept( - effects -> Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> { - for (PotionEffect effect : player.getActivePotionEffects()) { - player.removePotionEffect(effect.getType()); - } - for (PotionEffect effect : effects) { - player.addPotionEffect(effect); - } - })); + return BukkitSerializer.deserializePotionEffects(potionEffectData.serializedPotionEffects) + .thenApplyAsync(effects -> { + final CompletableFuture potionEffectsSetFuture = new CompletableFuture<>(); + Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> { + for (PotionEffect effect : player.getActivePotionEffects()) { + player.removePotionEffect(effect.getType()); + } + for (PotionEffect effect : effects) { + player.addPotionEffect(effect); + } + potionEffectsSetFuture.complete(null); + }); + return potionEffectsSetFuture.join(); + }); } @Override @@ -362,7 +368,7 @@ public class BukkitPlayer extends OnlineUser { @Override public CompletableFuture setLocation(@NotNull LocationData locationData) { - final CompletableFuture completableFuture = new CompletableFuture<>(); + final CompletableFuture teleportFuture = new CompletableFuture<>(); AtomicReference bukkitWorld = new AtomicReference<>(Bukkit.getWorld(locationData.worldName)); if (bukkitWorld.get() == null) { bukkitWorld.set(Bukkit.getWorld(locationData.worldUuid)); @@ -372,12 +378,14 @@ public class BukkitPlayer extends OnlineUser { .valueOf(locationData.worldEnvironment)).findFirst().ifPresent(bukkitWorld::set); } if (bukkitWorld.get() != null) { - player.teleport(new Location(bukkitWorld.get(), - locationData.x, locationData.y, locationData.z, - locationData.yaw, locationData.pitch), PlayerTeleportEvent.TeleportCause.PLUGIN); + Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> { + player.teleport(new Location(bukkitWorld.get(), + locationData.x, locationData.y, locationData.z, + locationData.yaw, locationData.pitch), PlayerTeleportEvent.TeleportCause.PLUGIN); + teleportFuture.complete(null); + }); } - CompletableFuture.runAsync(() -> completableFuture.completeAsync(() -> null)); - return completableFuture; + return teleportFuture; } @Override @@ -413,12 +421,17 @@ public class BukkitPlayer extends OnlineUser { @Override public boolean isDead() { - return player.isDead() || player.getHealth() <= 0; + return player.getHealth() <= 0d; } @Override public boolean isOffline() { - return player == null; + try { + return player == null; + } catch (Exception e) { + e.printStackTrace(); + throw e; + } } @Override @@ -428,7 +441,7 @@ public class BukkitPlayer extends OnlineUser { @Override public void showMenu(@NotNull InventoryEditorMenu menu) { - BukkitSerializer.deserializeInventory(menu.inventoryData.serializedInventory).thenAccept(inventoryContents -> { + BukkitSerializer.deserializeItemStackArray(menu.itemData.serializedItems).thenAccept(inventoryContents -> { final Inventory inventory = Bukkit.createInventory(player, menu.slotCount, BaseComponent.toLegacyText(menu.menuTitle.toComponent())); inventory.setContents(inventoryContents); diff --git a/bukkit/src/main/java/net/william278/husksync/util/BukkitLogger.java b/bukkit/src/main/java/net/william278/husksync/util/BukkitLogger.java index 5bcd0662..c87a5673 100644 --- a/bukkit/src/main/java/net/william278/husksync/util/BukkitLogger.java +++ b/bukkit/src/main/java/net/william278/husksync/util/BukkitLogger.java @@ -1,37 +1,39 @@ package net.william278.husksync.util; +import org.jetbrains.annotations.NotNull; + import java.util.logging.Level; -public class BukkitLogger implements Logger { +public class BukkitLogger extends Logger { private final java.util.logging.Logger logger; - public BukkitLogger(java.util.logging.Logger logger) { + public BukkitLogger(@NotNull java.util.logging.Logger logger) { this.logger = logger; } @Override - public void log(Level level, String message, Exception e) { + public void log(@NotNull Level level, @NotNull String message, @NotNull Exception e) { logger.log(level, message, e); } @Override - public void log(Level level, String message) { + public void log(@NotNull Level level, @NotNull String message) { logger.log(level, message); } @Override - public void info(String message) { + public void info(@NotNull String message) { logger.info(message); } @Override - public void severe(String message) { + public void severe(@NotNull String message) { logger.severe(message); } @Override - public void config(String message) { + public void config(@NotNull String message) { logger.config(message); } diff --git a/bukkit/src/main/resources/plugin.yml b/bukkit/src/main/resources/plugin.yml index 6c5a3c60..651b14b8 100644 --- a/bukkit/src/main/resources/plugin.yml +++ b/bukkit/src/main/resources/plugin.yml @@ -10,4 +10,8 @@ libraries: - 'mysql:mysql-connector-java:8.0.29' commands: husksync: - usage: '/husksync ' \ No newline at end of file + usage: '/husksync ' + invsee: + usage: '/invsee ' + echest: + usage: '/echest ' \ 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 index 56020775..777be7aa 100644 --- a/common/src/main/java/net/william278/husksync/HuskSync.java +++ b/common/src/main/java/net/william278/husksync/HuskSync.java @@ -5,6 +5,7 @@ import net.william278.husksync.config.Settings; import net.william278.husksync.data.DataAdapter; import net.william278.husksync.editor.DataEditor; import net.william278.husksync.database.Database; +import net.william278.husksync.event.EventCannon; import net.william278.husksync.player.OnlineUser; import net.william278.husksync.redis.RedisManager; import net.william278.husksync.util.Logger; @@ -29,6 +30,8 @@ public interface HuskSync { @NotNull DataEditor getDataEditor(); + @NotNull EventCannon getEventCannon(); + @NotNull Settings getSettings(); @NotNull Locales getLocales(); diff --git a/common/src/main/java/net/william278/husksync/command/EchestCommand.java b/common/src/main/java/net/william278/husksync/command/EchestCommand.java index f5212245..7484af58 100644 --- a/common/src/main/java/net/william278/husksync/command/EchestCommand.java +++ b/common/src/main/java/net/william278/husksync/command/EchestCommand.java @@ -3,6 +3,7 @@ package net.william278.husksync.command; import net.william278.husksync.HuskSync; import net.william278.husksync.data.UserData; import net.william278.husksync.data.VersionedUserData; +import net.william278.husksync.data.DataSaveCause; import net.william278.husksync.editor.InventoryEditorMenu; import net.william278.husksync.player.OnlineUser; import org.jetbrains.annotations.NotNull; @@ -24,40 +25,42 @@ public class EchestCommand extends CommandBase { .ifPresent(player::sendMessage); return; } - plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAcceptAsync(optionalUser -> { - optionalUser.ifPresentOrElse(user -> { - List userData = plugin.getDatabase().getUserData(user).join(); - Optional dataToView; - if (args.length == 2) { - try { - final UUID version = UUID.fromString(args[1]); - dataToView = userData.stream().filter(data -> data.versionUUID().equals(version)).findFirst(); - } catch (IllegalArgumentException e) { - plugin.getLocales().getLocale("error_invalid_syntax", - "/echest [version_uuid]").ifPresent(player::sendMessage); - return; + plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAcceptAsync(optionalUser -> + optionalUser.ifPresentOrElse(user -> { + List userData = plugin.getDatabase().getUserData(user).join(); + Optional dataToView; + if (args.length == 2) { + try { + final UUID version = UUID.fromString(args[1]); + dataToView = userData.stream().filter(data -> data.versionUUID().equals(version)).findFirst(); + } catch (IllegalArgumentException e) { + plugin.getLocales().getLocale("error_invalid_syntax", + "/echest [version_uuid]").ifPresent(player::sendMessage); + return; + } + } else { + dataToView = userData.stream().sorted().findFirst(); } - } else { - dataToView = userData.stream().sorted().findFirst(); - } - dataToView.ifPresentOrElse(versionedUserData -> { - final UserData data = versionedUserData.userData(); - final InventoryEditorMenu menu = InventoryEditorMenu.createEnderChestMenu( - data.getEnderChestData(), user, player); - plugin.getLocales().getLocale("viewing_ender_chest_of", user.username) - .ifPresent(player::sendMessage); - plugin.getDataEditor().openInventoryMenu(player, menu).thenAcceptAsync(inventoryDataOnClose -> { - final UserData updatedUserData = new UserData(data.getStatusData(), - data.getInventoryData(), menu.canEdit ? inventoryDataOnClose : data.getEnderChestData(), - data.getPotionEffectData(), data.getAdvancementData(), - data.getStatisticData(), data.getLocationData(), - data.getPersistentDataContainerData()); - plugin.getDatabase().setUserData(user, updatedUserData).join(); - }); - }, () -> plugin.getLocales().getLocale(args.length == 2 ? "error_invalid_version_uuid" - : "error_no_data_to_display").ifPresent(player::sendMessage)); - }, () -> plugin.getLocales().getLocale("error_invalid_player").ifPresent(player::sendMessage)); - }); + dataToView.ifPresentOrElse(versionedUserData -> { + final UserData data = versionedUserData.userData(); + final InventoryEditorMenu menu = InventoryEditorMenu.createEnderChestMenu( + data.getEnderChestData(), user, player); + plugin.getLocales().getLocale("viewing_ender_chest_of", user.username) + .ifPresent(player::sendMessage); + plugin.getDataEditor().openInventoryMenu(player, menu).thenAcceptAsync(inventoryDataOnClose -> { + if (!menu.canEdit) { + return; + } + final UserData updatedUserData = new UserData(data.getStatusData(), + data.getInventoryData(), inventoryDataOnClose, + data.getPotionEffectsData(), data.getAdvancementData(), + data.getStatisticsData(), data.getLocationData(), + data.getPersistentDataContainerData()); + plugin.getDatabase().setUserData(user, updatedUserData, DataSaveCause.ECHEST_COMMAND_EDIT).join(); + }); + }, () -> plugin.getLocales().getLocale(args.length == 2 ? "error_invalid_version_uuid" + : "error_no_data_to_display").ifPresent(player::sendMessage)); + }, () -> plugin.getLocales().getLocale("error_invalid_player").ifPresent(player::sendMessage))); } } diff --git a/common/src/main/java/net/william278/husksync/command/InvseeCommand.java b/common/src/main/java/net/william278/husksync/command/InvseeCommand.java index e94a0a82..3f667ed6 100644 --- a/common/src/main/java/net/william278/husksync/command/InvseeCommand.java +++ b/common/src/main/java/net/william278/husksync/command/InvseeCommand.java @@ -3,6 +3,7 @@ package net.william278.husksync.command; import net.william278.husksync.HuskSync; import net.william278.husksync.data.UserData; import net.william278.husksync.data.VersionedUserData; +import net.william278.husksync.data.DataSaveCause; import net.william278.husksync.editor.InventoryEditorMenu; import net.william278.husksync.player.OnlineUser; import org.jetbrains.annotations.NotNull; @@ -24,40 +25,42 @@ public class InvseeCommand extends CommandBase { .ifPresent(player::sendMessage); return; } - plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAcceptAsync(optionalUser -> { - optionalUser.ifPresentOrElse(user -> { - List userData = plugin.getDatabase().getUserData(user).join(); - Optional dataToView; - if (args.length == 2) { - try { - final UUID version = UUID.fromString(args[1]); - dataToView = userData.stream().filter(data -> data.versionUUID().equals(version)).findFirst(); - } catch (IllegalArgumentException e) { - plugin.getLocales().getLocale("error_invalid_syntax", - "/invsee [version_uuid]").ifPresent(player::sendMessage); - return; + plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAcceptAsync(optionalUser -> + optionalUser.ifPresentOrElse(user -> { + List userData = plugin.getDatabase().getUserData(user).join(); + Optional dataToView; + if (args.length == 2) { + try { + final UUID version = UUID.fromString(args[1]); + dataToView = userData.stream().filter(data -> data.versionUUID().equals(version)).findFirst(); + } catch (IllegalArgumentException e) { + plugin.getLocales().getLocale("error_invalid_syntax", + "/invsee [version_uuid]").ifPresent(player::sendMessage); + return; + } + } else { + dataToView = userData.stream().sorted().findFirst(); } - } else { - dataToView = userData.stream().sorted().findFirst(); - } - dataToView.ifPresentOrElse(versionedUserData -> { - final UserData data = versionedUserData.userData(); - final InventoryEditorMenu menu = InventoryEditorMenu.createInventoryMenu( - data.getInventoryData(), user, player); - plugin.getLocales().getLocale("viewing_inventory_of", user.username) - .ifPresent(player::sendMessage); - plugin.getDataEditor().openInventoryMenu(player, menu).thenAcceptAsync(inventoryDataOnClose -> { - final UserData updatedUserData = new UserData(data.getStatusData(), - menu.canEdit ? inventoryDataOnClose : data.getInventoryData(), - data.getEnderChestData(), data.getPotionEffectData(), data.getAdvancementData(), - data.getStatisticData(), data.getLocationData(), - data.getPersistentDataContainerData()); - plugin.getDatabase().setUserData(user, updatedUserData).join(); - }); - }, () -> plugin.getLocales().getLocale(args.length == 2 ? "error_invalid_version_uuid" - : "error_no_data_to_display").ifPresent(player::sendMessage)); - }, () -> plugin.getLocales().getLocale("error_invalid_player").ifPresent(player::sendMessage)); - }); + dataToView.ifPresentOrElse(versionedUserData -> { + final UserData data = versionedUserData.userData(); + final InventoryEditorMenu menu = InventoryEditorMenu.createInventoryMenu( + data.getInventoryData(), user, player); + plugin.getLocales().getLocale("viewing_inventory_of", user.username) + .ifPresent(player::sendMessage); + plugin.getDataEditor().openInventoryMenu(player, menu).thenAcceptAsync(inventoryDataOnClose -> { + if (!menu.canEdit) { + return; + } + final UserData updatedUserData = new UserData(data.getStatusData(), + inventoryDataOnClose, + data.getEnderChestData(), data.getPotionEffectsData(), data.getAdvancementData(), + data.getStatisticsData(), data.getLocationData(), + data.getPersistentDataContainerData()); + plugin.getDatabase().setUserData(user, updatedUserData, DataSaveCause.INVSEE_COMMAND_EDIT).join(); + }); + }, () -> plugin.getLocales().getLocale(args.length == 2 ? "error_invalid_version_uuid" + : "error_no_data_to_display").ifPresent(player::sendMessage)); + }, () -> plugin.getLocales().getLocale("error_invalid_player").ifPresent(player::sendMessage))); } } 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 2f8e6c7d..d9de3755 100644 --- a/common/src/main/java/net/william278/husksync/config/Settings.java +++ b/common/src/main/java/net/william278/husksync/config/Settings.java @@ -119,6 +119,7 @@ public class Settings { CHECK_FOR_UPDATES("check_for_updates", OptionType.BOOLEAN, true), CLUSTER_ID("cluster_id", OptionType.STRING, ""), + DEBUG_LOGGING("debug_logging", OptionType.BOOLEAN, true), DATABASE_HOST("database.credentials.host", OptionType.STRING, "localhost"), DATABASE_PORT("database.credentials.port", OptionType.INTEGER, 3306), @@ -142,6 +143,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_NETWORK_LATENCY_MILLISECONDS("synchronization.network_latency_milliseconds", OptionType.INTEGER, 500), 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/DataAdapter.java b/common/src/main/java/net/william278/husksync/data/DataAdapter.java index 1ba2fa33..4384f67c 100644 --- a/common/src/main/java/net/william278/husksync/data/DataAdapter.java +++ b/common/src/main/java/net/william278/husksync/data/DataAdapter.java @@ -8,14 +8,25 @@ import org.jetbrains.annotations.NotNull; public interface DataAdapter { /** - * Converts {@link UserData} to a byte array. + * Converts {@link UserData} to a byte array * - * @param data The {@link UserData} to adapt. + * @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; + /** + * Serializes {@link UserData} to a JSON string. + * + * @param data The {@link UserData} to serialize + * @param pretty Whether to pretty print the JSON. + * @return The output json string. + * @throws DataAdaptionException If an error occurred during adaptation. + */ + @NotNull + String toJson(@NotNull UserData data, boolean pretty) throws DataAdaptionException; + /** * Converts a byte array to {@link UserData}. * diff --git a/common/src/main/java/net/william278/husksync/data/DataDeserializationException.java b/common/src/main/java/net/william278/husksync/data/DataDeserializationException.java index 6812b37f..a529ef1e 100644 --- a/common/src/main/java/net/william278/husksync/data/DataDeserializationException.java +++ b/common/src/main/java/net/william278/husksync/data/DataDeserializationException.java @@ -3,7 +3,7 @@ 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 + * For example, an exception deserializing {@link ItemData} item stack or {@link PotionEffectData} potion effect arrays */ public class DataDeserializationException extends RuntimeException { protected DataDeserializationException(String message, Throwable cause) { diff --git a/common/src/main/java/net/william278/husksync/data/DataSaveCause.java b/common/src/main/java/net/william278/husksync/data/DataSaveCause.java new file mode 100644 index 00000000..c833fc4a --- /dev/null +++ b/common/src/main/java/net/william278/husksync/data/DataSaveCause.java @@ -0,0 +1,53 @@ +package net.william278.husksync.data; + +import org.jetbrains.annotations.NotNull; + +/** + * Identifies the cause of a player data save. + * + * @implNote This enum is saved in the database. Cause names have a max length of 32 characters. + */ +public enum DataSaveCause { + + /** + * Indicates data saved when a player disconnected from the server (either to change servers, or to log off) + */ + DISCONNECT, + /** + * Indicates data saved when the world saved + */ + WORLD_SAVE, + /** + * Indicates data saved when the server shut down + */ + SERVER_SHUTDOWN, + /** + * Indicates data was saved by editing inventory contents via the {@code /invsee} command + */ + INVSEE_COMMAND_EDIT, + /** + * Indicates data was saved by editing Ender Chest contents via the {@code /echest} command + */ + ECHEST_COMMAND_EDIT, + /** + * Indicates data was saved by an API call + */ + API, + /** + * Indicates data was saved by an unknown cause. + *

+ * This should not be used and is only used for error handling purposes. + */ + UNKNOWN; + + @NotNull + public static DataSaveCause getCauseByName(@NotNull String name) { + for (DataSaveCause cause : values()) { + if (cause.name().equalsIgnoreCase(name)) { + return cause; + } + } + return UNKNOWN; + } + +} diff --git a/common/src/main/java/net/william278/husksync/data/InventoryData.java b/common/src/main/java/net/william278/husksync/data/InventoryData.java deleted file mode 100644 index f0ac1b2e..00000000 --- a/common/src/main/java/net/william278/husksync/data/InventoryData.java +++ /dev/null @@ -1,25 +0,0 @@ -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; - } - - @SuppressWarnings("unused") - protected InventoryData() { - } - -} diff --git a/common/src/main/java/net/william278/husksync/data/ItemData.java b/common/src/main/java/net/william278/husksync/data/ItemData.java new file mode 100644 index 00000000..1543e855 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/data/ItemData.java @@ -0,0 +1,25 @@ +package net.william278.husksync.data; + +import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; + +/** + * Stores information about the contents of a player's inventory or Ender Chest. + */ +public class ItemData { + + /** + * A Base-64 string of platform-serialized items + */ + @SerializedName("serialized_items") + public String serializedItems; + + public ItemData(@NotNull final String serializedItems) { + this.serializedItems = serializedItems; + } + + @SuppressWarnings("unused") + protected ItemData() { + } + +} diff --git a/common/src/main/java/net/william278/husksync/data/JsonDataAdapter.java b/common/src/main/java/net/william278/husksync/data/JsonDataAdapter.java index f3b1e6b2..b2d131d4 100644 --- a/common/src/main/java/net/william278/husksync/data/JsonDataAdapter.java +++ b/common/src/main/java/net/william278/husksync/data/JsonDataAdapter.java @@ -10,7 +10,12 @@ public class JsonDataAdapter implements DataAdapter { @Override public byte[] toBytes(@NotNull UserData data) throws DataAdaptionException { - return new GsonBuilder().create().toJson(data).getBytes(StandardCharsets.UTF_8); + return toJson(data, false).getBytes(StandardCharsets.UTF_8); + } + + @Override + public @NotNull String toJson(@NotNull UserData data, boolean pretty) throws DataAdaptionException { + return (pretty ? new GsonBuilder().setPrettyPrinting() : new GsonBuilder()).create().toJson(data); } @Override 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 cf8662a6..15bdd70f 100644 --- a/common/src/main/java/net/william278/husksync/data/PotionEffectData.java +++ b/common/src/main/java/net/william278/husksync/data/PotionEffectData.java @@ -11,8 +11,8 @@ public class PotionEffectData { @SerializedName("serialized_potion_effects") public String serializedPotionEffects; - public PotionEffectData(@NotNull final String serializedInventory) { - this.serializedPotionEffects = serializedInventory; + public PotionEffectData(@NotNull final String serializedPotionEffects) { + this.serializedPotionEffects = serializedPotionEffects; } @SuppressWarnings("unused") diff --git a/common/src/main/java/net/william278/husksync/data/StatusDataFlag.java b/common/src/main/java/net/william278/husksync/data/StatusDataFlag.java new file mode 100644 index 00000000..60a94934 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/data/StatusDataFlag.java @@ -0,0 +1,51 @@ +package net.william278.husksync.data; + +import net.william278.husksync.config.Settings; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.List; + +/** + * Flags for setting {@link StatusData}, indicating which elements should be synced + */ +public enum StatusDataFlag { + + SET_HEALTH(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HEALTH), + SET_MAX_HEALTH(Settings.ConfigOption.SYNCHRONIZATION_SYNC_MAX_HEALTH), + SET_HUNGER(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HUNGER), + SET_EXPERIENCE(Settings.ConfigOption.SYNCHRONIZATION_SYNC_EXPERIENCE), + SET_GAME_MODE(Settings.ConfigOption.SYNCHRONIZATION_SYNC_GAME_MODE), + SET_FLYING(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION), + SET_SELECTED_ITEM_SLOT(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES); + + private final Settings.ConfigOption configOption; + + StatusDataFlag(@NotNull Settings.ConfigOption configOption) { + this.configOption = configOption; + } + + /** + * Returns all status data flags + * + * @return all status data flags as a list + */ + @NotNull + @SuppressWarnings("unused") + public static List getAll() { + return Arrays.stream(StatusDataFlag.values()).toList(); + } + + /** + * Returns all status data flags that are enabled for setting as per the {@link Settings} + * + * @param settings the settings to use for determining which flags are enabled + * @return all status data flags that are enabled for setting + */ + @NotNull + public static List getFromSettings(@NotNull Settings settings) { + return Arrays.stream(StatusDataFlag.values()).filter( + flag -> settings.getBooleanValue(flag.configOption)).toList(); + } + +} 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 564dad04..593b9007 100644 --- a/common/src/main/java/net/william278/husksync/data/UserData.java +++ b/common/src/main/java/net/william278/husksync/data/UserData.java @@ -10,6 +10,13 @@ import java.util.List; */ public class UserData { + /** + * Indicates the version of the {@link UserData} format being used. + *

+ * This value is to be incremented whenever the format changes. + */ + private static final int CURRENT_FORMAT_VERSION = 1; + /** * Stores the user's status data, including health, food, etc. */ @@ -20,13 +27,13 @@ public class UserData { * Stores the user's inventory contents */ @SerializedName("inventory") - protected InventoryData inventoryData; + protected ItemData inventoryData; /** * Stores the user's ender chest contents */ @SerializedName("ender_chest") - protected InventoryData enderChestData; + protected ItemData enderChestData; /** * Store's the user's potion effects @@ -58,8 +65,14 @@ public class UserData { @SerializedName("persistent_data_container") protected PersistentDataContainerData persistentDataContainerData; - public UserData(@NotNull StatusData statusData, @NotNull InventoryData inventoryData, - @NotNull InventoryData enderChestData, @NotNull PotionEffectData potionEffectData, + /** + * Stores the version of the data format being used + */ + @SerializedName("format_version") + protected int formatVersion; + + public UserData(@NotNull StatusData statusData, @NotNull ItemData inventoryData, + @NotNull ItemData enderChestData, @NotNull PotionEffectData potionEffectData, @NotNull List advancementData, @NotNull StatisticsData statisticData, @NotNull LocationData locationData, @NotNull PersistentDataContainerData persistentDataContainerData) { this.statusData = statusData; @@ -70,6 +83,7 @@ public class UserData { this.statisticData = statisticData; this.locationData = locationData; this.persistentDataContainerData = persistentDataContainerData; + this.formatVersion = CURRENT_FORMAT_VERSION; } // Empty constructor to facilitate json serialization @@ -81,15 +95,15 @@ public class UserData { return statusData; } - public InventoryData getInventoryData() { + public ItemData getInventoryData() { return inventoryData; } - public InventoryData getEnderChestData() { + public ItemData getEnderChestData() { return enderChestData; } - public PotionEffectData getPotionEffectData() { + public PotionEffectData getPotionEffectsData() { return potionEffectData; } @@ -97,7 +111,7 @@ public class UserData { return advancementData; } - public StatisticsData getStatisticData() { + public StatisticsData getStatisticsData() { return statisticData; } diff --git a/common/src/main/java/net/william278/husksync/data/VersionedUserData.java b/common/src/main/java/net/william278/husksync/data/VersionedUserData.java index e8da5b48..00ebab88 100644 --- a/common/src/main/java/net/william278/husksync/data/VersionedUserData.java +++ b/common/src/main/java/net/william278/husksync/data/VersionedUserData.java @@ -6,17 +6,20 @@ import java.util.Date; import java.util.UUID; /** - * Represents a uniquely versioned and timestamped snapshot of a user's data + * Represents a uniquely versioned and timestamped snapshot of a user's data, including why it was saved. * * @param versionUUID The unique identifier for this user data version * @param versionTimestamp An epoch milliseconds timestamp of when this data was created * @param userData The {@link UserData} that has been versioned + * @param cause The {@link DataSaveCause} that caused this data to be saved */ public record VersionedUserData(@NotNull UUID versionUUID, @NotNull Date versionTimestamp, - @NotNull UserData userData) implements Comparable { + @NotNull DataSaveCause cause, @NotNull UserData userData) implements Comparable { /** * Version {@link UserData} into a {@link VersionedUserData}, assigning it a random {@link UUID} and the current timestamp {@link Date} + *

+ * Note that this method will set {@code cause} to {@link DataSaveCause#API} * * @param userData The {@link UserData} to version * @return A new {@link VersionedUserData} @@ -24,7 +27,7 @@ public record VersionedUserData(@NotNull UUID versionUUID, @NotNull Date version * Database implementations should instead use their own UUID generation functions. */ public static VersionedUserData version(@NotNull UserData userData) { - return new VersionedUserData(UUID.randomUUID(), new Date(), userData); + return new VersionedUserData(UUID.randomUUID(), new Date(), DataSaveCause.API, userData); } /** 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 25b4b07d..c8a2033a 100644 --- a/common/src/main/java/net/william278/husksync/database/Database.java +++ b/common/src/main/java/net/william278/husksync/database/Database.java @@ -1,8 +1,10 @@ package net.william278.husksync.database; import net.william278.husksync.data.DataAdapter; +import net.william278.husksync.data.DataSaveCause; import net.william278.husksync.data.UserData; import net.william278.husksync.data.VersionedUserData; +import net.william278.husksync.event.EventCannon; import net.william278.husksync.player.User; import net.william278.husksync.util.Logger; import net.william278.husksync.util.ResourceReader; @@ -51,6 +53,20 @@ public abstract class Database { return dataAdapter; } + /** + * {@link EventCannon} implementation used for firing events + */ + private final EventCannon eventCannon; + + /** + * Returns the {@link EventCannon} used to fire events + * + * @return instance of the {@link EventCannon} implementation + */ + protected EventCannon getEventCannon() { + return eventCannon; + } + /** * Logger instance used for database error logging */ @@ -71,12 +87,14 @@ public abstract class Database { private final ResourceReader resourceReader; protected Database(@NotNull String playerTableName, @NotNull String dataTableName, final int maxUserDataRecords, - @NotNull ResourceReader resourceReader, @NotNull DataAdapter dataAdapter, @NotNull Logger logger) { + @NotNull ResourceReader resourceReader, @NotNull DataAdapter dataAdapter, + @NotNull EventCannon eventCannon, @NotNull Logger logger) { this.playerTableName = playerTableName; this.dataTableName = dataTableName; this.maxUserDataRecords = maxUserDataRecords; this.resourceReader = resourceReader; this.dataAdapter = dataAdapter; + this.eventCannon = eventCannon; this.logger = logger; } @@ -159,7 +177,7 @@ public abstract class Database { protected abstract CompletableFuture pruneUserDataRecords(@NotNull User user); /** - * Add user data to the database

+ * Save 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 @@ -167,7 +185,7 @@ public abstract class Database { * @return A future returning void when complete * @see VersionedUserData#version(UserData) */ - public abstract CompletableFuture setUserData(@NotNull User user, @NotNull UserData userData); + public abstract CompletableFuture setUserData(@NotNull User user, @NotNull UserData userData, @NotNull DataSaveCause dataSaveCause); /** * Close the database connection 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 1dd9c0d8..8f9c34cf 100644 --- a/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java +++ b/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java @@ -2,19 +2,16 @@ 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.data.*; +import net.william278.husksync.event.DataSaveEvent; +import net.william278.husksync.event.EventCannon; import net.william278.husksync.player.User; import net.william278.husksync.util.Logger; import net.william278.husksync.util.ResourceReader; import org.jetbrains.annotations.NotNull; -import org.xerial.snappy.Snappy; import java.io.ByteArrayInputStream; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.sql.*; import java.util.*; import java.util.Date; @@ -55,11 +52,11 @@ public class MySqlDatabase extends Database { private HikariDataSource connectionPool; public MySqlDatabase(@NotNull Settings settings, @NotNull ResourceReader resourceReader, @NotNull Logger logger, - @NotNull DataAdapter dataAdapter) { + @NotNull DataAdapter dataAdapter, @NotNull EventCannon eventCannon) { 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, dataAdapter, logger); + resourceReader, dataAdapter, eventCannon, 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); @@ -213,7 +210,7 @@ public class MySqlDatabase extends Database { return CompletableFuture.supplyAsync(() -> { try (Connection connection = getConnection()) { try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" - SELECT `version_uuid`, `timestamp`, `data` + SELECT `version_uuid`, `timestamp`, `save_cause`, `data` FROM `%data_table%` WHERE `player_uuid`=? ORDER BY `timestamp` DESC @@ -227,6 +224,7 @@ public class MySqlDatabase extends Database { return Optional.of(new VersionedUserData( UUID.fromString(resultSet.getString("version_uuid")), Date.from(resultSet.getTimestamp("timestamp").toInstant()), + DataSaveCause.getCauseByName(resultSet.getString("save_cause")), getDataAdapter().fromBytes(dataByteArray))); } } @@ -243,7 +241,7 @@ public class MySqlDatabase extends Database { final List retrievedData = new ArrayList<>(); try (Connection connection = getConnection()) { try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" - SELECT `version_uuid`, `timestamp`, `data` + SELECT `version_uuid`, `timestamp`, `save_cause`, `data` FROM `%data_table%` WHERE `player_uuid`=? ORDER BY `timestamp` DESC;"""))) { @@ -256,6 +254,7 @@ public class MySqlDatabase extends Database { final VersionedUserData data = new VersionedUserData( UUID.fromString(resultSet.getString("version_uuid")), Date.from(resultSet.getTimestamp("timestamp").toInstant()), + DataSaveCause.getCauseByName(resultSet.getString("save_cause")), getDataAdapter().fromBytes(dataByteArray)); retrievedData.add(data); } @@ -290,20 +289,27 @@ public class MySqlDatabase extends Database { } @Override - public CompletableFuture setUserData(@NotNull User user, @NotNull UserData userData) { + public CompletableFuture setUserData(@NotNull User user, @NotNull UserData userData, + @NotNull DataSaveCause saveCause) { return CompletableFuture.runAsync(() -> { - try (Connection connection = getConnection()) { - try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" - INSERT INTO `%data_table%` - (`player_uuid`,`version_uuid`,`timestamp`,`data`) - VALUES (?,UUID(),NOW(),?);"""))) { - statement.setString(1, user.uuid.toString()); - statement.setBlob(2, new ByteArrayInputStream( - getDataAdapter().toBytes(userData))); - statement.executeUpdate(); + final DataSaveEvent dataSaveEvent = (DataSaveEvent) getEventCannon().fireDataSaveEvent(user, + userData, saveCause).join(); + if (!dataSaveEvent.isCancelled()) { + final UserData finalData = dataSaveEvent.getUserData(); + try (Connection connection = getConnection()) { + try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" + INSERT INTO `%data_table%` + (`player_uuid`,`version_uuid`,`timestamp`,`save_cause`,`data`) + VALUES (?,UUID(),NOW(),?,?);"""))) { + statement.setString(1, user.uuid.toString()); + statement.setString(2, saveCause.name()); + statement.setBlob(3, new ByteArrayInputStream( + getDataAdapter().toBytes(finalData))); + statement.executeUpdate(); + } + } catch (SQLException | DataAdaptionException e) { + getLogger().log(Level.SEVERE, "Failed to set user data in the database", 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/editor/DataEditor.java b/common/src/main/java/net/william278/husksync/editor/DataEditor.java index c0cf35ae..fe7287f6 100644 --- a/common/src/main/java/net/william278/husksync/editor/DataEditor.java +++ b/common/src/main/java/net/william278/husksync/editor/DataEditor.java @@ -1,7 +1,6 @@ package net.william278.husksync.editor; -import net.william278.husksync.config.Locales; -import net.william278.husksync.data.InventoryData; +import net.william278.husksync.data.ItemData; import net.william278.husksync.data.VersionedUserData; import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.User; @@ -32,11 +31,11 @@ public class DataEditor { * @param user The online user to open the editor for * @param inventoryEditorMenu The {@link InventoryEditorMenu} to open * @return The inventory editor menu - * @see InventoryEditorMenu#createInventoryMenu(InventoryData, User, OnlineUser) - * @see InventoryEditorMenu#createEnderChestMenu(InventoryData, User, OnlineUser) + * @see InventoryEditorMenu#createInventoryMenu(ItemData, User, OnlineUser) + * @see InventoryEditorMenu#createEnderChestMenu(ItemData, User, OnlineUser) */ - public CompletableFuture openInventoryMenu(@NotNull OnlineUser user, - @NotNull InventoryEditorMenu inventoryEditorMenu) { + public CompletableFuture openInventoryMenu(@NotNull OnlineUser user, + @NotNull InventoryEditorMenu inventoryEditorMenu) { this.openInventoryMenus.put(user.uuid, inventoryEditorMenu); return inventoryEditorMenu.showInventory(user); } @@ -45,11 +44,11 @@ public class DataEditor { * Close an inventory or ender chest editor menu * * @param user The online user to close the editor for - * @param inventoryData the {@link InventoryData} contained within the menu at the time of closing + * @param itemData the {@link ItemData} contained within the menu at the time of closing */ - public void closeInventoryMenu(@NotNull OnlineUser user, @NotNull InventoryData inventoryData) { + public void closeInventoryMenu(@NotNull OnlineUser user, @NotNull ItemData itemData) { if (this.openInventoryMenus.containsKey(user.uuid)) { - this.openInventoryMenus.get(user.uuid).closeInventory(inventoryData); + this.openInventoryMenus.get(user.uuid).closeInventory(itemData); } this.openInventoryMenus.remove(user.uuid); } diff --git a/common/src/main/java/net/william278/husksync/editor/InventoryEditorMenu.java b/common/src/main/java/net/william278/husksync/editor/InventoryEditorMenu.java index 3a66ca3e..1e097643 100644 --- a/common/src/main/java/net/william278/husksync/editor/InventoryEditorMenu.java +++ b/common/src/main/java/net/william278/husksync/editor/InventoryEditorMenu.java @@ -2,7 +2,7 @@ package net.william278.husksync.editor; import de.themoep.minedown.MineDown; import net.william278.husksync.command.Permission; -import net.william278.husksync.data.InventoryData; +import net.william278.husksync.data.ItemData; import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.User; import org.jetbrains.annotations.NotNull; @@ -11,41 +11,41 @@ import java.util.concurrent.CompletableFuture; public class InventoryEditorMenu { - public final InventoryData inventoryData; + public final ItemData itemData; public final int slotCount; public final MineDown menuTitle; public final boolean canEdit; - private CompletableFuture inventoryDataCompletableFuture; + private CompletableFuture inventoryDataCompletableFuture; - private InventoryEditorMenu(@NotNull InventoryData inventoryData, int slotCount, + private InventoryEditorMenu(@NotNull ItemData itemData, int slotCount, @NotNull MineDown menuTitle, boolean canEdit) { - this.inventoryData = inventoryData; + this.itemData = itemData; this.menuTitle = menuTitle; this.slotCount = slotCount; this.canEdit = canEdit; } - public CompletableFuture showInventory(@NotNull OnlineUser user) { + public CompletableFuture showInventory(@NotNull OnlineUser user) { inventoryDataCompletableFuture = new CompletableFuture<>(); user.showMenu(this); return inventoryDataCompletableFuture; } - public void closeInventory(@NotNull InventoryData inventoryData) { - inventoryDataCompletableFuture.completeAsync(() -> inventoryData); + public void closeInventory(@NotNull ItemData itemData) { + inventoryDataCompletableFuture.completeAsync(() -> itemData); } - public static InventoryEditorMenu createInventoryMenu(@NotNull InventoryData inventoryData, @NotNull User dataOwner, + public static InventoryEditorMenu createInventoryMenu(@NotNull ItemData itemData, @NotNull User dataOwner, @NotNull OnlineUser viewer) { - return new InventoryEditorMenu(inventoryData, 45, + return new InventoryEditorMenu(itemData, 45, new MineDown(dataOwner.username + "'s Inventory"), viewer.hasPermission(Permission.COMMAND_EDIT_INVENTORIES.node)); } - public static InventoryEditorMenu createEnderChestMenu(@NotNull InventoryData inventoryData, @NotNull User dataOwner, + public static InventoryEditorMenu createEnderChestMenu(@NotNull ItemData itemData, @NotNull User dataOwner, @NotNull OnlineUser viewer) { - return new InventoryEditorMenu(inventoryData, 27, + return new InventoryEditorMenu(itemData, 27, new MineDown(dataOwner.username + "'s Ender Chest"), viewer.hasPermission(Permission.COMMAND_EDIT_ENDER_CHESTS.node)); } diff --git a/common/src/main/java/net/william278/husksync/event/CancellableEvent.java b/common/src/main/java/net/william278/husksync/event/CancellableEvent.java new file mode 100644 index 00000000..c8f0cba5 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/event/CancellableEvent.java @@ -0,0 +1,9 @@ +package net.william278.husksync.event; + +public interface CancellableEvent extends Event { + + boolean isCancelled(); + + void setCancelled(boolean cancelled); + +} diff --git a/common/src/main/java/net/william278/husksync/event/DataSaveEvent.java b/common/src/main/java/net/william278/husksync/event/DataSaveEvent.java new file mode 100644 index 00000000..b23b50a4 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/event/DataSaveEvent.java @@ -0,0 +1,20 @@ +package net.william278.husksync.event; + +import net.william278.husksync.data.DataSaveCause; +import net.william278.husksync.data.UserData; +import net.william278.husksync.player.User; +import org.jetbrains.annotations.NotNull; + +public interface DataSaveEvent extends CancellableEvent { + + @NotNull + UserData getUserData(); + + void setUserData(@NotNull UserData userData); + + @NotNull User getUser(); + + @NotNull + DataSaveCause getSaveCause(); + +} diff --git a/common/src/main/java/net/william278/husksync/event/Event.java b/common/src/main/java/net/william278/husksync/event/Event.java new file mode 100644 index 00000000..afee122d --- /dev/null +++ b/common/src/main/java/net/william278/husksync/event/Event.java @@ -0,0 +1,9 @@ +package net.william278.husksync.event; + +import java.util.concurrent.CompletableFuture; + +public interface Event { + + CompletableFuture fire(); + +} diff --git a/common/src/main/java/net/william278/husksync/event/EventCannon.java b/common/src/main/java/net/william278/husksync/event/EventCannon.java new file mode 100644 index 00000000..dd6c7e7e --- /dev/null +++ b/common/src/main/java/net/william278/husksync/event/EventCannon.java @@ -0,0 +1,23 @@ +package net.william278.husksync.event; + +import net.william278.husksync.data.DataSaveCause; +import net.william278.husksync.data.UserData; +import net.william278.husksync.player.OnlineUser; +import net.william278.husksync.player.User; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +public abstract class EventCannon { + + protected EventCannon() { + } + + public abstract CompletableFuture firePreSyncEvent(@NotNull OnlineUser user, @NotNull UserData userData); + + public abstract CompletableFuture fireDataSaveEvent(@NotNull User user, @NotNull UserData userData, + @NotNull DataSaveCause saveCause); + + public abstract void fireSyncCompleteEvent(@NotNull OnlineUser user); + +} diff --git a/common/src/main/java/net/william278/husksync/event/PlayerEvent.java b/common/src/main/java/net/william278/husksync/event/PlayerEvent.java new file mode 100644 index 00000000..78718a62 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/event/PlayerEvent.java @@ -0,0 +1,9 @@ +package net.william278.husksync.event; + +import net.william278.husksync.player.OnlineUser; + +public interface PlayerEvent extends Event { + + OnlineUser getUser(); + +} diff --git a/common/src/main/java/net/william278/husksync/event/PreSyncEvent.java b/common/src/main/java/net/william278/husksync/event/PreSyncEvent.java new file mode 100644 index 00000000..bc411751 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/event/PreSyncEvent.java @@ -0,0 +1,13 @@ +package net.william278.husksync.event; + +import net.william278.husksync.data.UserData; +import org.jetbrains.annotations.NotNull; + +public interface PreSyncEvent extends CancellableEvent { + + @NotNull + UserData getUserData(); + + void setUserData(@NotNull UserData userData); + +} diff --git a/common/src/main/java/net/william278/husksync/event/SyncCompleteEvent.java b/common/src/main/java/net/william278/husksync/event/SyncCompleteEvent.java new file mode 100644 index 00000000..119ec650 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/event/SyncCompleteEvent.java @@ -0,0 +1,5 @@ +package net.william278.husksync.event; + +public interface SyncCompleteEvent extends PlayerEvent { + +} 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 b314ae14..1273ab32 100644 --- a/common/src/main/java/net/william278/husksync/listener/EventListener.java +++ b/common/src/main/java/net/william278/husksync/listener/EventListener.java @@ -2,7 +2,9 @@ package net.william278.husksync.listener; import net.william278.husksync.HuskSync; import net.william278.husksync.config.Settings; -import net.william278.husksync.data.InventoryData; +import net.william278.husksync.data.ItemData; +import net.william278.husksync.data.DataSaveCause; +import net.william278.husksync.data.UserData; import net.william278.husksync.player.OnlineUser; import org.jetbrains.annotations.NotNull; @@ -14,6 +16,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; public abstract class EventListener { @@ -43,45 +46,67 @@ public abstract class EventListener { return; } usersAwaitingSync.add(user.uuid); - CompletableFuture.runAsync(() -> huskSync.getRedisManager().getUserServerSwitch(user).thenAccept(changingServers -> { - if (!changingServers) { - // Fetch from the database if the user isn't changing servers - setUserFromDatabase(user).thenRun(() -> handleSynchronisationCompletion(user)); - } else { - final int TIME_OUT_MILLISECONDS = 3200; - CompletableFuture.runAsync(() -> { - final AtomicInteger currentMilliseconds = new AtomicInteger(0); - final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + CompletableFuture.runAsync(() -> { + try { + // Hold reading data for the network latency threshold, to ensure the source server has set the redis key + Thread.sleep(Math.min(0, huskSync.getSettings().getIntegerValue(Settings.ConfigOption.SYNCHRONIZATION_NETWORK_LATENCY_MILLISECONDS))); + } catch (InterruptedException e) { + huskSync.getLoggingAdapter().log(Level.SEVERE, "An exception occurred handling a player join", e); + } finally { + huskSync.getRedisManager().getUserServerSwitch(user).thenAccept(changingServers -> { + huskSync.getLoggingAdapter().info("Handling server change check " + ((changingServers) ? "true" : "false")); + if (!changingServers) { + huskSync.getLoggingAdapter().info("User is not changing servers"); + // Fetch from the database if the user isn't changing servers + setUserFromDatabase(user).thenRun(() -> handleSynchronisationCompletion(user)); + } else { + huskSync.getLoggingAdapter().info("User is changing servers, setting from db"); + final int TIME_OUT_MILLISECONDS = 3200; + CompletableFuture.runAsync(() -> { + final AtomicInteger currentMilliseconds = new AtomicInteger(0); + final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); - // Set the user as soon as the source server has set the data to redis - executor.scheduleAtFixedRate(() -> { - if (disabling || currentMilliseconds.get() > TIME_OUT_MILLISECONDS) { - executor.shutdown(); - setUserFromDatabase(user).thenRun(() -> handleSynchronisationCompletion(user)); - return; - } - huskSync.getRedisManager().getUserData(user).thenAccept(redisUserData -> - redisUserData.ifPresent(redisData -> { - user.setData(redisData, huskSync.getSettings()).join(); + // Set the user as soon as the source server has set the data to redis + executor.scheduleAtFixedRate(() -> { + if (user.isOffline()) { executor.shutdown(); - })).join(); - currentMilliseconds.addAndGet(200); - }, 0, 200L, TimeUnit.MILLISECONDS); + huskSync.getLoggingAdapter().info("Cancelled sync, user gone offline!"); + return; + } + if (disabling || currentMilliseconds.get() > TIME_OUT_MILLISECONDS) { + executor.shutdown(); + setUserFromDatabase(user).thenRun(() -> handleSynchronisationCompletion(user)); + huskSync.getLoggingAdapter().info("Setting user from db as fallback"); + return; + } + huskSync.getRedisManager().getUserData(user).thenAccept(redisUserData -> + redisUserData.ifPresent(redisData -> { + huskSync.getLoggingAdapter().info("Setting user from redis!"); + user.setData(redisData, huskSync.getSettings(), huskSync.getEventCannon()) + .thenRun(() -> handleSynchronisationCompletion(user)).join(); + executor.shutdown(); + })).join(); + currentMilliseconds.addAndGet(200); + }, 0, 200L, TimeUnit.MILLISECONDS); + }); + } }); } - })); + }); } private CompletableFuture setUserFromDatabase(@NotNull OnlineUser user) { return huskSync.getDatabase().getCurrentUserData(user) - .thenAccept(databaseUserData -> databaseUserData.ifPresent(databaseData -> user - .setData(databaseData.userData(), huskSync.getSettings()).join())); + .thenAccept(databaseUserData -> databaseUserData.ifPresent(databaseData -> + user.setData(databaseData.userData(), huskSync.getSettings(), + huskSync.getEventCannon()).join())); } private void handleSynchronisationCompletion(@NotNull OnlineUser user) { huskSync.getLocales().getLocale("synchronisation_complete").ifPresent(user::sendActionBar); usersAwaitingSync.remove(user.uuid); huskSync.getDatabase().ensureUser(user).join(); + huskSync.getEventCannon().fireSyncCompleteEvent(user); } public final void handlePlayerQuit(@NotNull OnlineUser user) { @@ -89,9 +114,14 @@ public abstract class EventListener { if (disabling) { return; } + // Don't sync players awaiting synchronization + if (usersAwaitingSync.contains(user.uuid)) { + return; + } huskSync.getRedisManager().setUserServerSwitch(user).thenRun(() -> user.getUserData().thenAccept( userData -> huskSync.getRedisManager().setUserData(user, userData).thenRun( - () -> huskSync.getDatabase().setUserData(user, userData).join()))); + () -> huskSync.getDatabase().setUserData(user, userData, DataSaveCause.DISCONNECT).join()))); + usersAwaitingSync.remove(user.uuid); } public final void handleWorldSave(@NotNull List usersInWorld) { @@ -99,20 +129,20 @@ public abstract class EventListener { return; } CompletableFuture.runAsync(() -> usersInWorld.forEach(user -> - huskSync.getDatabase().setUserData(user, user.getUserData().join()).join())); + huskSync.getDatabase().setUserData(user, user.getUserData().join(), DataSaveCause.WORLD_SAVE).join())); } public final void handlePluginDisable() { disabling = true; huskSync.getOnlineUsers().stream().filter(user -> !usersAwaitingSync.contains(user.uuid)).forEach(user -> - huskSync.getDatabase().setUserData(user, user.getUserData().join()).join()); + huskSync.getDatabase().setUserData(user, user.getUserData().join(), DataSaveCause.SERVER_SHUTDOWN).join()); huskSync.getDatabase().close(); huskSync.getRedisManager().close(); } - public final void handleMenuClose(@NotNull OnlineUser user, @NotNull InventoryData menuInventory) { + public final void handleMenuClose(@NotNull OnlineUser user, @NotNull ItemData menuInventory) { if (disabling) { return; } diff --git a/common/src/main/java/net/william278/husksync/player/OnlineUser.java b/common/src/main/java/net/william278/husksync/player/OnlineUser.java index 7d1aa845..88c860e6 100644 --- a/common/src/main/java/net/william278/husksync/player/OnlineUser.java +++ b/common/src/main/java/net/william278/husksync/player/OnlineUser.java @@ -4,8 +4,11 @@ import de.themoep.minedown.MineDown; import net.william278.husksync.config.Settings; import net.william278.husksync.data.*; import net.william278.husksync.editor.InventoryEditorMenu; +import net.william278.husksync.event.EventCannon; +import net.william278.husksync.event.PreSyncEvent; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -29,49 +32,42 @@ public abstract class OnlineUser extends User { /** * Set the player's {@link StatusData} * - * @param statusData the player's {@link StatusData} - * @param setHealth whether to set the player's health - * @param setMaxHealth whether to set the player's max health - * @param setHunger whether to set the player's hunger - * @param setExperience whether to set the player's experience - * @param setGameMode whether to set the player's game mode + * @param statusData the player's {@link StatusData} + * @param statusDataFlags the flags to use for setting the status data * @return a future returning void when complete */ public abstract CompletableFuture setStatus(@NotNull StatusData statusData, - final boolean setHealth, final boolean setMaxHealth, - final boolean setHunger, final boolean setExperience, - final boolean setGameMode, final boolean setFlying, - final boolean setSelectedItemSlot); + @NotNull List statusDataFlags); /** - * Get the player's inventory {@link InventoryData} contents + * Get the player's inventory {@link ItemData} contents * - * @return The player's inventory {@link InventoryData} contents + * @return The player's inventory {@link ItemData} contents */ - public abstract CompletableFuture getInventory(); + public abstract CompletableFuture getInventory(); /** - * Set the player's {@link InventoryData} + * Set the player's {@link ItemData} * - * @param inventoryData The player's {@link InventoryData} + * @param itemData The player's {@link ItemData} * @return a future returning void when complete */ - public abstract CompletableFuture setInventory(@NotNull InventoryData inventoryData); + public abstract CompletableFuture setInventory(@NotNull ItemData itemData); /** - * Get the player's ender chest {@link InventoryData} contents + * Get the player's ender chest {@link ItemData} contents * - * @return The player's ender chest {@link InventoryData} contents + * @return The player's ender chest {@link ItemData} contents */ - public abstract CompletableFuture getEnderChest(); + public abstract CompletableFuture getEnderChest(); /** - * Set the player's {@link InventoryData} + * Set the player's {@link ItemData} * - * @param enderChestData The player's {@link InventoryData} + * @param enderChestData The player's {@link ItemData} * @return a future returning void when complete */ - public abstract CompletableFuture setEnderChest(@NotNull InventoryData enderChestData); + public abstract CompletableFuture setEnderChest(@NotNull ItemData enderChestData); /** @@ -170,49 +166,40 @@ public abstract class OnlineUser extends User { * @param settings Plugin settings, for determining what needs setting * @return a future that will be completed when done */ - public final CompletableFuture setData(@NotNull UserData data, @NotNull Settings settings) { + public final CompletableFuture setData(@NotNull UserData data, @NotNull Settings settings, + @NotNull EventCannon eventCannon) { return CompletableFuture.runAsync(() -> { - try { - // Don't set offline players - if (isOffline()) { - return; + final PreSyncEvent preSyncEvent = (PreSyncEvent) eventCannon.firePreSyncEvent(this, data).join(); + final UserData finalData = preSyncEvent.getUserData(); + final List> dataSetOperations = new ArrayList<>() {{ + if (!isOffline() && !isDead() && !preSyncEvent.isCancelled()) { + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)) { + add(setInventory(finalData.getInventoryData())); + } + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ENDER_CHESTS)) { + add(setEnderChest(finalData.getEnderChestData())); + } + add(setStatus(finalData.getStatusData(), StatusDataFlag.getFromSettings(settings))); + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_POTION_EFFECTS)) { + add(setPotionEffects(finalData.getPotionEffectsData())); + } + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ADVANCEMENTS)) { + add(setAdvancements(finalData.getAdvancementData())); + } + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_STATISTICS)) { + add(setStatistics(finalData.getStatisticsData())); + } + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION)) { + add(setLocation(finalData.getLocationData())); + } + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER)) { + add(setPersistentDataContainer(finalData.getPersistentDataContainerData())); + } } - // Don't set dead players - if (isDead()) { - return; - } - if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)) { - setInventory(data.getInventoryData()).join(); - } - if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ENDER_CHESTS)) { - setEnderChest(data.getEnderChestData()).join(); - } - setStatus(data.getStatusData(), settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HEALTH), - settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_MAX_HEALTH), - settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HUNGER), - settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_EXPERIENCE), - settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_GAME_MODE), - settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION), - settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)).join(); - if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_POTION_EFFECTS)) { - setPotionEffects(data.getPotionEffectData()).join(); - } - if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ADVANCEMENTS)) { - setAdvancements(data.getAdvancementData()).join(); - } - if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_STATISTICS)) { - setStatistics(data.getStatisticData()).join(); - } - if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER)) { - setPersistentDataContainer(data.getPersistentDataContainerData()).join(); - } - if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION)) { - setLocation(data.getLocationData()).join(); - } - } catch (Exception e) { - e.printStackTrace(); - } + }}; + CompletableFuture.allOf(dataSetOperations.toArray(new CompletableFuture[0])).join(); }); + } /** 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 02ce812e..7e1939cc 100644 --- a/common/src/main/java/net/william278/husksync/redis/RedisManager.java +++ b/common/src/main/java/net/william278/husksync/redis/RedisManager.java @@ -4,6 +4,7 @@ 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 net.william278.husksync.util.Logger; import org.jetbrains.annotations.NotNull; import org.xerial.snappy.Snappy; import redis.clients.jedis.Jedis; @@ -13,6 +14,7 @@ import redis.clients.jedis.exceptions.JedisException; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; import java.util.Date; import java.util.Optional; import java.util.UUID; @@ -29,6 +31,7 @@ public class RedisManager { private final JedisPoolConfig jedisPoolConfig; private final DataAdapter dataAdapter; + private final Logger logger; private final String redisHost; private final int redisPort; private final String redisPassword; @@ -36,9 +39,12 @@ public class RedisManager { private JedisPool jedisPool; - public RedisManager(@NotNull Settings settings, @NotNull DataAdapter dataAdapter) { + public RedisManager(@NotNull Settings settings, @NotNull DataAdapter dataAdapter, @NotNull Logger logger) { clusterId = settings.getStringValue(Settings.ConfigOption.CLUSTER_ID); this.dataAdapter = dataAdapter; + this.logger = logger; + + // Set redis credentials this.redisHost = settings.getStringValue(Settings.ConfigOption.REDIS_HOST); this.redisPort = settings.getIntegerValue(Settings.ConfigOption.REDIS_PORT); this.redisPassword = settings.getStringValue(Settings.ConfigOption.REDIS_PASSWORD); @@ -87,6 +93,8 @@ public class RedisManager { jedis.setex(getKey(RedisKeyType.DATA_UPDATE, user.uuid), RedisKeyType.DATA_UPDATE.timeToLive, dataAdapter.toBytes(userData)); + logger.debug("[" + user.username + "] Set " + RedisKeyType.DATA_UPDATE.name() + " key to redis at: " + + new SimpleDateFormat("mm:ss.SSS").format(new Date())); } }); } catch (Exception e) { @@ -100,6 +108,10 @@ public class RedisManager { try (Jedis jedis = jedisPool.getResource()) { jedis.setex(getKey(RedisKeyType.SERVER_SWITCH, user.uuid), RedisKeyType.SERVER_SWITCH.timeToLive, new byte[0]); + logger.debug("[" + user.username + "] Set " + RedisKeyType.SERVER_SWITCH.name() + " key to redis at: " + + new SimpleDateFormat("mm:ss.SSS").format(new Date())); + } catch (Exception e) { + e.printStackTrace(); } }); } @@ -114,7 +126,8 @@ public class RedisManager { return CompletableFuture.supplyAsync(() -> { try (Jedis jedis = jedisPool.getResource()) { final byte[] key = getKey(RedisKeyType.DATA_UPDATE, user.uuid); - System.out.println("Reading key at " + new Date().getTime()); + logger.debug("[" + user.username + "] Read " + RedisKeyType.DATA_UPDATE.name() + " key from redis at: " + + new SimpleDateFormat("mm:ss.SSS").format(new Date())); final byte[] dataByteArray = jedis.get(key); if (dataByteArray == null) { return Optional.empty(); @@ -124,6 +137,9 @@ public class RedisManager { // Use Snappy to decompress the json return Optional.of(dataAdapter.fromBytes(dataByteArray)); + } catch (Exception e) { + e.printStackTrace(); + return Optional.empty(); } }); } @@ -132,13 +148,18 @@ public class RedisManager { return CompletableFuture.supplyAsync(() -> { try (Jedis jedis = jedisPool.getResource()) { final byte[] key = getKey(RedisKeyType.SERVER_SWITCH, user.uuid); - final byte[] compressedJson = jedis.get(key); - if (compressedJson == null) { + logger.debug("[" + user.username + "] Read " + RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " + + new SimpleDateFormat("mm:ss.SSS").format(new Date())); + final byte[] readData = jedis.get(key); + if (readData == null) { return false; } // Consume the key (delete from redis) jedis.del(key); return true; + } catch (Exception e) { + e.printStackTrace(); + return false; } }); } 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 2d663f4f..d13546cc 100644 --- a/common/src/main/java/net/william278/husksync/util/Logger.java +++ b/common/src/main/java/net/william278/husksync/util/Logger.java @@ -1,20 +1,34 @@ package net.william278.husksync.util; +import org.jetbrains.annotations.NotNull; + import java.util.logging.Level; /** * An abstract, cross-platform representation of a logger */ -public interface Logger { +public abstract class Logger { - void log(Level level, String message, Exception e); + private boolean debug; - void log(Level level, String message); + public abstract void log(@NotNull Level level, @NotNull String message, @NotNull Exception e); - void info(String message); + public abstract void log(@NotNull Level level, @NotNull String message); - void severe(String message); + public abstract void info(@NotNull String message); - void config(String message); + public abstract void severe(@NotNull String message); + + public final void debug(@NotNull String message) { + if (debug) { + log(Level.INFO, "[DEBUG] " + message); + } + } + + public abstract void config(@NotNull String message); + + public final void showDebugLogs(boolean debug) { + this.debug = debug; + } } diff --git a/common/src/main/resources/config.yml b/common/src/main/resources/config.yml index 161736a4..4858e257 100644 --- a/common/src/main/resources/config.yml +++ b/common/src/main/resources/config.yml @@ -7,6 +7,7 @@ language: 'en-gb' check_for_updates: true cluster_id: '' +debug_logging: true database: credentials: @@ -37,6 +38,7 @@ synchronization: max_user_data_records: 5 save_on_world_save: true compress_data: true + network_latency_milliseconds: 500 features: inventories: true ender_chests: true diff --git a/common/src/main/resources/database/mysql_schema.sql b/common/src/main/resources/database/mysql_schema.sql index 964c6f27..61a6b3ec 100644 --- a/common/src/main/resources/database/mysql_schema.sql +++ b/common/src/main/resources/database/mysql_schema.sql @@ -10,10 +10,11 @@ CREATE TABLE IF NOT EXISTS `%players_table%` # 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` mediumblob NOT NULL, + `version_uuid` char(36) NOT NULL, + `player_uuid` char(36) NOT NULL, + `timestamp` datetime NOT NULL, + `save_cause` varchar(32) NOT NULL, + `data` mediumblob NOT NULL, PRIMARY KEY (`version_uuid`), FOREIGN KEY (`player_uuid`) REFERENCES `%players_table%` (`uuid`) ON DELETE CASCADE diff --git a/common/src/test/java/net/william278/husksync/data/DataAdaptionTests.java b/common/src/test/java/net/william278/husksync/data/DataAdaptionTests.java new file mode 100644 index 00000000..3487de52 --- /dev/null +++ b/common/src/test/java/net/william278/husksync/data/DataAdaptionTests.java @@ -0,0 +1,75 @@ +package net.william278.husksync.data; + +import net.william278.husksync.player.DummyPlayer; +import net.william278.husksync.player.OnlineUser; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; + +public class DataAdaptionTests { + + @Test + public void testJsonDataAdapter() { + final OnlineUser dummyUser = DummyPlayer.create(); + final UserData dummyUserData = dummyUser.getUserData().join(); + final DataAdapter dataAdapter = new JsonDataAdapter(); + final byte[] data = dataAdapter.toBytes(dummyUserData); + final UserData deserializedUserData = dataAdapter.fromBytes(data); + + boolean isEquals = deserializedUserData.getInventoryData().serializedItems + .equals(dummyUserData.getInventoryData().serializedItems) + && deserializedUserData.getEnderChestData().serializedItems + .equals(dummyUserData.getEnderChestData().serializedItems) + && deserializedUserData.getPotionEffectsData().serializedPotionEffects + .equals(dummyUserData.getPotionEffectsData().serializedPotionEffects) + && deserializedUserData.getStatusData().health == dummyUserData.getStatusData().health + && deserializedUserData.getStatusData().hunger == dummyUserData.getStatusData().hunger + && deserializedUserData.getStatusData().saturation == dummyUserData.getStatusData().saturation + && deserializedUserData.getStatusData().saturationExhaustion == dummyUserData.getStatusData().saturationExhaustion + && deserializedUserData.getStatusData().selectedItemSlot == dummyUserData.getStatusData().selectedItemSlot + && deserializedUserData.getStatusData().totalExperience == dummyUserData.getStatusData().totalExperience + && deserializedUserData.getStatusData().maxHealth == dummyUserData.getStatusData().maxHealth + && deserializedUserData.getStatusData().healthScale == dummyUserData.getStatusData().healthScale; + + Assertions.assertTrue(isEquals); + } + + @Test + public void testJsonFormat() { + final OnlineUser dummyUser = DummyPlayer.create(); + final UserData dummyUserData = dummyUser.getUserData().join(); + final DataAdapter dataAdapter = new JsonDataAdapter(); + final byte[] data = dataAdapter.toBytes(dummyUserData); + final String json = new String(data, StandardCharsets.UTF_8); + final String expectedJson = "{\"status\":{\"health\":20.0,\"max_health\":20.0,\"health_scale\":0.0,\"hunger\":20,\"saturation\":5.0,\"saturation_exhaustion\":5.0,\"selected_item_slot\":1,\"total_experience\":100,\"experience_level\":1,\"experience_progress\":1.0,\"game_mode\":\"SURVIVAL\",\"is_flying\":false},\"inventory\":{\"serialized_inventory\":\"\"},\"ender_chest\":{\"serialized_inventory\":\"\"},\"potion_effects\":{\"serialized_potion_effects\":\"\"},\"advancements\":[],\"statistics\":{\"untyped_statistics\":{},\"block_statistics\":{},\"item_statistics\":{},\"entity_statistics\":{}},\"location\":{\"world_name\":\"dummy_world\",\"world_uuid\":\"00000000-0000-0000-0000-000000000000\",\"world_environment\":\"NORMAL\",\"x\":0.0,\"y\":64.0,\"z\":0.0,\"yaw\":90.0,\"pitch\":180.0},\"persistent_data_container\":{\"persistent_data_map\":{}},\"format_version\":1}"; + Assertions.assertEquals(expectedJson, json); + } + + @Test + public void testCompressedDataAdapter() { + final OnlineUser dummyUser = DummyPlayer.create(); + final UserData dummyUserData = dummyUser.getUserData().join(); + final DataAdapter dataAdapter = new CompressedDataAdapter(); + final byte[] data = dataAdapter.toBytes(dummyUserData); + final UserData deserializedUserData = dataAdapter.fromBytes(data); + + boolean isEquals = deserializedUserData.getInventoryData().serializedItems + .equals(dummyUserData.getInventoryData().serializedItems) + && deserializedUserData.getEnderChestData().serializedItems + .equals(dummyUserData.getEnderChestData().serializedItems) + && deserializedUserData.getPotionEffectsData().serializedPotionEffects + .equals(dummyUserData.getPotionEffectsData().serializedPotionEffects) + && deserializedUserData.getStatusData().health == dummyUserData.getStatusData().health + && deserializedUserData.getStatusData().hunger == dummyUserData.getStatusData().hunger + && deserializedUserData.getStatusData().saturation == dummyUserData.getStatusData().saturation + && deserializedUserData.getStatusData().saturationExhaustion == dummyUserData.getStatusData().saturationExhaustion + && deserializedUserData.getStatusData().selectedItemSlot == dummyUserData.getStatusData().selectedItemSlot + && deserializedUserData.getStatusData().totalExperience == dummyUserData.getStatusData().totalExperience + && deserializedUserData.getStatusData().maxHealth == dummyUserData.getStatusData().maxHealth + && deserializedUserData.getStatusData().healthScale == dummyUserData.getStatusData().healthScale; + + Assertions.assertTrue(isEquals); + } + +} diff --git a/common/src/test/java/net/william278/husksync/player/DummyPlayer.java b/common/src/test/java/net/william278/husksync/player/DummyPlayer.java new file mode 100644 index 00000000..8843276e --- /dev/null +++ b/common/src/test/java/net/william278/husksync/player/DummyPlayer.java @@ -0,0 +1,156 @@ +package net.william278.husksync.player; + +import de.themoep.minedown.MineDown; +import net.william278.husksync.data.*; +import net.william278.husksync.editor.InventoryEditorMenu; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class DummyPlayer extends OnlineUser { + + private DummyPlayer(@NotNull UUID uuid, @NotNull String username) { + super(uuid, username); + } + + public static DummyPlayer create() { + return new DummyPlayer(UUID.fromString("00000000-0000-0000-0000-000000000000"), + "DummyPlayer"); + } + + @Override + public CompletableFuture getStatus() { + return CompletableFuture.supplyAsync(() -> new StatusData(20, 20, 0, + 20, 5, 5, 1, + 100, 1, 1f, "SURVIVAL", false)); + } + + @Override + public CompletableFuture setStatus(@NotNull StatusData statusData, @NotNull List statusDataFlags) { + return CompletableFuture.runAsync(() -> { + // do nothing + }); + } + + @Override + public CompletableFuture getInventory() { + return CompletableFuture.supplyAsync(() -> new ItemData("")); + } + + @Override + public CompletableFuture setInventory(@NotNull ItemData itemData) { + return CompletableFuture.runAsync(() -> { + // do nothing + }); + } + + @Override + public CompletableFuture getEnderChest() { + return CompletableFuture.supplyAsync(() -> new ItemData("")); + } + + @Override + public CompletableFuture setEnderChest(@NotNull ItemData enderChestData) { + return CompletableFuture.runAsync(() -> { + // do nothing + }); + } + + @Override + public CompletableFuture getPotionEffects() { + return CompletableFuture.supplyAsync(() -> new PotionEffectData("")); + } + + @Override + public CompletableFuture setPotionEffects(@NotNull PotionEffectData potionEffectData) { + return CompletableFuture.runAsync(() -> { + // do nothing + }); + } + + @Override + public CompletableFuture> getAdvancements() { + return CompletableFuture.supplyAsync(ArrayList::new); + } + + @Override + public CompletableFuture setAdvancements(@NotNull List advancementData) { + return CompletableFuture.runAsync(() -> { + // do nothing + }); + } + + @Override + public CompletableFuture getStatistics() { + return CompletableFuture.supplyAsync(() -> new StatisticsData(new HashMap<>(), + new HashMap<>(), new HashMap<>(), new HashMap<>())); + } + + @Override + public CompletableFuture setStatistics(@NotNull StatisticsData statisticsData) { + return CompletableFuture.runAsync(() -> { + // do nothing + }); + } + + @Override + public CompletableFuture getLocation() { + return CompletableFuture.supplyAsync(() -> new LocationData("dummy_world", + UUID.fromString("00000000-0000-0000-0000-000000000000"), + "NORMAL", 0, 64, 0, 90f, 180f)); + } + + @Override + public CompletableFuture setLocation(@NotNull LocationData locationData) { + return CompletableFuture.runAsync(() -> { + // do nothing + }); + } + + @Override + public CompletableFuture getPersistentDataContainer() { + return CompletableFuture.supplyAsync(() -> new PersistentDataContainerData(new HashMap<>())); + } + + @Override + public CompletableFuture setPersistentDataContainer(@NotNull PersistentDataContainerData persistentDataContainerData) { + return CompletableFuture.runAsync(() -> { + // do nothing + }); + } + + @Override + public boolean isDead() { + return false; + } + + @Override + public boolean isOffline() { + return false; + } + + @Override + public void sendMessage(@NotNull MineDown mineDown) { + // do nothing + } + + @Override + public void sendActionBar(@NotNull MineDown mineDown) { + // do nothing + } + + @Override + public boolean hasPermission(@NotNull String node) { + return true; + } + + @Override + public void showMenu(@NotNull InventoryEditorMenu menu) { + // do nothing + } + +}