diff --git a/bukkit/src/main/java/net/william278/husksync/api/HuskSyncAPI.java b/bukkit/src/main/java/net/william278/husksync/api/HuskSyncAPI.java index 468cddeb..86d6f135 100644 --- a/bukkit/src/main/java/net/william278/husksync/api/HuskSyncAPI.java +++ b/bukkit/src/main/java/net/william278/husksync/api/HuskSyncAPI.java @@ -66,7 +66,7 @@ public class HuskSyncAPI extends BaseHuskSyncAPI { return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData -> userData.ifPresent(data -> serializeItemStackArray(inventoryContents) .thenAccept(serializedInventory -> { - data.getInventoryData().serializedItems = serializedInventory; + data.getInventory().orElse(ItemData.empty()).serializedItems = serializedInventory; setUserData(user, data).join(); })))); } @@ -95,7 +95,7 @@ public class HuskSyncAPI extends BaseHuskSyncAPI { return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData -> userData.ifPresent(data -> serializeItemStackArray(enderChestContents) .thenAccept(serializedInventory -> { - data.getEnderChestData().serializedItems = serializedInventory; + data.getEnderChest().orElse(ItemData.empty()).serializedItems = serializedInventory; setUserData(user, data).join(); })))); } @@ -106,12 +106,14 @@ public class HuskSyncAPI extends BaseHuskSyncAPI { * @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} + * @apiNote If the {@link UserData} does not contain an inventory (i.e. inventory synchronisation is disabled), the + * returned {@link BukkitInventoryMap} will be equivalent an empty inventory. * @since 2.0 */ public CompletableFuture> getPlayerInventory(@NotNull User user) { return CompletableFuture.supplyAsync(() -> getUserData(user).join() - .map(userData -> deserializeInventory(userData - .getInventoryData().serializedItems).join())); + .map(userData -> deserializeInventory(userData.getInventory() + .orElse(ItemData.empty()).serializedItems).join())); } /** @@ -120,12 +122,14 @@ public class HuskSyncAPI extends BaseHuskSyncAPI { * @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} + * @apiNote If the {@link UserData} does not contain an Ender Chest (i.e. Ender Chest synchronisation is disabled), + * the returned {@link BukkitInventoryMap} will be equivalent to an empty inventory. * @since 2.0 */ public CompletableFuture> getPlayerEnderChest(@NotNull User user) { return CompletableFuture.supplyAsync(() -> getUserData(user).join() - .map(userData -> deserializeItemStackArray(userData - .getEnderChestData().serializedItems).join())); + .map(userData -> deserializeItemStackArray(userData.getEnderChest() + .orElse(ItemData.empty()).serializedItems).join())); } /** diff --git a/bukkit/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java b/bukkit/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java index f67302e2..11c3bdff 100644 --- a/bukkit/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java +++ b/bukkit/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java @@ -287,13 +287,16 @@ public class LegacyMigrator extends Migrator { legacyLocationData == null ? 90f : legacyLocationData.yaw(), legacyLocationData == null ? 180f : legacyLocationData.pitch()); - return new UserData(new StatusData(health, maxHealth, healthScale, hunger, saturation, - saturationExhaustion, selectedSlot, totalExp, expLevel, expProgress, gameMode, isFlying), - new ItemData(serializedInventory), new ItemData(serializedEnderChest), - new PotionEffectData(serializedPotionEffects), convertedAdvancements, - convertedStatisticData, convertedLocationData, - new PersistentDataContainerData(new HashMap<>()), - minecraftVersion); + return UserData.builder(minecraftVersion) + .setStatus(new StatusData(health, maxHealth, healthScale, hunger, saturation, + saturationExhaustion, selectedSlot, totalExp, expLevel, expProgress, gameMode, isFlying)) + .setInventory(new ItemData(serializedInventory)) + .setEnderChest(new ItemData(serializedEnderChest)) + .setPotionEffects(new PotionEffectData(serializedPotionEffects)) + .setAdvancements(convertedAdvancements) + .setStatistics(convertedStatisticData) + .setLocation(convertedLocationData) + .build(); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/bukkit/src/main/java/net/william278/husksync/migrator/MpdbMigrator.java b/bukkit/src/main/java/net/william278/husksync/migrator/MpdbMigrator.java index 2e56603f..df5dbd1d 100644 --- a/bukkit/src/main/java/net/william278/husksync/migrator/MpdbMigrator.java +++ b/bukkit/src/main/java/net/william278/husksync/migrator/MpdbMigrator.java @@ -280,18 +280,14 @@ public class MpdbMigrator extends Migrator { } // Create user data record - return new UserData(new StatusData(20, 20, 0, 20, 10, - 1, 0, totalExp, expLevel, expProgress, "SURVIVAL", - false), - new ItemData(BukkitSerializer.serializeItemStackArray(inventory.getContents()).join()), - new ItemData(BukkitSerializer.serializeItemStackArray(converter - .getItemStackFromSerializedData(serializedEnderChest)).join()), - new PotionEffectData(""), new ArrayList<>(), - new StatisticsData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()), - new LocationData("world", UUID.randomUUID(), "NORMAL", 0, 0, 0, - 0f, 0f), - new PersistentDataContainerData(new HashMap<>()), - minecraftVersion); + return UserData.builder(minecraftVersion) + .setStatus(new StatusData(20, 20, 0, 20, 10, + 1, 0, totalExp, expLevel, expProgress, "SURVIVAL", + false)) + .setInventory(new ItemData(BukkitSerializer.serializeItemStackArray(inventory.getContents()).join())) + .setEnderChest(new ItemData(BukkitSerializer.serializeItemStackArray(converter + .getItemStackFromSerializedData(serializedEnderChest)).join())) + .build(); }); } } diff --git a/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java b/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java index 2550da66..acf0f293 100644 --- a/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java +++ b/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java @@ -1,9 +1,7 @@ package net.william278.husksync.command; import net.william278.husksync.HuskSync; -import net.william278.husksync.data.DataSaveCause; -import net.william278.husksync.data.UserData; -import net.william278.husksync.data.UserDataSnapshot; +import net.william278.husksync.data.*; import net.william278.husksync.editor.ItemEditorMenu; import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.User; @@ -58,7 +56,8 @@ public class EnderChestCommand extends CommandBase implements TabCompletable { @NotNull User dataOwner, final boolean allowEdit) { CompletableFuture.runAsync(() -> { final UserData data = userDataSnapshot.userData(); - final ItemEditorMenu menu = ItemEditorMenu.createEnderChestMenu(data.getEnderChestData(), + final ItemEditorMenu menu = ItemEditorMenu.createEnderChestMenu( + data.getEnderChest().orElse(ItemData.empty()), dataOwner, player, plugin.getLocales(), allowEdit); plugin.getLocales().getLocale("viewing_ender_chest_of", dataOwner.username, DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault()) @@ -68,11 +67,18 @@ public class EnderChestCommand extends CommandBase implements TabCompletable { if (!menu.canEdit) { return; } - final UserData updatedUserData = new UserData(data.getStatusData(), data.getInventoryData(), - enderChestDataOnClose, data.getPotionEffectsData(), data.getAdvancementData(), - data.getStatisticsData(), data.getLocationData(), - data.getPersistentDataContainerData(), - plugin.getMinecraftVersion().toString()); + + final UserDataBuilder builder = UserData.builder(plugin.getMinecraftVersion()); + data.getStatus().ifPresent(builder::setStatus); + data.getInventory().ifPresent(builder::setInventory); + data.getAdvancements().ifPresent(builder::setAdvancements); + data.getLocation().ifPresent(builder::setLocation); + data.getPersistentDataContainer().ifPresent(builder::setPersistentDataContainer); + data.getStatistics().ifPresent(builder::setStatistics); + data.getPotionEffects().ifPresent(builder::setPotionEffects); + builder.setEnderChest(enderChestDataOnClose); + final UserData updatedUserData = builder.build(); + plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.ENDERCHEST_COMMAND).join(); plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join(); }); diff --git a/common/src/main/java/net/william278/husksync/command/InventoryCommand.java b/common/src/main/java/net/william278/husksync/command/InventoryCommand.java index 131b5e7f..665c0bd1 100644 --- a/common/src/main/java/net/william278/husksync/command/InventoryCommand.java +++ b/common/src/main/java/net/william278/husksync/command/InventoryCommand.java @@ -1,9 +1,7 @@ package net.william278.husksync.command; import net.william278.husksync.HuskSync; -import net.william278.husksync.data.DataSaveCause; -import net.william278.husksync.data.UserData; -import net.william278.husksync.data.UserDataSnapshot; +import net.william278.husksync.data.*; import net.william278.husksync.editor.ItemEditorMenu; import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.User; @@ -58,7 +56,8 @@ public class InventoryCommand extends CommandBase implements TabCompletable { @NotNull User dataOwner, boolean allowEdit) { CompletableFuture.runAsync(() -> { final UserData data = userDataSnapshot.userData(); - final ItemEditorMenu menu = ItemEditorMenu.createInventoryMenu(data.getInventoryData(), + final ItemEditorMenu menu = ItemEditorMenu.createInventoryMenu( + data.getInventory().orElse(ItemData.empty()), dataOwner, player, plugin.getLocales(), allowEdit); plugin.getLocales().getLocale("viewing_inventory_of", dataOwner.username, DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault()) @@ -68,11 +67,18 @@ public class InventoryCommand extends CommandBase implements TabCompletable { 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.getMinecraftVersion().toString()); + + final UserDataBuilder builder = UserData.builder(plugin.getMinecraftVersion()); + data.getStatus().ifPresent(builder::setStatus); + data.getEnderChest().ifPresent(builder::setEnderChest); + data.getAdvancements().ifPresent(builder::setAdvancements); + data.getLocation().ifPresent(builder::setLocation); + data.getPersistentDataContainer().ifPresent(builder::setPersistentDataContainer); + data.getStatistics().ifPresent(builder::setStatistics); + data.getPotionEffects().ifPresent(builder::setPotionEffects); + builder.setEnderChest(inventoryDataOnClose); + final UserData updatedUserData = builder.build(); + plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND).join(); plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join(); }); diff --git a/common/src/main/java/net/william278/husksync/data/ItemData.java b/common/src/main/java/net/william278/husksync/data/ItemData.java index 1543e855..a924bcfd 100644 --- a/common/src/main/java/net/william278/husksync/data/ItemData.java +++ b/common/src/main/java/net/william278/husksync/data/ItemData.java @@ -14,6 +14,16 @@ public class ItemData { @SerializedName("serialized_items") public String serializedItems; + /** + * Get an empty item data object, representing an empty inventory or Ender Chest + * + * @return an empty item data object + */ + @NotNull + public static ItemData empty() { + return new ItemData(""); + } + public ItemData(@NotNull final String serializedItems) { this.serializedItems = serializedItems; } 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 a74dc45d..3112a038 100644 --- a/common/src/main/java/net/william278/husksync/data/UserData.java +++ b/common/src/main/java/net/william278/husksync/data/UserData.java @@ -1,6 +1,7 @@ package net.william278.husksync.data; import com.google.gson.annotations.SerializedName; +import net.william278.desertwell.Version; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -86,12 +87,28 @@ public class UserData { * Stores the version of the data format being used */ @SerializedName("format_version") - protected int formatVersion; + protected int formatVersion = CURRENT_FORMAT_VERSION; - 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, + /** + * Create a new {@link UserData} object with the provided data + * + * @param statusData the user's status data ({@link StatusData}) + * @param inventoryData the user's inventory data ({@link ItemData}) + * @param enderChestData the user's ender chest data ({@link ItemData}) + * @param potionEffectData the user's potion effect data ({@link PotionEffectData}) + * @param advancementData the user's advancement data ({@link AdvancementData}) + * @param statisticData the user's statistic data ({@link StatisticsData}) + * @param locationData the user's location data ({@link LocationData}) + * @param persistentDataContainerData the user's persistent data container data ({@link PersistentDataContainerData}) + * @param minecraftVersion the version of Minecraft this data was generated in (e.g. {@code "1.19.2"}) + * @deprecated see {@link #builder(String)} or {@link #builder(Version)} to create a {@link UserDataBuilder}, which + * you can use to {@link UserDataBuilder#build()} a {@link UserData} instance with + */ + @Deprecated(since = "2.1") + public UserData(@Nullable StatusData statusData, @Nullable ItemData inventoryData, + @Nullable ItemData enderChestData, @Nullable PotionEffectData potionEffectData, + @Nullable List advancementData, @Nullable StatisticsData statisticData, + @Nullable LocationData locationData, @Nullable PersistentDataContainerData persistentDataContainerData, @NotNull String minecraftVersion) { this.statusData = statusData; this.inventoryData = inventoryData; @@ -102,7 +119,6 @@ public class UserData { this.locationData = locationData; this.persistentDataContainerData = persistentDataContainerData; this.minecraftVersion = minecraftVersion; - this.formatVersion = CURRENT_FORMAT_VERSION; } // Empty constructor to facilitate json serialization @@ -313,4 +329,29 @@ public class UserData { return formatVersion; } + /** + * Get a new {@link UserDataBuilder} for creating {@link UserData} + * + * @param minecraftVersion the version of Minecraft this data was generated in (e.g. {@code "1.19.2"}) + * @return a UserData {@link UserDataBuilder} instance + * @since 2.1 + */ + @NotNull + public static UserDataBuilder builder(@NotNull String minecraftVersion) { + return new UserDataBuilder(minecraftVersion); + } + + /** + * Get a new {@link UserDataBuilder} for creating {@link UserData} + * + * @param minecraftVersion a {@link Version} object, representing the Minecraft version this data was generated in + * @return a UserData {@link UserDataBuilder} instance + * @since 2.1 + */ + @NotNull + public static UserDataBuilder builder(@NotNull Version minecraftVersion) { + return builder(minecraftVersion.toStringWithoutMetadata()); + } + + } diff --git a/common/src/main/java/net/william278/husksync/data/UserDataBuilder.java b/common/src/main/java/net/william278/husksync/data/UserDataBuilder.java new file mode 100644 index 00000000..3f852d2f --- /dev/null +++ b/common/src/main/java/net/william278/husksync/data/UserDataBuilder.java @@ -0,0 +1,140 @@ +package net.william278.husksync.data; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * A builder utility for creating {@link UserData} instances + * + * @since 2.1 + */ +@SuppressWarnings("UnusedReturnValue") +public class UserDataBuilder { + + @NotNull + private final UserData userData; + + protected UserDataBuilder(@NotNull String minecraftVersion) { + this.userData = new UserData(); + this.userData.minecraftVersion = minecraftVersion; + } + + /** + * Set the {@link StatusData} to this {@link UserData} + * + * @param status the {@link StatusData} to set + * @return this {@link UserDataBuilder} + * @since 2.1 + */ + @NotNull + public UserDataBuilder setStatus(@NotNull StatusData status) { + this.userData.statusData = status; + return this; + } + + /** + * Set the inventory {@link ItemData} to this {@link UserData} + * + * @param inventoryData the inventory {@link ItemData} to set + * @return this {@link UserDataBuilder} + * @since 2.1 + */ + @NotNull + public UserDataBuilder setInventory(@Nullable ItemData inventoryData) { + this.userData.inventoryData = inventoryData; + return this; + } + + /** + * Set the ender chest {@link ItemData} to this {@link UserData} + * + * @param enderChestData the ender chest {@link ItemData} to set + * @return this {@link UserDataBuilder} + * @since 2.1 + */ + @NotNull + public UserDataBuilder setEnderChest(@Nullable ItemData enderChestData) { + this.userData.enderChestData = enderChestData; + return this; + } + + /** + * Set the {@link List} of {@link ItemData} to this {@link UserData} + * + * @param potionEffectData the {@link List} of {@link ItemData} to set + * @return this {@link UserDataBuilder} + * @since 2.1 + */ + @NotNull + public UserDataBuilder setPotionEffects(@Nullable PotionEffectData potionEffectData) { + this.userData.potionEffectData = potionEffectData; + return this; + } + + /** + * Set the {@link List} of {@link ItemData} to this {@link UserData} + * + * @param advancementData the {@link List} of {@link ItemData} to set + * @return this {@link UserDataBuilder} + * @since 2.1 + */ + @NotNull + public UserDataBuilder setAdvancements(@Nullable List advancementData) { + this.userData.advancementData = advancementData; + return this; + } + + /** + * Set the {@link StatisticsData} to this {@link UserData} + * + * @param statisticData the {@link StatisticsData} to set + * @return this {@link UserDataBuilder} + * @since 2.1 + */ + @NotNull + public UserDataBuilder setStatistics(@Nullable StatisticsData statisticData) { + this.userData.statisticData = statisticData; + return this; + } + + + /** + * Set the {@link LocationData} to this {@link UserData} + * + * @param locationData the {@link LocationData} to set + * @return this {@link UserDataBuilder} + * @since 2.1 + */ + @NotNull + public UserDataBuilder setLocation(@Nullable LocationData locationData) { + this.userData.locationData = locationData; + return this; + } + + /** + * Set the {@link PersistentDataContainerData} to this {@link UserData} + * + * @param persistentDataContainerData the {@link PersistentDataContainerData} to set + * @return this {@link UserDataBuilder} + * @since 2.1 + */ + @NotNull + public UserDataBuilder setPersistentDataContainer(@Nullable PersistentDataContainerData persistentDataContainerData) { + this.userData.persistentDataContainerData = persistentDataContainerData; + return this; + } + + /** + * Build and get the {@link UserData} instance + * + * @return the {@link UserData} instance + * @since 2.1 + */ + @NotNull + public UserData build() { + return this.userData; + } + +} 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 4ab72e69..03e3d691 100644 --- a/common/src/main/java/net/william278/husksync/editor/DataEditor.java +++ b/common/src/main/java/net/william278/husksync/editor/DataEditor.java @@ -80,6 +80,7 @@ public class DataEditor { */ public void displayDataOverview(@NotNull OnlineUser user, @NotNull UserDataSnapshot userData, @NotNull User dataOwner) { + // Title message, timestamp, owner and cause. locales.getLocale("data_manager_title", userData.versionUUID().toString().split("-")[0], userData.versionUUID().toString(), @@ -95,19 +96,27 @@ public class DataEditor { locales.getLocale("data_manager_cause", userData.cause().name().toLowerCase().replaceAll("_", " ")) .ifPresent(user::sendMessage); - locales.getLocale("data_manager_status", - Integer.toString((int) userData.userData().getStatusData().health), - Integer.toString((int) userData.userData().getStatusData().maxHealth), - Integer.toString(userData.userData().getStatusData().hunger), - Integer.toString(userData.userData().getStatusData().expLevel), - userData.userData().getStatusData().gameMode.toLowerCase()) + + // User status data, if present in the snapshot + userData.userData().getStatus() + .flatMap(statusData -> locales.getLocale("data_manager_status", + Integer.toString((int) statusData.health), + Integer.toString((int) statusData.maxHealth), + Integer.toString(statusData.hunger), + Integer.toString(statusData.expLevel), + statusData.gameMode.toLowerCase())) .ifPresent(user::sendMessage); - locales.getLocale("data_manager_advancements_statistics", - Integer.toString(userData.userData().getAdvancementData().size()), - generateAdvancementPreview(userData.userData().getAdvancementData()), - String.format("%.2f", (((userData.userData().getStatisticsData().untypedStatistics.getOrDefault( - "PLAY_ONE_MINUTE", 0)) / 20d) / 60d) / 60d)) + + // Advancement and statistic data, if both are present in the snapshot + userData.userData().getAdvancements() + .flatMap(advancementData -> userData.userData().getStatistics() + .flatMap(statisticsData -> locales.getLocale("data_manager_advancements_statistics", + Integer.toString(advancementData.size()), + generateAdvancementPreview(advancementData), + String.format("%.2f", (((statisticsData.untypedStatistics.getOrDefault( + "PLAY_ONE_MINUTE", 0)) / 20d) / 60d) / 60d)))) .ifPresent(user::sendMessage); + if (user.hasPermission(Permission.COMMAND_INVENTORY.node) && user.hasPermission(Permission.COMMAND_ENDER_CHEST.node)) { locales.getLocale("data_manager_item_buttons", diff --git a/common/src/main/java/net/william278/husksync/hook/PlanDataExtension.java b/common/src/main/java/net/william278/husksync/hook/PlanDataExtension.java index 14d16ace..958ebe84 100644 --- a/common/src/main/java/net/william278/husksync/hook/PlanDataExtension.java +++ b/common/src/main/java/net/william278/husksync/hook/PlanDataExtension.java @@ -10,7 +10,6 @@ import com.djrapitops.plan.extension.icon.Family; import com.djrapitops.plan.extension.icon.Icon; import com.djrapitops.plan.extension.table.Table; import com.djrapitops.plan.extension.table.TableColumnFormat; -import net.william278.husksync.data.StatusData; import net.william278.husksync.data.UserDataSnapshot; import net.william278.husksync.database.Database; import net.william278.husksync.player.User; @@ -114,9 +113,9 @@ public class PlanDataExtension implements DataExtension { ) @Tab("Current Status") public String getCurrentDataId(@NotNull UUID uuid) { - return getCurrentUserData(uuid).join().map( - versionedUserData -> versionedUserData.versionUUID().toString() - .split(Pattern.quote("-"))[0]) + return getCurrentUserData(uuid).join() + .map(versionedUserData -> versionedUserData.versionUUID().toString() + .split(Pattern.quote("-"))[0]) .orElse(UNKNOWN_STRING); } @@ -130,11 +129,9 @@ public class PlanDataExtension implements DataExtension { ) @Tab("Current Status") public String getHealth(@NotNull UUID uuid) { - return getCurrentUserData(uuid).join().map( - versionedUserData -> { - final StatusData statusData = versionedUserData.userData().getStatusData(); - return (int) statusData.health + "/" + (int) statusData.maxHealth; - }) + return getCurrentUserData(uuid).join() + .flatMap(versionedUserData -> versionedUserData.userData().getStatus()) + .map(statusData -> (int) statusData.health + "/" + (int) statusData.maxHealth) .orElse(UNKNOWN_STRING); } @@ -148,8 +145,9 @@ public class PlanDataExtension implements DataExtension { ) @Tab("Current Status") public long getHunger(@NotNull UUID uuid) { - return getCurrentUserData(uuid).join().map( - versionedUserData -> (long) versionedUserData.userData().getStatusData().hunger) + return getCurrentUserData(uuid).join() + .flatMap(versionedUserData -> versionedUserData.userData().getStatus()) + .map(statusData -> (long) statusData.hunger) .orElse(0L); } @@ -163,8 +161,9 @@ public class PlanDataExtension implements DataExtension { ) @Tab("Current Status") public long getExperienceLevel(@NotNull UUID uuid) { - return getCurrentUserData(uuid).join().map( - versionedUserData -> (long) versionedUserData.userData().getStatusData().expLevel) + return getCurrentUserData(uuid).join() + .flatMap(versionedUserData -> versionedUserData.userData().getStatus()) + .map(statusData -> (long) statusData.expLevel) .orElse(0L); } @@ -178,8 +177,9 @@ public class PlanDataExtension implements DataExtension { ) @Tab("Current Status") public String getGameMode(@NotNull UUID uuid) { - return getCurrentUserData(uuid).join().map( - versionedUserData -> versionedUserData.userData().getStatusData().gameMode.toLowerCase()) + return getCurrentUserData(uuid).join() + .flatMap(versionedUserData -> versionedUserData.userData().getStatus()) + .map(status -> status.gameMode) .orElse(UNKNOWN_STRING); } @@ -192,8 +192,9 @@ public class PlanDataExtension implements DataExtension { ) @Tab("Current Status") public long getAdvancementsCompleted(@NotNull UUID playerUUID) { - return getCurrentUserData(playerUUID).join().map( - versionedUserData -> (long) versionedUserData.userData().getAdvancementData().size()) + return getCurrentUserData(playerUUID).join() + .flatMap(versionedUserData -> versionedUserData.userData().getAdvancements()) + .map(advancementsData -> (long) advancementsData.size()) .orElse(0L); } @@ -201,7 +202,7 @@ public class PlanDataExtension implements DataExtension { @TableProvider(tableColor = Color.LIGHT_BLUE) @Tab("Data Snapshots") public Table getDataSnapshots(@NotNull UUID playerUUID) { - Table.Factory dataSnapshotsTable = Table.builder() + final Table.Factory dataSnapshotsTable = Table.builder() .columnOne("Time", new Icon(Family.SOLID, "clock", Color.NONE)) .columnOneFormat(TableColumnFormat.DATE_SECOND) .columnTwo("ID", new Icon(Family.SOLID, "bolt", Color.NONE)) 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 b3deb3c0..da718143 100644 --- a/common/src/main/java/net/william278/husksync/player/OnlineUser.java +++ b/common/src/main/java/net/william278/husksync/player/OnlineUser.java @@ -165,11 +165,53 @@ public abstract class OnlineUser extends User { public abstract Version getMinecraftVersion(); /** - * Set {@link UserData} to a player + * Dispatch a MineDown-formatted message to this player + * + * @param mineDown the parsed {@link MineDown} to send + */ + public abstract void sendMessage(@NotNull MineDown mineDown); + + /** + * Dispatch a MineDown-formatted action bar message to this player + * + * @param mineDown the parsed {@link MineDown} to send + */ + public abstract void sendActionBar(@NotNull MineDown mineDown); + + /** + * Returns if the player has the permission node + * + * @param node The permission node string + * @return {@code true} if the player has permission node; {@code false} otherwise + */ + public abstract boolean hasPermission(@NotNull String node); + + /** + * Show the player a {@link ItemEditorMenu} GUI * - * @param data The data to set - * @param settings Plugin settings, for determining what needs setting - * @return a future returning a boolean when complete; if the sync was successful, the future will return {@code true} + * @param menu The {@link ItemEditorMenu} interface to show + */ + public abstract void showMenu(@NotNull ItemEditorMenu menu); + + /** + * Returns true if the player is dead + * + * @return true if the player is dead + */ + public abstract boolean isDead(); + + /** + * Apply {@link UserData} to a player, updating their inventory, status, statistics, etc. as per the config. + *

+ * This will only set data that is enabled as per the enabled settings in the config file. + * Data present in the {@link UserData} object, but not enabled to be set in the config, will be ignored. + * + * @param data The {@link UserData} to set to the player + * @param settings The plugin {@link Settings} to determine which data to set + * @param eventCannon The {@link EventCannon} to fire the synchronisation events + * @param logger The {@link Logger} for debug and error logging + * @param serverMinecraftVersion The server's Minecraft version, for validating the format of the {@link UserData} + * @return a future returning a boolean when complete; if the sync was successful, the future will return {@code true}. */ public final CompletableFuture setData(@NotNull UserData data, @NotNull Settings settings, @NotNull EventCannon eventCannon, @NotNull Logger logger, @@ -196,26 +238,28 @@ public abstract class OnlineUser extends User { final List> dataSetOperations = new ArrayList<>() {{ if (!isOffline() && !preSyncEvent.isCancelled()) { if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)) { - add(setInventory(finalData.getInventoryData())); + finalData.getInventory().ifPresent(itemData -> add(setInventory(itemData))); } if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ENDER_CHESTS)) { - add(setEnderChest(finalData.getEnderChestData())); + finalData.getEnderChest().ifPresent(itemData -> add(setEnderChest(itemData))); } - add(setStatus(finalData.getStatusData(), StatusDataFlag.getFromSettings(settings))); + finalData.getStatus().ifPresent(statusData -> add(setStatus(statusData, + StatusDataFlag.getFromSettings(settings)))); if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_POTION_EFFECTS)) { - add(setPotionEffects(finalData.getPotionEffectsData())); + finalData.getPotionEffects().ifPresent(potionEffectData -> add(setPotionEffects(potionEffectData))); } if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ADVANCEMENTS)) { - add(setAdvancements(finalData.getAdvancementData())); + finalData.getAdvancements().ifPresent(advancementData -> add(setAdvancements(advancementData))); } if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_STATISTICS)) { - add(setStatistics(finalData.getStatisticsData())); + finalData.getStatistics().ifPresent(statisticData -> add(setStatistics(statisticData))); } if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION)) { - add(setLocation(finalData.getLocationData())); + finalData.getLocation().ifPresent(locationData -> add(setLocation(locationData))); } if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER)) { - add(setPersistentDataContainer(finalData.getPersistentDataContainerData())); + finalData.getPersistentDataContainer().ifPresent(persistentDataContainerData -> + add(setPersistentDataContainer(persistentDataContainerData))); } } }}; @@ -232,46 +276,13 @@ public abstract class OnlineUser extends User { } /** - * Dispatch a MineDown-formatted message to this player - * - * @param mineDown the parsed {@link MineDown} to send - */ - public abstract void sendMessage(@NotNull MineDown mineDown); - - /** - * Dispatch a MineDown-formatted action bar message to this player - * - * @param mineDown the parsed {@link MineDown} to send - */ - public abstract void sendActionBar(@NotNull MineDown mineDown); - - /** - * Returns if the player has the permission node - * - * @param node The permission node string - * @return {@code true} if the player has permission node; {@code false} otherwise - */ - public abstract boolean hasPermission(@NotNull String node); - - /** - * Show the player a {@link ItemEditorMenu} GUI - * - * @param menu The {@link ItemEditorMenu} interface to show - */ - public abstract void showMenu(@NotNull ItemEditorMenu menu); - - /** - * Returns true if the player is dead - * - * @return true if the player is dead - */ - public abstract boolean isDead(); - - /** - * Get the player's current {@link UserData} in an {@link Optional} + * Get the player's current {@link UserData} in an {@link Optional}. *

- * If the {@code SYNCHRONIZATION_SAVE_DEAD_PLAYER_INVENTORIES} ConfigOption has been set, - * the user's inventory will only be returned if they are alive + * Since v2.1, this method will respect the data synchronisation settings; user data will only be as big as the + * enabled synchronisation values set in the config file + *

+ * Also note that if the {@code SYNCHRONIZATION_SAVE_DEAD_PLAYER_INVENTORIES} ConfigOption has been set, + * the user's inventory will only be returned if the player is alive. *

* If the user data could not be returned due to an exception, the optional will return empty * @@ -279,12 +290,43 @@ public abstract class OnlineUser extends User { * @return the player's current {@link UserData} in an optional; empty if an exception occurs */ public final CompletableFuture> getUserData(@NotNull Logger logger, @NotNull Settings settings) { - return CompletableFuture.supplyAsync(() -> Optional.of(new UserData(getStatus().join(), - (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SAVE_DEAD_PLAYER_INVENTORIES) - ? getInventory().join() : (isDead() ? new ItemData("") : getInventory().join())), - getEnderChest().join(), getPotionEffects().join(), getAdvancements().join(), - getStatistics().join(), getLocation().join(), getPersistentDataContainer().join(), - getMinecraftVersion().toString()))) + return CompletableFuture.supplyAsync(() -> { + final UserDataBuilder builder = UserData.builder(getMinecraftVersion()); + final List> dataGetOperations = new ArrayList<>() {{ + if (!isOffline()) { + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)) { + if (isDead() && settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SAVE_DEAD_PLAYER_INVENTORIES)) { + add(CompletableFuture.runAsync(() -> builder.setInventory(ItemData.empty()))); + } else { + add(getInventory().thenAccept(builder::setInventory)); + } + } + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ENDER_CHESTS)) { + add(getEnderChest().thenAccept(builder::setEnderChest)); + } + add(getStatus().thenAccept(builder::setStatus)); + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_POTION_EFFECTS)) { + add(getPotionEffects().thenAccept(builder::setPotionEffects)); + } + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ADVANCEMENTS)) { + add(getAdvancements().thenAccept(builder::setAdvancements)); + } + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_STATISTICS)) { + add(getStatistics().thenAccept(builder::setStatistics)); + } + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION)) { + add(getLocation().thenAccept(builder::setLocation)); + } + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER)) { + add(getPersistentDataContainer().thenAccept(builder::setPersistentDataContainer)); + } + } + }}; + + // Apply operations in parallel, join when complete + CompletableFuture.allOf(dataGetOperations.toArray(new CompletableFuture[0])).join(); + return Optional.of(builder.build()); + }) .exceptionally(exception -> { logger.log(Level.SEVERE, "Failed to get user data from online player " + username + " (" + exception.getMessage() + ")"); exception.printStackTrace(); 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 3d420e05..9a34a1e2 100644 --- a/common/src/test/java/net/william278/husksync/data/DataAdaptionTests.java +++ b/common/src/test/java/net/william278/husksync/data/DataAdaptionTests.java @@ -11,7 +11,6 @@ import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; /** @@ -22,28 +21,18 @@ public class DataAdaptionTests { @Test public void testJsonDataAdapter() { final OnlineUser dummyUser = DummyPlayer.create(); - final AtomicBoolean isEquals = new AtomicBoolean(false); dummyUser.getUserData(new DummyLogger(), DummySettings.get()).join().ifPresent(dummyUserData -> { final DataAdapter dataAdapter = new JsonDataAdapter(); final byte[] data = dataAdapter.toBytes(dummyUserData); final UserData deserializedUserData = dataAdapter.fromBytes(data); - 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); + // Assert all deserialized data is equal to the original data + Assertions.assertEquals(dummyUserData.getStatus(), deserializedUserData.getStatus()); + Assertions.assertEquals(dummyUserData.getInventory(), deserializedUserData.getInventory()); + Assertions.assertEquals(dummyUserData.getEnderChest(), deserializedUserData.getEnderChest()); + Assertions.assertEquals(dummyUserData.getAdvancements(), deserializedUserData.getAdvancements()); + Assertions.assertEquals(dummyUserData.getFormatVersion(), deserializedUserData.getFormatVersion()); }); - Assertions.assertTrue(isEquals.get()); } @Test @@ -62,28 +51,18 @@ public class DataAdaptionTests { @Test public void testCompressedDataAdapter() { final OnlineUser dummyUser = DummyPlayer.create(); - AtomicBoolean isEquals = new AtomicBoolean(false); dummyUser.getUserData(new DummyLogger(), DummySettings.get()).join().ifPresent(dummyUserData -> { final DataAdapter dataAdapter = new CompressedDataAdapter(); final byte[] data = dataAdapter.toBytes(dummyUserData); final UserData deserializedUserData = dataAdapter.fromBytes(data); - 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); + // Assert all deserialized data is equal to the original data + Assertions.assertEquals(dummyUserData.getStatus(), deserializedUserData.getStatus()); + Assertions.assertEquals(dummyUserData.getInventory(), deserializedUserData.getInventory()); + Assertions.assertEquals(dummyUserData.getEnderChest(), deserializedUserData.getEnderChest()); + Assertions.assertEquals(dummyUserData.getAdvancements(), deserializedUserData.getAdvancements()); + Assertions.assertEquals(dummyUserData.getFormatVersion(), deserializedUserData.getFormatVersion()); }); - Assertions.assertTrue(isEquals.get()); } private String getTestSerializedPersistentDataContainer() { diff --git a/common/src/test/java/net/william278/husksync/logger/DummyLogger.java b/common/src/test/java/net/william278/husksync/logger/DummyLogger.java index ffd60dba..25ad9f63 100644 --- a/common/src/test/java/net/william278/husksync/logger/DummyLogger.java +++ b/common/src/test/java/net/william278/husksync/logger/DummyLogger.java @@ -1,6 +1,5 @@ package net.william278.husksync.logger; -import de.themoep.minedown.adventure.MineDown; import net.william278.husksync.util.Logger; import org.jetbrains.annotations.NotNull;