Implement variable-sized user data; only save needed data

feat/data-edit-commands
William 2 years ago
parent b9e474d946
commit cbf5d9c24e

@ -66,7 +66,7 @@ public class HuskSyncAPI extends BaseHuskSyncAPI {
return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData -> return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData ->
userData.ifPresent(data -> serializeItemStackArray(inventoryContents) userData.ifPresent(data -> serializeItemStackArray(inventoryContents)
.thenAccept(serializedInventory -> { .thenAccept(serializedInventory -> {
data.getInventoryData().serializedItems = serializedInventory; data.getInventory().orElse(ItemData.empty()).serializedItems = serializedInventory;
setUserData(user, data).join(); setUserData(user, data).join();
})))); }))));
} }
@ -95,7 +95,7 @@ public class HuskSyncAPI extends BaseHuskSyncAPI {
return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData -> return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData ->
userData.ifPresent(data -> serializeItemStackArray(enderChestContents) userData.ifPresent(data -> serializeItemStackArray(enderChestContents)
.thenAccept(serializedInventory -> { .thenAccept(serializedInventory -> {
data.getEnderChestData().serializedItems = serializedInventory; data.getEnderChest().orElse(ItemData.empty()).serializedItems = serializedInventory;
setUserData(user, data).join(); setUserData(user, data).join();
})))); }))));
} }
@ -106,12 +106,14 @@ public class HuskSyncAPI extends BaseHuskSyncAPI {
* @param user the {@link User} to get the {@link BukkitInventoryMap} for * @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, * @return future returning the {@link BukkitInventoryMap} for the given {@link User} if they exist,
* otherwise an empty {@link Optional} * 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 * @since 2.0
*/ */
public CompletableFuture<Optional<BukkitInventoryMap>> getPlayerInventory(@NotNull User user) { public CompletableFuture<Optional<BukkitInventoryMap>> getPlayerInventory(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> getUserData(user).join() return CompletableFuture.supplyAsync(() -> getUserData(user).join()
.map(userData -> deserializeInventory(userData .map(userData -> deserializeInventory(userData.getInventory()
.getInventoryData().serializedItems).join())); .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 * @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, * @return future returning the {@link ItemStack} array of Ender Chest items for the user if they exist,
* otherwise an empty {@link Optional} * 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 * @since 2.0
*/ */
public CompletableFuture<Optional<ItemStack[]>> getPlayerEnderChest(@NotNull User user) { public CompletableFuture<Optional<ItemStack[]>> getPlayerEnderChest(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> getUserData(user).join() return CompletableFuture.supplyAsync(() -> getUserData(user).join()
.map(userData -> deserializeItemStackArray(userData .map(userData -> deserializeItemStackArray(userData.getEnderChest()
.getEnderChestData().serializedItems).join())); .orElse(ItemData.empty()).serializedItems).join()));
} }
/** /**

@ -287,13 +287,16 @@ public class LegacyMigrator extends Migrator {
legacyLocationData == null ? 90f : legacyLocationData.yaw(), legacyLocationData == null ? 90f : legacyLocationData.yaw(),
legacyLocationData == null ? 180f : legacyLocationData.pitch()); legacyLocationData == null ? 180f : legacyLocationData.pitch());
return new UserData(new StatusData(health, maxHealth, healthScale, hunger, saturation, return UserData.builder(minecraftVersion)
saturationExhaustion, selectedSlot, totalExp, expLevel, expProgress, gameMode, isFlying), .setStatus(new StatusData(health, maxHealth, healthScale, hunger, saturation,
new ItemData(serializedInventory), new ItemData(serializedEnderChest), saturationExhaustion, selectedSlot, totalExp, expLevel, expProgress, gameMode, isFlying))
new PotionEffectData(serializedPotionEffects), convertedAdvancements, .setInventory(new ItemData(serializedInventory))
convertedStatisticData, convertedLocationData, .setEnderChest(new ItemData(serializedEnderChest))
new PersistentDataContainerData(new HashMap<>()), .setPotionEffects(new PotionEffectData(serializedPotionEffects))
minecraftVersion); .setAdvancements(convertedAdvancements)
.setStatistics(convertedStatisticData)
.setLocation(convertedLocationData)
.build();
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

@ -280,18 +280,14 @@ public class MpdbMigrator extends Migrator {
} }
// Create user data record // Create user data record
return new UserData(new StatusData(20, 20, 0, 20, 10, return UserData.builder(minecraftVersion)
1, 0, totalExp, expLevel, expProgress, "SURVIVAL", .setStatus(new StatusData(20, 20, 0, 20, 10,
false), 1, 0, totalExp, expLevel, expProgress, "SURVIVAL",
new ItemData(BukkitSerializer.serializeItemStackArray(inventory.getContents()).join()), false))
new ItemData(BukkitSerializer.serializeItemStackArray(converter .setInventory(new ItemData(BukkitSerializer.serializeItemStackArray(inventory.getContents()).join()))
.getItemStackFromSerializedData(serializedEnderChest)).join()), .setEnderChest(new ItemData(BukkitSerializer.serializeItemStackArray(converter
new PotionEffectData(""), new ArrayList<>(), .getItemStackFromSerializedData(serializedEnderChest)).join()))
new StatisticsData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()), .build();
new LocationData("world", UUID.randomUUID(), "NORMAL", 0, 0, 0,
0f, 0f),
new PersistentDataContainerData(new HashMap<>()),
minecraftVersion);
}); });
} }
} }

@ -1,9 +1,7 @@
package net.william278.husksync.command; package net.william278.husksync.command;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSaveCause; import net.william278.husksync.data.*;
import net.william278.husksync.data.UserData;
import net.william278.husksync.data.UserDataSnapshot;
import net.william278.husksync.editor.ItemEditorMenu; import net.william278.husksync.editor.ItemEditorMenu;
import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User; import net.william278.husksync.player.User;
@ -58,7 +56,8 @@ public class EnderChestCommand extends CommandBase implements TabCompletable {
@NotNull User dataOwner, final boolean allowEdit) { @NotNull User dataOwner, final boolean allowEdit) {
CompletableFuture.runAsync(() -> { CompletableFuture.runAsync(() -> {
final UserData data = userDataSnapshot.userData(); 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); dataOwner, player, plugin.getLocales(), allowEdit);
plugin.getLocales().getLocale("viewing_ender_chest_of", dataOwner.username, plugin.getLocales().getLocale("viewing_ender_chest_of", dataOwner.username,
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault()) DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
@ -68,11 +67,18 @@ public class EnderChestCommand extends CommandBase implements TabCompletable {
if (!menu.canEdit) { if (!menu.canEdit) {
return; return;
} }
final UserData updatedUserData = new UserData(data.getStatusData(), data.getInventoryData(),
enderChestDataOnClose, data.getPotionEffectsData(), data.getAdvancementData(), final UserDataBuilder builder = UserData.builder(plugin.getMinecraftVersion());
data.getStatisticsData(), data.getLocationData(), data.getStatus().ifPresent(builder::setStatus);
data.getPersistentDataContainerData(), data.getInventory().ifPresent(builder::setInventory);
plugin.getMinecraftVersion().toString()); 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.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.ENDERCHEST_COMMAND).join();
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join(); plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
}); });

@ -1,9 +1,7 @@
package net.william278.husksync.command; package net.william278.husksync.command;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSaveCause; import net.william278.husksync.data.*;
import net.william278.husksync.data.UserData;
import net.william278.husksync.data.UserDataSnapshot;
import net.william278.husksync.editor.ItemEditorMenu; import net.william278.husksync.editor.ItemEditorMenu;
import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User; import net.william278.husksync.player.User;
@ -58,7 +56,8 @@ public class InventoryCommand extends CommandBase implements TabCompletable {
@NotNull User dataOwner, boolean allowEdit) { @NotNull User dataOwner, boolean allowEdit) {
CompletableFuture.runAsync(() -> { CompletableFuture.runAsync(() -> {
final UserData data = userDataSnapshot.userData(); 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); dataOwner, player, plugin.getLocales(), allowEdit);
plugin.getLocales().getLocale("viewing_inventory_of", dataOwner.username, plugin.getLocales().getLocale("viewing_inventory_of", dataOwner.username,
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault()) DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
@ -68,11 +67,18 @@ public class InventoryCommand extends CommandBase implements TabCompletable {
if (!menu.canEdit) { if (!menu.canEdit) {
return; return;
} }
final UserData updatedUserData = new UserData(data.getStatusData(), inventoryDataOnClose,
data.getEnderChestData(), data.getPotionEffectsData(), data.getAdvancementData(), final UserDataBuilder builder = UserData.builder(plugin.getMinecraftVersion());
data.getStatisticsData(), data.getLocationData(), data.getStatus().ifPresent(builder::setStatus);
data.getPersistentDataContainerData(), data.getEnderChest().ifPresent(builder::setEnderChest);
plugin.getMinecraftVersion().toString()); 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.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND).join();
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join(); plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
}); });

@ -14,6 +14,16 @@ public class ItemData {
@SerializedName("serialized_items") @SerializedName("serialized_items")
public String serializedItems; 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) { public ItemData(@NotNull final String serializedItems) {
this.serializedItems = serializedItems; this.serializedItems = serializedItems;
} }

@ -1,6 +1,7 @@
package net.william278.husksync.data; package net.william278.husksync.data;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import net.william278.desertwell.Version;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -86,12 +87,28 @@ public class UserData {
* Stores the version of the data format being used * Stores the version of the data format being used
*/ */
@SerializedName("format_version") @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, * Create a new {@link UserData} object with the provided data
@NotNull List<AdvancementData> advancementData, @NotNull StatisticsData statisticData, *
@NotNull LocationData locationData, @NotNull PersistentDataContainerData persistentDataContainerData, * @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> advancementData, @Nullable StatisticsData statisticData,
@Nullable LocationData locationData, @Nullable PersistentDataContainerData persistentDataContainerData,
@NotNull String minecraftVersion) { @NotNull String minecraftVersion) {
this.statusData = statusData; this.statusData = statusData;
this.inventoryData = inventoryData; this.inventoryData = inventoryData;
@ -102,7 +119,6 @@ public class UserData {
this.locationData = locationData; this.locationData = locationData;
this.persistentDataContainerData = persistentDataContainerData; this.persistentDataContainerData = persistentDataContainerData;
this.minecraftVersion = minecraftVersion; this.minecraftVersion = minecraftVersion;
this.formatVersion = CURRENT_FORMAT_VERSION;
} }
// Empty constructor to facilitate json serialization // Empty constructor to facilitate json serialization
@ -313,4 +329,29 @@ public class UserData {
return formatVersion; 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());
}
} }

@ -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> 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;
}
}

@ -80,6 +80,7 @@ public class DataEditor {
*/ */
public void displayDataOverview(@NotNull OnlineUser user, @NotNull UserDataSnapshot userData, public void displayDataOverview(@NotNull OnlineUser user, @NotNull UserDataSnapshot userData,
@NotNull User dataOwner) { @NotNull User dataOwner) {
// Title message, timestamp, owner and cause.
locales.getLocale("data_manager_title", locales.getLocale("data_manager_title",
userData.versionUUID().toString().split("-")[0], userData.versionUUID().toString().split("-")[0],
userData.versionUUID().toString(), userData.versionUUID().toString(),
@ -95,19 +96,27 @@ public class DataEditor {
locales.getLocale("data_manager_cause", locales.getLocale("data_manager_cause",
userData.cause().name().toLowerCase().replaceAll("_", " ")) userData.cause().name().toLowerCase().replaceAll("_", " "))
.ifPresent(user::sendMessage); .ifPresent(user::sendMessage);
locales.getLocale("data_manager_status",
Integer.toString((int) userData.userData().getStatusData().health), // User status data, if present in the snapshot
Integer.toString((int) userData.userData().getStatusData().maxHealth), userData.userData().getStatus()
Integer.toString(userData.userData().getStatusData().hunger), .flatMap(statusData -> locales.getLocale("data_manager_status",
Integer.toString(userData.userData().getStatusData().expLevel), Integer.toString((int) statusData.health),
userData.userData().getStatusData().gameMode.toLowerCase()) Integer.toString((int) statusData.maxHealth),
Integer.toString(statusData.hunger),
Integer.toString(statusData.expLevel),
statusData.gameMode.toLowerCase()))
.ifPresent(user::sendMessage); .ifPresent(user::sendMessage);
locales.getLocale("data_manager_advancements_statistics",
Integer.toString(userData.userData().getAdvancementData().size()), // Advancement and statistic data, if both are present in the snapshot
generateAdvancementPreview(userData.userData().getAdvancementData()), userData.userData().getAdvancements()
String.format("%.2f", (((userData.userData().getStatisticsData().untypedStatistics.getOrDefault( .flatMap(advancementData -> userData.userData().getStatistics()
"PLAY_ONE_MINUTE", 0)) / 20d) / 60d) / 60d)) .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); .ifPresent(user::sendMessage);
if (user.hasPermission(Permission.COMMAND_INVENTORY.node) if (user.hasPermission(Permission.COMMAND_INVENTORY.node)
&& user.hasPermission(Permission.COMMAND_ENDER_CHEST.node)) { && user.hasPermission(Permission.COMMAND_ENDER_CHEST.node)) {
locales.getLocale("data_manager_item_buttons", locales.getLocale("data_manager_item_buttons",

@ -10,7 +10,6 @@ import com.djrapitops.plan.extension.icon.Family;
import com.djrapitops.plan.extension.icon.Icon; import com.djrapitops.plan.extension.icon.Icon;
import com.djrapitops.plan.extension.table.Table; import com.djrapitops.plan.extension.table.Table;
import com.djrapitops.plan.extension.table.TableColumnFormat; import com.djrapitops.plan.extension.table.TableColumnFormat;
import net.william278.husksync.data.StatusData;
import net.william278.husksync.data.UserDataSnapshot; import net.william278.husksync.data.UserDataSnapshot;
import net.william278.husksync.database.Database; import net.william278.husksync.database.Database;
import net.william278.husksync.player.User; import net.william278.husksync.player.User;
@ -114,9 +113,9 @@ public class PlanDataExtension implements DataExtension {
) )
@Tab("Current Status") @Tab("Current Status")
public String getCurrentDataId(@NotNull UUID uuid) { public String getCurrentDataId(@NotNull UUID uuid) {
return getCurrentUserData(uuid).join().map( return getCurrentUserData(uuid).join()
versionedUserData -> versionedUserData.versionUUID().toString() .map(versionedUserData -> versionedUserData.versionUUID().toString()
.split(Pattern.quote("-"))[0]) .split(Pattern.quote("-"))[0])
.orElse(UNKNOWN_STRING); .orElse(UNKNOWN_STRING);
} }
@ -130,11 +129,9 @@ public class PlanDataExtension implements DataExtension {
) )
@Tab("Current Status") @Tab("Current Status")
public String getHealth(@NotNull UUID uuid) { public String getHealth(@NotNull UUID uuid) {
return getCurrentUserData(uuid).join().map( return getCurrentUserData(uuid).join()
versionedUserData -> { .flatMap(versionedUserData -> versionedUserData.userData().getStatus())
final StatusData statusData = versionedUserData.userData().getStatusData(); .map(statusData -> (int) statusData.health + "/" + (int) statusData.maxHealth)
return (int) statusData.health + "/" + (int) statusData.maxHealth;
})
.orElse(UNKNOWN_STRING); .orElse(UNKNOWN_STRING);
} }
@ -148,8 +145,9 @@ public class PlanDataExtension implements DataExtension {
) )
@Tab("Current Status") @Tab("Current Status")
public long getHunger(@NotNull UUID uuid) { public long getHunger(@NotNull UUID uuid) {
return getCurrentUserData(uuid).join().map( return getCurrentUserData(uuid).join()
versionedUserData -> (long) versionedUserData.userData().getStatusData().hunger) .flatMap(versionedUserData -> versionedUserData.userData().getStatus())
.map(statusData -> (long) statusData.hunger)
.orElse(0L); .orElse(0L);
} }
@ -163,8 +161,9 @@ public class PlanDataExtension implements DataExtension {
) )
@Tab("Current Status") @Tab("Current Status")
public long getExperienceLevel(@NotNull UUID uuid) { public long getExperienceLevel(@NotNull UUID uuid) {
return getCurrentUserData(uuid).join().map( return getCurrentUserData(uuid).join()
versionedUserData -> (long) versionedUserData.userData().getStatusData().expLevel) .flatMap(versionedUserData -> versionedUserData.userData().getStatus())
.map(statusData -> (long) statusData.expLevel)
.orElse(0L); .orElse(0L);
} }
@ -178,8 +177,9 @@ public class PlanDataExtension implements DataExtension {
) )
@Tab("Current Status") @Tab("Current Status")
public String getGameMode(@NotNull UUID uuid) { public String getGameMode(@NotNull UUID uuid) {
return getCurrentUserData(uuid).join().map( return getCurrentUserData(uuid).join()
versionedUserData -> versionedUserData.userData().getStatusData().gameMode.toLowerCase()) .flatMap(versionedUserData -> versionedUserData.userData().getStatus())
.map(status -> status.gameMode)
.orElse(UNKNOWN_STRING); .orElse(UNKNOWN_STRING);
} }
@ -192,8 +192,9 @@ public class PlanDataExtension implements DataExtension {
) )
@Tab("Current Status") @Tab("Current Status")
public long getAdvancementsCompleted(@NotNull UUID playerUUID) { public long getAdvancementsCompleted(@NotNull UUID playerUUID) {
return getCurrentUserData(playerUUID).join().map( return getCurrentUserData(playerUUID).join()
versionedUserData -> (long) versionedUserData.userData().getAdvancementData().size()) .flatMap(versionedUserData -> versionedUserData.userData().getAdvancements())
.map(advancementsData -> (long) advancementsData.size())
.orElse(0L); .orElse(0L);
} }
@ -201,7 +202,7 @@ public class PlanDataExtension implements DataExtension {
@TableProvider(tableColor = Color.LIGHT_BLUE) @TableProvider(tableColor = Color.LIGHT_BLUE)
@Tab("Data Snapshots") @Tab("Data Snapshots")
public Table getDataSnapshots(@NotNull UUID playerUUID) { 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)) .columnOne("Time", new Icon(Family.SOLID, "clock", Color.NONE))
.columnOneFormat(TableColumnFormat.DATE_SECOND) .columnOneFormat(TableColumnFormat.DATE_SECOND)
.columnTwo("ID", new Icon(Family.SOLID, "bolt", Color.NONE)) .columnTwo("ID", new Icon(Family.SOLID, "bolt", Color.NONE))

@ -165,11 +165,53 @@ public abstract class OnlineUser extends User {
public abstract Version getMinecraftVersion(); 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 menu The {@link ItemEditorMenu} interface to show
* @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} 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.
* <p>
* 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<Boolean> setData(@NotNull UserData data, @NotNull Settings settings, public final CompletableFuture<Boolean> setData(@NotNull UserData data, @NotNull Settings settings,
@NotNull EventCannon eventCannon, @NotNull Logger logger, @NotNull EventCannon eventCannon, @NotNull Logger logger,
@ -196,26 +238,28 @@ public abstract class OnlineUser extends User {
final List<CompletableFuture<Void>> dataSetOperations = new ArrayList<>() {{ final List<CompletableFuture<Void>> dataSetOperations = new ArrayList<>() {{
if (!isOffline() && !preSyncEvent.isCancelled()) { if (!isOffline() && !preSyncEvent.isCancelled()) {
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)) { 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)) { 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)) { 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)) { 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)) { 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)) { 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)) { 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 * Get the player's current {@link UserData} in an {@link Optional}.
*
* @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}
* <p> * <p>
* If the {@code SYNCHRONIZATION_SAVE_DEAD_PLAYER_INVENTORIES} ConfigOption has been set, * Since v2.1, this method will respect the data synchronisation settings; user data will only be as big as the
* the user's inventory will only be returned if they are alive * enabled synchronisation values set in the config file
* <p>
* 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.
* <p> * <p>
* If the user data could not be returned due to an exception, the optional will return empty * 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 * @return the player's current {@link UserData} in an optional; empty if an exception occurs
*/ */
public final CompletableFuture<Optional<UserData>> getUserData(@NotNull Logger logger, @NotNull Settings settings) { public final CompletableFuture<Optional<UserData>> getUserData(@NotNull Logger logger, @NotNull Settings settings) {
return CompletableFuture.supplyAsync(() -> Optional.of(new UserData(getStatus().join(), return CompletableFuture.supplyAsync(() -> {
(settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SAVE_DEAD_PLAYER_INVENTORIES) final UserDataBuilder builder = UserData.builder(getMinecraftVersion());
? getInventory().join() : (isDead() ? new ItemData("") : getInventory().join())), final List<CompletableFuture<Void>> dataGetOperations = new ArrayList<>() {{
getEnderChest().join(), getPotionEffects().join(), getAdvancements().join(), if (!isOffline()) {
getStatistics().join(), getLocation().join(), getPersistentDataContainer().join(), if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)) {
getMinecraftVersion().toString()))) 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 -> { .exceptionally(exception -> {
logger.log(Level.SEVERE, "Failed to get user data from online player " + username + " (" + exception.getMessage() + ")"); logger.log(Level.SEVERE, "Failed to get user data from online player " + username + " (" + exception.getMessage() + ")");
exception.printStackTrace(); exception.printStackTrace();

@ -11,7 +11,6 @@ import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
/** /**
@ -22,28 +21,18 @@ public class DataAdaptionTests {
@Test @Test
public void testJsonDataAdapter() { public void testJsonDataAdapter() {
final OnlineUser dummyUser = DummyPlayer.create(); final OnlineUser dummyUser = DummyPlayer.create();
final AtomicBoolean isEquals = new AtomicBoolean(false);
dummyUser.getUserData(new DummyLogger(), DummySettings.get()).join().ifPresent(dummyUserData -> { dummyUser.getUserData(new DummyLogger(), DummySettings.get()).join().ifPresent(dummyUserData -> {
final DataAdapter dataAdapter = new JsonDataAdapter(); final DataAdapter dataAdapter = new JsonDataAdapter();
final byte[] data = dataAdapter.toBytes(dummyUserData); final byte[] data = dataAdapter.toBytes(dummyUserData);
final UserData deserializedUserData = dataAdapter.fromBytes(data); final UserData deserializedUserData = dataAdapter.fromBytes(data);
isEquals.set(deserializedUserData.getInventoryData().serializedItems // Assert all deserialized data is equal to the original data
.equals(dummyUserData.getInventoryData().serializedItems) Assertions.assertEquals(dummyUserData.getStatus(), deserializedUserData.getStatus());
&& deserializedUserData.getEnderChestData().serializedItems Assertions.assertEquals(dummyUserData.getInventory(), deserializedUserData.getInventory());
.equals(dummyUserData.getEnderChestData().serializedItems) Assertions.assertEquals(dummyUserData.getEnderChest(), deserializedUserData.getEnderChest());
&& deserializedUserData.getPotionEffectsData().serializedPotionEffects Assertions.assertEquals(dummyUserData.getAdvancements(), deserializedUserData.getAdvancements());
.equals(dummyUserData.getPotionEffectsData().serializedPotionEffects) Assertions.assertEquals(dummyUserData.getFormatVersion(), deserializedUserData.getFormatVersion());
&& 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 @Test
@ -62,28 +51,18 @@ public class DataAdaptionTests {
@Test @Test
public void testCompressedDataAdapter() { public void testCompressedDataAdapter() {
final OnlineUser dummyUser = DummyPlayer.create(); final OnlineUser dummyUser = DummyPlayer.create();
AtomicBoolean isEquals = new AtomicBoolean(false);
dummyUser.getUserData(new DummyLogger(), DummySettings.get()).join().ifPresent(dummyUserData -> { dummyUser.getUserData(new DummyLogger(), DummySettings.get()).join().ifPresent(dummyUserData -> {
final DataAdapter dataAdapter = new CompressedDataAdapter(); final DataAdapter dataAdapter = new CompressedDataAdapter();
final byte[] data = dataAdapter.toBytes(dummyUserData); final byte[] data = dataAdapter.toBytes(dummyUserData);
final UserData deserializedUserData = dataAdapter.fromBytes(data); final UserData deserializedUserData = dataAdapter.fromBytes(data);
isEquals.set(deserializedUserData.getInventoryData().serializedItems // Assert all deserialized data is equal to the original data
.equals(dummyUserData.getInventoryData().serializedItems) Assertions.assertEquals(dummyUserData.getStatus(), deserializedUserData.getStatus());
&& deserializedUserData.getEnderChestData().serializedItems Assertions.assertEquals(dummyUserData.getInventory(), deserializedUserData.getInventory());
.equals(dummyUserData.getEnderChestData().serializedItems) Assertions.assertEquals(dummyUserData.getEnderChest(), deserializedUserData.getEnderChest());
&& deserializedUserData.getPotionEffectsData().serializedPotionEffects Assertions.assertEquals(dummyUserData.getAdvancements(), deserializedUserData.getAdvancements());
.equals(dummyUserData.getPotionEffectsData().serializedPotionEffects) Assertions.assertEquals(dummyUserData.getFormatVersion(), deserializedUserData.getFormatVersion());
&& 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());
} }
private String getTestSerializedPersistentDataContainer() { private String getTestSerializedPersistentDataContainer() {

@ -1,6 +1,5 @@
package net.william278.husksync.logger; package net.william278.husksync.logger;
import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.util.Logger; import net.william278.husksync.util.Logger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;

Loading…
Cancel
Save