diff --git a/common/src/main/java/net/william278/husksync/api/BaseHuskSyncAPI.java b/common/src/main/java/net/william278/husksync/api/BaseHuskSyncAPI.java index fe1fc24f..3827e8c6 100644 --- a/common/src/main/java/net/william278/husksync/api/BaseHuskSyncAPI.java +++ b/common/src/main/java/net/william278/husksync/api/BaseHuskSyncAPI.java @@ -72,7 +72,7 @@ public abstract class BaseHuskSyncAPI { public final CompletableFuture> getUserData(@NotNull User user) { return CompletableFuture.supplyAsync(() -> { if (user instanceof OnlineUser) { - return Optional.of(((OnlineUser) user).getUserData().join()); + return ((OnlineUser) user).getUserData(plugin.getLoggingAdapter()).join(); } else { return plugin.getDatabase().getCurrentUserData(user).join().map(UserDataSnapshot::userData); } @@ -103,8 +103,8 @@ public abstract class BaseHuskSyncAPI { * @since 2.0 */ public final CompletableFuture saveUserData(@NotNull OnlineUser user) { - return CompletableFuture.runAsync(() -> user.getUserData().thenAccept(userData -> - plugin.getDatabase().setUserData(user, userData, DataSaveCause.API).join())); + return CompletableFuture.runAsync(() -> user.getUserData(plugin.getLoggingAdapter()).thenAccept(optionalUserData -> optionalUserData.ifPresent( + userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.API).join()))); } /** diff --git a/common/src/main/java/net/william278/husksync/listener/EventListener.java b/common/src/main/java/net/william278/husksync/listener/EventListener.java index c2e634f0..274c23c6 100644 --- a/common/src/main/java/net/william278/husksync/listener/EventListener.java +++ b/common/src/main/java/net/william278/husksync/listener/EventListener.java @@ -76,8 +76,8 @@ public abstract class EventListener { } if (disabling || currentMilliseconds.get() > TIME_OUT_MILLISECONDS) { executor.shutdown(); - setUserFromDatabase(user) - .thenAccept(succeeded -> handleSynchronisationCompletion(user, succeeded)); + setUserFromDatabase(user).thenAccept( + succeeded -> handleSynchronisationCompletion(user, succeeded)); return; } plugin.getRedisManager().getUserData(user).thenAccept(redisUserData -> @@ -145,9 +145,10 @@ public abstract class EventListener { if (usersAwaitingSync.contains(user.uuid)) { return; } - plugin.getRedisManager().setUserServerSwitch(user).thenRun(() -> user.getUserData().thenAccept( - userData -> plugin.getRedisManager().setUserData(user, userData).thenRun( - () -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.DISCONNECT).join()))); + plugin.getRedisManager().setUserServerSwitch(user).thenRun(() -> user.getUserData(plugin.getLoggingAdapter()).thenAccept( + optionalUserData -> optionalUserData.ifPresent( + userData -> plugin.getRedisManager().setUserData(user, userData).thenRun( + () -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.DISCONNECT).join())))); usersAwaitingSync.remove(user.uuid); } @@ -160,8 +161,8 @@ public abstract class EventListener { if (disabling || !plugin.getSettings().getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SAVE_ON_WORLD_SAVE)) { return; } - usersInWorld.forEach(user -> plugin.getDatabase().setUserData(user, user.getUserData().join(), - DataSaveCause.WORLD_SAVE).join()); + usersInWorld.forEach(user -> user.getUserData(plugin.getLoggingAdapter()).join().ifPresent( + userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.WORLD_SAVE).join())); } /** @@ -204,8 +205,9 @@ public abstract class EventListener { public final void handlePluginDisable() { disabling = true; - plugin.getOnlineUsers().stream().filter(user -> !usersAwaitingSync.contains(user.uuid)).forEach(user -> - plugin.getDatabase().setUserData(user, user.getUserData().join(), DataSaveCause.SERVER_SHUTDOWN).join()); + plugin.getOnlineUsers().stream().filter(user -> !usersAwaitingSync.contains(user.uuid)).forEach( + user -> user.getUserData(plugin.getLoggingAdapter()).join().ifPresent( + userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.SERVER_SHUTDOWN).join())); plugin.getDatabase().close(); plugin.getRedisManager().close(); 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 a8bc1a46..5a918dac 100644 --- a/common/src/main/java/net/william278/husksync/player/OnlineUser.java +++ b/common/src/main/java/net/william278/husksync/player/OnlineUser.java @@ -12,6 +12,7 @@ import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.logging.Level; @@ -251,16 +252,23 @@ public abstract class OnlineUser extends User { public abstract void showMenu(@NotNull ItemEditorMenu menu); /** - * Get the player's current {@link UserData} + * Get the player's current {@link UserData} in an {@link Optional} + *

+ * If the user data could not be returned due to an exception, the optional will return empty * - * @return the player's current {@link UserData} + * @param logger The logger to use for handling exceptions + * @return the player's current {@link UserData} in an optional; empty if an exception occurs */ - public final CompletableFuture getUserData() { - return CompletableFuture.supplyAsync( - () -> new UserData(getStatus().join(), getInventory().join(), + public final CompletableFuture> getUserData(@NotNull Logger logger) { + return CompletableFuture.supplyAsync(() -> Optional.of(new UserData(getStatus().join(), getInventory().join(), getEnderChest().join(), getPotionEffects().join(), getAdvancements().join(), getStatistics().join(), getLocation().join(), getPersistentDataContainer().join(), - getMinecraftVersion().toString())); + getMinecraftVersion().toString()))) + .exceptionally(exception -> { + logger.log(Level.SEVERE, "Failed to fetch user data for online player " + username + " (" + exception.getMessage() + ")"); + exception.printStackTrace(); + return Optional.empty(); + }); } } 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 51ce6ab4..949e41a1 100644 --- a/common/src/main/java/net/william278/husksync/redis/RedisManager.java +++ b/common/src/main/java/net/william278/husksync/redis/RedisManager.java @@ -165,13 +165,17 @@ public class RedisManager { return CompletableFuture.supplyAsync(() -> { try (Jedis jedis = jedisPool.getResource()) { final byte[] key = getKey(RedisKeyType.DATA_UPDATE, user.uuid); - plugin.getLoggingAdapter().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) { + plugin.getLoggingAdapter().debug("[" + user.username + "] Could not read " + + RedisKeyType.DATA_UPDATE.name() + " key from redis at: " + + new SimpleDateFormat("mm:ss.SSS").format(new Date())); return Optional.empty(); } + plugin.getLoggingAdapter().debug("[" + user.username + "] Successfully read " + + RedisKeyType.DATA_UPDATE.name() + " key from redis at: " + + new SimpleDateFormat("mm:ss.SSS").format(new Date())); + // Consume the key (delete from redis) jedis.del(key); @@ -188,13 +192,17 @@ public class RedisManager { return CompletableFuture.supplyAsync(() -> { try (Jedis jedis = jedisPool.getResource()) { final byte[] key = getKey(RedisKeyType.SERVER_SWITCH, user.uuid); - plugin.getLoggingAdapter().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) { + plugin.getLoggingAdapter().debug("[" + user.username + "] Could not read " + + RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " + + new SimpleDateFormat("mm:ss.SSS").format(new Date())); return false; } + plugin.getLoggingAdapter().debug("[" + user.username + "] Successfully read " + + RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " + + new SimpleDateFormat("mm:ss.SSS").format(new Date())); + // Consume the key (delete from redis) jedis.del(key); return true; diff --git a/common/src/test/java/net/william278/husksync/data/DataAdaptionTests.java b/common/src/test/java/net/william278/husksync/data/DataAdaptionTests.java index a9c6c11f..2da22ec8 100644 --- a/common/src/test/java/net/william278/husksync/data/DataAdaptionTests.java +++ b/common/src/test/java/net/william278/husksync/data/DataAdaptionTests.java @@ -1,11 +1,14 @@ package net.william278.husksync.data; +import net.william278.husksync.logger.DummyLogger; 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; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; /** * Tests for the data system {@link DataAdapter} @@ -15,64 +18,68 @@ 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); + final AtomicBoolean isEquals = new AtomicBoolean(false); + dummyUser.getUserData(new DummyLogger()).join().ifPresent(dummyUserData -> { + 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); + isEquals.set(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.get()); } @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_items\":\"\"},\"ender_chest\":{\"serialized_items\":\"\"},\"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\":{}},\"minecraft_version\":\"1.19-beta123456\",\"format_version\":1}"; - Assertions.assertEquals(expectedJson, json); + AtomicReference json = new AtomicReference<>(); + dummyUser.getUserData(new DummyLogger()).join().ifPresent(dummyUserData -> { + final DataAdapter dataAdapter = new JsonDataAdapter(); + final byte[] data = dataAdapter.toBytes(dummyUserData); + json.set(new String(data, StandardCharsets.UTF_8)); + }); + Assertions.assertEquals(expectedJson, json.get()); } @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; + AtomicBoolean isEquals = new AtomicBoolean(false); + dummyUser.getUserData(new DummyLogger()).join().ifPresent(dummyUserData -> { + final DataAdapter dataAdapter = new CompressedDataAdapter(); + final byte[] data = dataAdapter.toBytes(dummyUserData); + final UserData deserializedUserData = dataAdapter.fromBytes(data); - Assertions.assertTrue(isEquals); + isEquals.set(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.get()); } } diff --git a/common/src/test/java/net/william278/husksync/logger/DummyLogger.java b/common/src/test/java/net/william278/husksync/logger/DummyLogger.java new file mode 100644 index 00000000..c44378a1 --- /dev/null +++ b/common/src/test/java/net/william278/husksync/logger/DummyLogger.java @@ -0,0 +1,44 @@ +package net.william278.husksync.logger; + +import de.themoep.minedown.MineDown; +import net.william278.husksync.util.Logger; +import org.jetbrains.annotations.NotNull; + +import java.util.logging.Level; + +public class DummyLogger extends Logger { + + public DummyLogger() { + } + + @Override + public void log(@NotNull Level level, @NotNull String message, @NotNull Exception e) { + System.out.println(level.getName() + ": " + message); + e.printStackTrace(); + } + + @Override + public void log(@NotNull Level level, @NotNull String message) { + System.out.println(level.getName() + ": " + message); + } + + @Override + public void log(@NotNull Level level, @NotNull MineDown mineDown) { + System.out.println(level.getName() + ": " + mineDown.message()); + } + + @Override + public void info(@NotNull String message) { + System.out.println(Level.INFO.getName() + ": " + message); + } + + @Override + public void severe(@NotNull String message) { + System.out.println(Level.SEVERE.getName() + ": " + message); + } + + @Override + public void config(@NotNull String message) { + System.out.println(Level.CONFIG.getName() + ": " + message); + } +} diff --git a/gradle.properties b/gradle.properties index 8d5cd6a9..20a73e28 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,5 +3,5 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8' org.gradle.daemon=true javaVersion=16 -plugin_version=2.0 +plugin_version=2.1 plugin_archive=husksync \ No newline at end of file