Events & API work, save DataSaveCauses as part of versioning

feat/data-edit-commands
William 2 years ago
parent fd08a3e7d0
commit 1c9d74f925

@ -1,4 +1,181 @@
package net.william278.husksync.api;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.data.*;
import net.william278.husksync.player.BukkitPlayer;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
* The HuskSync API for the Bukkit platform, providing methods to access and modify player {@link UserData} held by {@link User}s.
* </p>
* Retrieve an instance of the API class via {@link #getInstance()}.
*/
@SuppressWarnings("unused")
public class HuskSyncAPI {
/**
* <b>(Internal use only)</b> - Instance of the API class.
*/
private static final HuskSyncAPI INSTANCE = new HuskSyncAPI();
/**
* <b>(Internal use only)</b> - Instance of the implementing plugin.
*/
private static final BukkitHuskSync PLUGIN = BukkitHuskSync.getInstance();
/**
* <b>(Internal use only)</b> - Constructor.
*/
private HuskSyncAPI() {
}
/**
* Entrypoint to the HuskSync API - returns an instance of the API
*
* @return instance of the HuskSync API
*/
public static @NotNull HuskSyncAPI getInstance() {
return INSTANCE;
}
/**
* Returns a {@link User} instance for the given bukkit {@link Player}.
*
* @param player the bukkit player to get the {@link User} instance for
* @return the {@link User} instance for the given bukkit player
*/
@NotNull
public OnlineUser getUser(@NotNull Player player) {
return BukkitPlayer.adapt(player);
}
/**
* Returns a {@link User} by the given player's account {@link UUID}, if they exist.
*
* @param uuid the unique id of the player to get the {@link User} instance for
* @return future returning the {@link User} instance for the given player's unique id if they exist, otherwise an empty {@link Optional}
* @apiNote The player does not have to be online
*/
public CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid) {
return PLUGIN.getDatabase().getUser(uuid);
}
/**
* Returns a {@link User} by the given player's username (case-insensitive), if they exist.
*
* @param username the username of the {@link User} instance for
* @return future returning the {@link User} instance for the given player's username if they exist, otherwise an empty {@link Optional}
* @apiNote The player does not have to be online, though their username has to be the username
* they had when they last joined the server.
*/
public CompletableFuture<Optional<User>> getUser(@NotNull String username) {
return PLUGIN.getDatabase().getUserByName(username);
}
/**
* Returns a {@link User}'s current {@link UserData}
*
* @param user the {@link User} to get the {@link UserData} for
* @return future returning the {@link UserData} for the given {@link User} if they exist, otherwise an empty {@link Optional}
* @apiNote If the user is not online on the implementing bukkit server,
* the {@link UserData} returned will be their last database-saved UserData.</p>
* If the user happens to be online on another server on the network,
* then the {@link UserData} returned here may not be reflective of their actual current UserData.
*/
public CompletableFuture<Optional<UserData>> getUserData(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> {
if (user instanceof OnlineUser) {
return Optional.of(((OnlineUser) user).getUserData().join());
} else {
return PLUGIN.getDatabase().getCurrentUserData(user).join().map(VersionedUserData::userData);
}
});
}
/**
* Returns the saved {@link VersionedUserData} records for the given {@link User}
*
* @param user the {@link User} to get the {@link VersionedUserData} for
* @return future returning a list {@link VersionedUserData} for the given {@link User} if they exist,
* otherwise an empty {@link Optional}
* @apiNote The length of the list of VersionedUserData will correspond to the configured
* {@code max_user_data_records} config option
*/
public CompletableFuture<List<VersionedUserData>> getSavedUserData(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> PLUGIN.getDatabase().getUserData(user).join());
}
/**
* Returns the JSON string representation of the given {@link UserData}
*
* @param userData the {@link UserData} to get the JSON string representation of
* @param prettyPrint whether to pretty print the JSON string
* @return the JSON string representation of the given {@link UserData}
*/
@NotNull
public String getUserDataJson(@NotNull UserData userData, boolean prettyPrint) {
return PLUGIN.getDataAdapter().toJson(userData, prettyPrint);
}
/**
* Returns a {@link BukkitInventoryMap} for the given {@link User}, containing their current inventory item data
*
* @param user the {@link User} to get the {@link BukkitInventoryMap} for
* @return future returning the {@link BukkitInventoryMap} for the given {@link User} if they exist,
* otherwise an empty {@link Optional}
*/
public CompletableFuture<Optional<BukkitInventoryMap>> getPlayerInventory(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> getUserData(user).join()
.map(userData -> BukkitSerializer.deserializeInventory(userData
.getInventoryData().serializedItems).join()));
}
/**
* Returns the {@link ItemStack}s array contents of the given {@link User}'s Ender Chest data
*
* @param user the {@link User} to get the Ender Chest contents of
* @return future returning the {@link ItemStack} array of Ender Chest items for the user if they exist,
* otherwise an empty {@link Optional}
*/
public CompletableFuture<Optional<ItemStack[]>> getPlayerEnderChest(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> getUserData(user).join()
.map(userData -> BukkitSerializer.deserializeItemStackArray(userData
.getEnderChestData().serializedItems).join()));
}
/**
* Deserialize a Base-64 encoded inventory array string into a {@link ItemStack} array.
*
* @param serializedItemStackArray The Base-64 encoded inventory array string.
* @return The deserialized {@link ItemStack} array.
* @throws DataDeserializationException If an error occurs during deserialization.
*/
public CompletableFuture<ItemStack[]> deserializeItemStackArray(@NotNull String serializedItemStackArray)
throws DataDeserializationException {
return CompletableFuture.supplyAsync(() -> BukkitSerializer
.deserializeItemStackArray(serializedItemStackArray).join());
}
/**
* Deserialize a Base-64 encoded potion effect array string into a {@link PotionEffect} array.
*
* @param serializedPotionEffectArray The Base-64 encoded potion effect array string.
* @return The deserialized {@link PotionEffect} array.
* @throws DataDeserializationException If an error occurs during deserialization.
*/
public CompletableFuture<PotionEffect[]> deserializePotionEffectArray(@NotNull String serializedPotionEffectArray)
throws DataDeserializationException {
return CompletableFuture.supplyAsync(() -> BukkitSerializer
.deserializePotionEffects(serializedPotionEffectArray).join());
}
}

@ -32,6 +32,15 @@ allprojects {
maven { url 'https://jitpack.io' }
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
}
test {
useJUnitPlatform()
}
processResources {
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
tokens: rootProject.ext.properties

@ -15,6 +15,8 @@ import net.william278.husksync.data.JsonDataAdapter;
import net.william278.husksync.database.Database;
import net.william278.husksync.database.MySqlDatabase;
import net.william278.husksync.editor.DataEditor;
import net.william278.husksync.event.BukkitEventCannon;
import net.william278.husksync.event.EventCannon;
import net.william278.husksync.listener.BukkitEventListener;
import net.william278.husksync.listener.EventListener;
import net.william278.husksync.player.BukkitPlayer;
@ -51,12 +53,18 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
private DataEditor dataEditor;
private EventCannon eventCannon;
private Settings settings;
private Locales locales;
private static BukkitHuskSync instance;
/**
* (<b>Internal use only)</b> Returns the instance of the implementing Bukkit plugin
*
* @return the instance of the Bukkit plugin
*/
public static BukkitHuskSync getInstance() {
return instance;
}
@ -90,6 +98,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
getLoggingAdapter().log(Level.INFO, "Loading plugin configuration settings & locales...");
return reload().thenApply(loadedSettings -> {
if (loadedSettings) {
logger.showDebugLogs(settings.getBooleanValue(Settings.ConfigOption.DEBUG_LOGGING));
getLoggingAdapter().log(Level.INFO, "Successfully loaded plugin configuration settings & locales");
} else {
getLoggingAdapter().log(Level.SEVERE, "Failed to load plugin configuration settings and/or locales");
@ -106,6 +115,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
}
}
return succeeded;
}).thenApply(succeeded -> {
// Prepare event cannon
if (succeeded) {
eventCannon = new BukkitEventCannon();
}
return succeeded;
}).thenApply(succeeded -> {
// Prepare data editor
if (succeeded) {
@ -114,15 +129,14 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
return succeeded;
}).thenApply(succeeded -> {
// Establish connection to the database
this.database = new MySqlDatabase(settings, resourceReader, logger, dataAdapter);
if (succeeded) {
this.database = new MySqlDatabase(settings, resourceReader, logger, dataAdapter, eventCannon);
getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the database...");
final CompletableFuture<Boolean> databaseConnectFuture = new CompletableFuture<>();
Bukkit.getScheduler().runTask(this, () -> {
final boolean initialized = this.database.initialize();
if (!initialized) {
getLoggingAdapter().log(Level.SEVERE, "Failed to establish a connection to the database. "
+ "Please check the supplied database credentials in the config file");
getLoggingAdapter().log(Level.SEVERE, "Failed to establish a connection to the database. " + "Please check the supplied database credentials in the config file");
databaseConnectFuture.completeAsync(() -> false);
return;
}
@ -134,13 +148,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
return false;
}).thenApply(succeeded -> {
// Establish connection to the Redis server
this.redisManager = new RedisManager(settings, dataAdapter);
if (succeeded) {
this.redisManager = new RedisManager(settings, dataAdapter, logger);
getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the Redis server...");
return this.redisManager.initialize().thenApply(initialized -> {
if (!initialized) {
getLoggingAdapter().log(Level.SEVERE, "Failed to establish a connection to the Redis server. "
+ "Please check the supplied Redis credentials in the config file");
getLoggingAdapter().log(Level.SEVERE, "Failed to establish a connection to the Redis server. " + "Please check the supplied Redis credentials in the config file");
return false;
}
getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the Redis server");
@ -178,7 +191,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
return succeeded;
}).thenApply(succeeded -> {
// Check for updates
if (settings.getBooleanValue(Settings.ConfigOption.CHECK_FOR_UPDATES) && succeeded) {
if (succeeded && settings.getBooleanValue(Settings.ConfigOption.CHECK_FOR_UPDATES)) {
getLoggingAdapter().log(Level.INFO, "Checking for updates...");
new UpdateChecker(getVersion(), getLoggingAdapter()).logToConsole();
}
@ -186,8 +199,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
}).thenAccept(succeeded -> {
// Handle failed initialization
if (!succeeded) {
getLoggingAdapter().log(Level.SEVERE, "Failed to initialize HuskSync. " +
"The plugin will now be disabled");
getLoggingAdapter().log(Level.SEVERE, "Failed to initialize HuskSync. " + "The plugin will now be disabled");
getServer().getPluginManager().disablePlugin(this);
} else {
getLoggingAdapter().log(Level.INFO, "Successfully enabled HuskSync v" + getVersion());
@ -237,6 +249,11 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
return dataEditor;
}
@Override
public @NotNull EventCannon getEventCannon() {
return eventCannon;
}
@Override
public @NotNull Settings getSettings() {
return settings;

@ -0,0 +1,125 @@
package net.william278.husksync.data;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
/**
* A mapped player inventory, providing methods to easily access a player's inventory.
*/
public class BukkitInventoryMap {
private ItemStack[] contents;
/**
* Creates a new mapped inventory from the given contents.
*
* @param contents the contents of the inventory
*/
protected BukkitInventoryMap(ItemStack[] contents) {
this.contents = contents;
}
/**
* Gets the contents of the inventory.
*
* @return the contents of the inventory
*/
public ItemStack[] getContents() {
return contents;
}
/**
* Set the contents of the inventory.
*
* @param contents the contents of the inventory
*/
public void setContents(ItemStack[] contents) {
this.contents = contents;
}
/**
* Gets the size of the inventory.
*
* @return the size of the inventory
*/
public int getSize() {
return contents.length;
}
/**
* Gets the item at the given index.
*
* @param index the index of the item to get
* @return the item at the given index
*/
public Optional<ItemStack> getItemAt(int index) {
if (contents.length >= index) {
if (contents[index] == null) {
return Optional.empty();
}
return Optional.of(contents[index]);
}
return Optional.empty();
}
/**
* Sets the item at the given index.
*
* @param itemStack the item to set at the given index
* @param index the index of the item to set
* @throws IllegalArgumentException if the index is out of bounds
*/
public void setItemAt(@NotNull ItemStack itemStack, int index) throws IllegalArgumentException {
contents[index] = itemStack;
}
/**
* Returns the main inventory contents.
*
* @return the main inventory contents
*/
public ItemStack[] getInventory() {
final ItemStack[] inventory = new ItemStack[36];
System.arraycopy(contents, 0, inventory, 0, Math.min(contents.length, inventory.length));
return inventory;
}
public ItemStack[] getHotbar() {
final ItemStack[] armor = new ItemStack[9];
for (int i = 0; i <= 9; i++) {
armor[i] = getItemAt(i).orElse(null);
}
return armor;
}
public Optional<ItemStack> getOffHand() {
return getItemAt(40);
}
public Optional<ItemStack> getHelmet() {
return getItemAt(39);
}
public Optional<ItemStack> getChestplate() {
return getItemAt(38);
}
public Optional<ItemStack> getLeggings() {
return getItemAt(37);
}
public Optional<ItemStack> getBoots() {
return getItemAt(36);
}
public ItemStack[] getArmor() {
final ItemStack[] armor = new ItemStack[4];
for (int i = 36; i < 40; i++) {
armor[i - 36] = getItemAt(i).orElse(null);
}
return armor;
}
}

@ -4,6 +4,7 @@ import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
@ -21,7 +22,8 @@ public class BukkitSerializer {
* @param inventoryContents The contents of the inventory
* @return The serialized inventory contents
*/
public static CompletableFuture<String> serializeInventory(ItemStack[] inventoryContents) throws DataDeserializationException {
public static CompletableFuture<String> serializeItemStackArray(ItemStack[] inventoryContents)
throws DataDeserializationException {
return CompletableFuture.supplyAsync(() -> {
// Return an empty string if there is no inventory item data to serialize
if (inventoryContents.length == 0) {
@ -49,20 +51,35 @@ public class BukkitSerializer {
}
/**
* Returns an array of ItemStacks from serialized inventory data. Note: empty slots will be represented by {@code null}
* Returns a {@link BukkitInventoryMap} from a serialized array of ItemStacks representing the contents of a player's inventory.
*
* @param inventoryData The serialized {@link ItemStack[]} array
* @return The inventory contents as an array of {@link ItemStack}s
* @param serializedPlayerInventory The serialized {@link ItemStack[]} inventory array
* @return The deserialized ItemStacks, mapped for convenience as a {@link BukkitInventoryMap}
* @throws DataDeserializationException If the serialized item stack array could not be deserialized
*/
public static CompletableFuture<ItemStack[]> deserializeInventory(String inventoryData) throws DataDeserializationException {
public static CompletableFuture<BukkitInventoryMap> deserializeInventory(@NotNull String serializedPlayerInventory)
throws DataDeserializationException {
return CompletableFuture.supplyAsync(() -> new BukkitInventoryMap(deserializeItemStackArray(serializedPlayerInventory).join()));
}
/**
* Returns an array of ItemStacks from serialized inventory data.
*
* @param serializeItemStackArray The serialized {@link ItemStack[]} array
* @return The deserialized array of {@link ItemStack}s
* @throws DataDeserializationException If the serialized item stack array could not be deserialized
* @implNote Empty slots will be represented by {@code null}
*/
public static CompletableFuture<ItemStack[]> deserializeItemStackArray(String serializeItemStackArray)
throws DataDeserializationException {
return CompletableFuture.supplyAsync(() -> {
// Return empty array if there is no inventory data (set the player as having an empty inventory)
if (inventoryData.isEmpty()) {
if (serializeItemStackArray.isEmpty()) {
return new ItemStack[0];
}
// Create a byte input stream to read the serialized data
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(inventoryData))) {
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(serializeItemStackArray))) {
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
// Read the length of the Bukkit input stream and set the length of the array to this value
ItemStack[] inventoryContents = new ItemStack[bukkitInputStream.readInt()];

@ -0,0 +1,60 @@
package net.william278.husksync.event;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.UserData;
import net.william278.husksync.player.User;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
public class BukkitDataSavePlayerEvent extends BukkitEvent implements DataSaveEvent, Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
private boolean cancelled = false;
private UserData userData;
private final User user;
private final DataSaveCause saveCause;
protected BukkitDataSavePlayerEvent(@NotNull User user, @NotNull UserData userData,
@NotNull DataSaveCause saveCause) {
this.user = user;
this.userData = userData;
this.saveCause = saveCause;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
@NotNull
@Override
public User getUser() {
return user;
}
@Override
public @NotNull UserData getUserData() {
return userData;
}
@Override
public void setUserData(@NotNull UserData userData) {
this.userData = userData;
}
@Override
public @NotNull DataSaveCause getSaveCause() {
return saveCause;
}
@NotNull
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
}

@ -0,0 +1,25 @@
package net.william278.husksync.event;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.player.BukkitPlayer;
import net.william278.husksync.player.OnlineUser;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
public abstract class BukkitEvent extends Event implements net.william278.husksync.event.Event {
@Override
public CompletableFuture<net.william278.husksync.event.Event> fire() {
final CompletableFuture<net.william278.husksync.event.Event> eventFireFuture = new CompletableFuture<>();
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
Bukkit.getServer().getPluginManager().callEvent(this);
eventFireFuture.complete(this);
});
return eventFireFuture;
}
}

@ -0,0 +1,33 @@
package net.william278.husksync.event;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.UserData;
import net.william278.husksync.player.BukkitPlayer;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
public class BukkitEventCannon extends EventCannon {
public BukkitEventCannon() {
}
@Override
public CompletableFuture<Event> firePreSyncEvent(@NotNull OnlineUser user, @NotNull UserData userData) {
return new BukkitPreSyncEvent(((BukkitPlayer) user).getPlayer(), userData).fire();
}
@Override
public CompletableFuture<Event> fireDataSaveEvent(@NotNull User user, @NotNull UserData userData,
@NotNull DataSaveCause saveCause) {
return new BukkitDataSavePlayerEvent(user, userData, saveCause).fire();
}
@Override
public void fireSyncCompleteEvent(@NotNull OnlineUser user) {
new BukkitSyncCompletePlayerEvent(((BukkitPlayer) user).getPlayer()).fire();
}
}

@ -0,0 +1,34 @@
package net.william278.husksync.event;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.player.BukkitPlayer;
import net.william278.husksync.player.OnlineUser;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
public abstract class BukkitPlayerEvent extends org.bukkit.event.player.PlayerEvent implements PlayerEvent {
public BukkitPlayerEvent(@NotNull Player who) {
super(who);
}
@Override
public OnlineUser getUser() {
return BukkitPlayer.adapt(player);
}
@Override
public CompletableFuture<Event> fire() {
final CompletableFuture<Event> eventFireFuture = new CompletableFuture<>();
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
Bukkit.getServer().getPluginManager().callEvent(this);
eventFireFuture.complete(this);
});
return eventFireFuture;
}
}

@ -0,0 +1,51 @@
package net.william278.husksync.event;
import net.william278.husksync.data.UserData;
import net.william278.husksync.player.BukkitPlayer;
import net.william278.husksync.player.OnlineUser;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEvent, Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
private boolean cancelled = false;
private UserData userData;
protected BukkitPreSyncEvent(@NotNull Player player, @NotNull UserData userData) {
super(player);
this.userData = userData;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
@Override
public OnlineUser getUser() {
return BukkitPlayer.adapt(player);
}
@Override
public @NotNull UserData getUserData() {
return userData;
}
@Override
public void setUserData(@NotNull UserData userData) {
this.userData = userData;
}
@NotNull
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
}

@ -0,0 +1,26 @@
package net.william278.husksync.event;
import net.william278.husksync.player.BukkitPlayer;
import net.william278.husksync.player.OnlineUser;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
public class BukkitSyncCompletePlayerEvent extends BukkitPlayerEvent implements SyncCompleteEvent {
private static final HandlerList HANDLER_LIST = new HandlerList();
protected BukkitSyncCompletePlayerEvent(@NotNull Player player) {
super(player);
}
@Override
public OnlineUser getUser() {
return BukkitPlayer.adapt(player);
}
@NotNull
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
}

@ -3,7 +3,7 @@ package net.william278.husksync.listener;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.data.BukkitSerializer;
import net.william278.husksync.data.DataDeserializationException;
import net.william278.husksync.data.InventoryData;
import net.william278.husksync.data.ItemData;
import net.william278.husksync.player.BukkitPlayer;
import net.william278.husksync.player.OnlineUser;
import org.bukkit.Bukkit;
@ -41,7 +41,6 @@ public class BukkitEventListener extends EventListener implements Listener {
@EventHandler
public void onPlayerQuit(@NotNull PlayerQuitEvent event) {
super.handlePlayerQuit(BukkitPlayer.adapt(event.getPlayer()));
BukkitPlayer.remove(event.getPlayer());
}
@EventHandler(ignoreCancelled = true)
@ -56,8 +55,8 @@ public class BukkitEventListener extends EventListener implements Listener {
final OnlineUser user = BukkitPlayer.adapt(player);
if (huskSync.getDataEditor().isEditingInventoryData(user)) {
try {
BukkitSerializer.serializeInventory(event.getInventory().getContents()).thenAccept(
serializedInventory -> super.handleMenuClose(user, new InventoryData(serializedInventory)));
BukkitSerializer.serializeItemStackArray(event.getInventory().getContents()).thenAccept(
serializedInventory -> super.handleMenuClose(user, new ItemData(serializedInventory)));
} catch (DataDeserializationException e) {
huskSync.getLoggingAdapter().log(Level.SEVERE,
"Failed to serialize inventory data during menu close", e);

@ -31,7 +31,6 @@ import java.util.concurrent.atomic.AtomicReference;
*/
public class BukkitPlayer extends OnlineUser {
private static final HashMap<UUID, BukkitPlayer> cachedPlayers = new HashMap<>();
private final Player player;
private BukkitPlayer(@NotNull Player player) {
@ -40,16 +39,11 @@ public class BukkitPlayer extends OnlineUser {
}
public static BukkitPlayer adapt(@NotNull Player player) {
if (cachedPlayers.containsKey(player.getUniqueId())) {
return cachedPlayers.get(player.getUniqueId());
}
final BukkitPlayer bukkitPlayer = new BukkitPlayer(player);
cachedPlayers.put(player.getUniqueId(), bukkitPlayer);
return bukkitPlayer;
return new BukkitPlayer(player);
}
public static void remove(@NotNull Player player) {
cachedPlayers.remove(player.getUniqueId());
public Player getPlayer() {
return player;
}
@Override
@ -73,21 +67,18 @@ public class BukkitPlayer extends OnlineUser {
@Override
public CompletableFuture<Void> setStatus(@NotNull StatusData statusData,
final boolean setHealth, final boolean setMaxHealth,
final boolean setHunger, final boolean setExperience,
final boolean setGameMode, final boolean setFlying,
final boolean setSelectedItemSlot) {
@NotNull List<StatusDataFlag> statusDataFlags) {
return CompletableFuture.runAsync(() -> {
double currentMaxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH))
.getBaseValue();
if (setMaxHealth) {
if (statusDataFlags.contains(StatusDataFlag.SET_MAX_HEALTH)) {
if (statusData.maxHealth != 0d) {
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH))
.setBaseValue(statusData.maxHealth);
currentMaxHealth = statusData.maxHealth;
}
}
if (setHealth) {
if (statusDataFlags.contains(StatusDataFlag.SET_HEALTH)) {
final double currentHealth = player.getHealth();
if (statusData.health != currentHealth) {
player.setHealth(currentHealth > currentMaxHealth ? currentMaxHealth : statusData.health);
@ -100,24 +91,24 @@ public class BukkitPlayer extends OnlineUser {
}
player.setHealthScaled(statusData.healthScale != 0D);
}
if (setHunger) {
if (statusDataFlags.contains(StatusDataFlag.SET_HUNGER)) {
player.setFoodLevel(statusData.hunger);
player.setSaturation(statusData.saturation);
player.setExhaustion(statusData.saturationExhaustion);
}
if (setSelectedItemSlot) {
if (statusDataFlags.contains(StatusDataFlag.SET_SELECTED_ITEM_SLOT)) {
player.getInventory().setHeldItemSlot(statusData.selectedItemSlot);
}
if (setExperience) {
if (statusDataFlags.contains(StatusDataFlag.SET_EXPERIENCE)) {
player.setTotalExperience(statusData.totalExperience);
player.setLevel(statusData.expLevel);
player.setExp(statusData.expProgress);
}
if (setGameMode) {
if (statusDataFlags.contains(StatusDataFlag.SET_GAME_MODE)) {
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () ->
player.setGameMode(GameMode.valueOf(statusData.gameMode)));
}
if (setFlying) {
if (statusDataFlags.contains(StatusDataFlag.SET_FLYING)) {
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
if (statusData.isFlying) {
player.setAllowFlight(true);
@ -130,29 +121,39 @@ public class BukkitPlayer extends OnlineUser {
}
@Override
public CompletableFuture<InventoryData> getInventory() {
return BukkitSerializer.serializeInventory(player.getInventory().getContents())
.thenApply(InventoryData::new);
public CompletableFuture<ItemData> getInventory() {
return BukkitSerializer.serializeItemStackArray(player.getInventory().getContents())
.thenApply(ItemData::new);
}
@Override
public CompletableFuture<Void> setInventory(@NotNull InventoryData inventoryData) {
return BukkitSerializer.deserializeInventory(inventoryData.serializedInventory).thenAccept(contents ->
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(),
() -> player.getInventory().setContents(contents)));
public CompletableFuture<Void> setInventory(@NotNull ItemData itemData) {
return BukkitSerializer.deserializeInventory(itemData.serializedItems).thenApplyAsync(contents -> {
final CompletableFuture<Void> inventorySetFuture = new CompletableFuture<>();
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
player.getInventory().setContents(contents.getContents());
inventorySetFuture.complete(null);
});
return inventorySetFuture.join();
});
}
@Override
public CompletableFuture<InventoryData> getEnderChest() {
return BukkitSerializer.serializeInventory(player.getEnderChest().getContents())
.thenApply(InventoryData::new);
public CompletableFuture<ItemData> getEnderChest() {
return BukkitSerializer.serializeItemStackArray(player.getEnderChest().getContents())
.thenApply(ItemData::new);
}
@Override
public CompletableFuture<Void> setEnderChest(@NotNull InventoryData enderChestData) {
return BukkitSerializer.deserializeInventory(enderChestData.serializedInventory).thenAccept(contents ->
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(),
() -> player.getEnderChest().setContents(contents)));
public CompletableFuture<Void> setEnderChest(@NotNull ItemData enderChestData) {
return BukkitSerializer.deserializeItemStackArray(enderChestData.serializedItems).thenApplyAsync(contents -> {
final CompletableFuture<Void> enderChestSetFuture = new CompletableFuture<>();
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
player.getEnderChest().setContents(contents);
enderChestSetFuture.complete(null);
});
return enderChestSetFuture.join();
});
}
@Override
@ -163,15 +164,20 @@ public class BukkitPlayer extends OnlineUser {
@Override
public CompletableFuture<Void> setPotionEffects(@NotNull PotionEffectData potionEffectData) {
return BukkitSerializer.deserializePotionEffects(potionEffectData.serializedPotionEffects).thenAccept(
effects -> Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
for (PotionEffect effect : player.getActivePotionEffects()) {
player.removePotionEffect(effect.getType());
}
for (PotionEffect effect : effects) {
player.addPotionEffect(effect);
}
}));
return BukkitSerializer.deserializePotionEffects(potionEffectData.serializedPotionEffects)
.thenApplyAsync(effects -> {
final CompletableFuture<Void> potionEffectsSetFuture = new CompletableFuture<>();
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
for (PotionEffect effect : player.getActivePotionEffects()) {
player.removePotionEffect(effect.getType());
}
for (PotionEffect effect : effects) {
player.addPotionEffect(effect);
}
potionEffectsSetFuture.complete(null);
});
return potionEffectsSetFuture.join();
});
}
@Override
@ -362,7 +368,7 @@ public class BukkitPlayer extends OnlineUser {
@Override
public CompletableFuture<Void> setLocation(@NotNull LocationData locationData) {
final CompletableFuture<Void> completableFuture = new CompletableFuture<>();
final CompletableFuture<Void> teleportFuture = new CompletableFuture<>();
AtomicReference<World> bukkitWorld = new AtomicReference<>(Bukkit.getWorld(locationData.worldName));
if (bukkitWorld.get() == null) {
bukkitWorld.set(Bukkit.getWorld(locationData.worldUuid));
@ -372,12 +378,14 @@ public class BukkitPlayer extends OnlineUser {
.valueOf(locationData.worldEnvironment)).findFirst().ifPresent(bukkitWorld::set);
}
if (bukkitWorld.get() != null) {
player.teleport(new Location(bukkitWorld.get(),
locationData.x, locationData.y, locationData.z,
locationData.yaw, locationData.pitch), PlayerTeleportEvent.TeleportCause.PLUGIN);
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
player.teleport(new Location(bukkitWorld.get(),
locationData.x, locationData.y, locationData.z,
locationData.yaw, locationData.pitch), PlayerTeleportEvent.TeleportCause.PLUGIN);
teleportFuture.complete(null);
});
}
CompletableFuture.runAsync(() -> completableFuture.completeAsync(() -> null));
return completableFuture;
return teleportFuture;
}
@Override
@ -413,12 +421,17 @@ public class BukkitPlayer extends OnlineUser {
@Override
public boolean isDead() {
return player.isDead() || player.getHealth() <= 0;
return player.getHealth() <= 0d;
}
@Override
public boolean isOffline() {
return player == null;
try {
return player == null;
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
@Override
@ -428,7 +441,7 @@ public class BukkitPlayer extends OnlineUser {
@Override
public void showMenu(@NotNull InventoryEditorMenu menu) {
BukkitSerializer.deserializeInventory(menu.inventoryData.serializedInventory).thenAccept(inventoryContents -> {
BukkitSerializer.deserializeItemStackArray(menu.itemData.serializedItems).thenAccept(inventoryContents -> {
final Inventory inventory = Bukkit.createInventory(player, menu.slotCount,
BaseComponent.toLegacyText(menu.menuTitle.toComponent()));
inventory.setContents(inventoryContents);

@ -1,37 +1,39 @@
package net.william278.husksync.util;
import org.jetbrains.annotations.NotNull;
import java.util.logging.Level;
public class BukkitLogger implements Logger {
public class BukkitLogger extends Logger {
private final java.util.logging.Logger logger;
public BukkitLogger(java.util.logging.Logger logger) {
public BukkitLogger(@NotNull java.util.logging.Logger logger) {
this.logger = logger;
}
@Override
public void log(Level level, String message, Exception e) {
public void log(@NotNull Level level, @NotNull String message, @NotNull Exception e) {
logger.log(level, message, e);
}
@Override
public void log(Level level, String message) {
public void log(@NotNull Level level, @NotNull String message) {
logger.log(level, message);
}
@Override
public void info(String message) {
public void info(@NotNull String message) {
logger.info(message);
}
@Override
public void severe(String message) {
public void severe(@NotNull String message) {
logger.severe(message);
}
@Override
public void config(String message) {
public void config(@NotNull String message) {
logger.config(message);
}

@ -10,4 +10,8 @@ libraries:
- 'mysql:mysql-connector-java:8.0.29'
commands:
husksync:
usage: '/husksync <update|info|reload>'
usage: '/husksync <update|info|reload>'
invsee:
usage: '/invsee <player>'
echest:
usage: '/echest <player>'

@ -5,6 +5,7 @@ import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataAdapter;
import net.william278.husksync.editor.DataEditor;
import net.william278.husksync.database.Database;
import net.william278.husksync.event.EventCannon;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.util.Logger;
@ -29,6 +30,8 @@ public interface HuskSync {
@NotNull DataEditor getDataEditor();
@NotNull EventCannon getEventCannon();
@NotNull Settings getSettings();
@NotNull Locales getLocales();

@ -3,6 +3,7 @@ package net.william278.husksync.command;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.UserData;
import net.william278.husksync.data.VersionedUserData;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.editor.InventoryEditorMenu;
import net.william278.husksync.player.OnlineUser;
import org.jetbrains.annotations.NotNull;
@ -24,40 +25,42 @@ public class EchestCommand extends CommandBase {
.ifPresent(player::sendMessage);
return;
}
plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAcceptAsync(optionalUser -> {
optionalUser.ifPresentOrElse(user -> {
List<VersionedUserData> userData = plugin.getDatabase().getUserData(user).join();
Optional<VersionedUserData> dataToView;
if (args.length == 2) {
try {
final UUID version = UUID.fromString(args[1]);
dataToView = userData.stream().filter(data -> data.versionUUID().equals(version)).findFirst();
} catch (IllegalArgumentException e) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/echest <player> [version_uuid]").ifPresent(player::sendMessage);
return;
plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAcceptAsync(optionalUser ->
optionalUser.ifPresentOrElse(user -> {
List<VersionedUserData> userData = plugin.getDatabase().getUserData(user).join();
Optional<VersionedUserData> dataToView;
if (args.length == 2) {
try {
final UUID version = UUID.fromString(args[1]);
dataToView = userData.stream().filter(data -> data.versionUUID().equals(version)).findFirst();
} catch (IllegalArgumentException e) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/echest <player> [version_uuid]").ifPresent(player::sendMessage);
return;
}
} else {
dataToView = userData.stream().sorted().findFirst();
}
} else {
dataToView = userData.stream().sorted().findFirst();
}
dataToView.ifPresentOrElse(versionedUserData -> {
final UserData data = versionedUserData.userData();
final InventoryEditorMenu menu = InventoryEditorMenu.createEnderChestMenu(
data.getEnderChestData(), user, player);
plugin.getLocales().getLocale("viewing_ender_chest_of", user.username)
.ifPresent(player::sendMessage);
plugin.getDataEditor().openInventoryMenu(player, menu).thenAcceptAsync(inventoryDataOnClose -> {
final UserData updatedUserData = new UserData(data.getStatusData(),
data.getInventoryData(), menu.canEdit ? inventoryDataOnClose : data.getEnderChestData(),
data.getPotionEffectData(), data.getAdvancementData(),
data.getStatisticData(), data.getLocationData(),
data.getPersistentDataContainerData());
plugin.getDatabase().setUserData(user, updatedUserData).join();
});
}, () -> plugin.getLocales().getLocale(args.length == 2 ? "error_invalid_version_uuid"
: "error_no_data_to_display").ifPresent(player::sendMessage));
}, () -> plugin.getLocales().getLocale("error_invalid_player").ifPresent(player::sendMessage));
});
dataToView.ifPresentOrElse(versionedUserData -> {
final UserData data = versionedUserData.userData();
final InventoryEditorMenu menu = InventoryEditorMenu.createEnderChestMenu(
data.getEnderChestData(), user, player);
plugin.getLocales().getLocale("viewing_ender_chest_of", user.username)
.ifPresent(player::sendMessage);
plugin.getDataEditor().openInventoryMenu(player, menu).thenAcceptAsync(inventoryDataOnClose -> {
if (!menu.canEdit) {
return;
}
final UserData updatedUserData = new UserData(data.getStatusData(),
data.getInventoryData(), inventoryDataOnClose,
data.getPotionEffectsData(), data.getAdvancementData(),
data.getStatisticsData(), data.getLocationData(),
data.getPersistentDataContainerData());
plugin.getDatabase().setUserData(user, updatedUserData, DataSaveCause.ECHEST_COMMAND_EDIT).join();
});
}, () -> plugin.getLocales().getLocale(args.length == 2 ? "error_invalid_version_uuid"
: "error_no_data_to_display").ifPresent(player::sendMessage));
}, () -> plugin.getLocales().getLocale("error_invalid_player").ifPresent(player::sendMessage)));
}
}

@ -3,6 +3,7 @@ package net.william278.husksync.command;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.UserData;
import net.william278.husksync.data.VersionedUserData;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.editor.InventoryEditorMenu;
import net.william278.husksync.player.OnlineUser;
import org.jetbrains.annotations.NotNull;
@ -24,40 +25,42 @@ public class InvseeCommand extends CommandBase {
.ifPresent(player::sendMessage);
return;
}
plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAcceptAsync(optionalUser -> {
optionalUser.ifPresentOrElse(user -> {
List<VersionedUserData> userData = plugin.getDatabase().getUserData(user).join();
Optional<VersionedUserData> dataToView;
if (args.length == 2) {
try {
final UUID version = UUID.fromString(args[1]);
dataToView = userData.stream().filter(data -> data.versionUUID().equals(version)).findFirst();
} catch (IllegalArgumentException e) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/invsee <player> [version_uuid]").ifPresent(player::sendMessage);
return;
plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAcceptAsync(optionalUser ->
optionalUser.ifPresentOrElse(user -> {
List<VersionedUserData> userData = plugin.getDatabase().getUserData(user).join();
Optional<VersionedUserData> dataToView;
if (args.length == 2) {
try {
final UUID version = UUID.fromString(args[1]);
dataToView = userData.stream().filter(data -> data.versionUUID().equals(version)).findFirst();
} catch (IllegalArgumentException e) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/invsee <player> [version_uuid]").ifPresent(player::sendMessage);
return;
}
} else {
dataToView = userData.stream().sorted().findFirst();
}
} else {
dataToView = userData.stream().sorted().findFirst();
}
dataToView.ifPresentOrElse(versionedUserData -> {
final UserData data = versionedUserData.userData();
final InventoryEditorMenu menu = InventoryEditorMenu.createInventoryMenu(
data.getInventoryData(), user, player);
plugin.getLocales().getLocale("viewing_inventory_of", user.username)
.ifPresent(player::sendMessage);
plugin.getDataEditor().openInventoryMenu(player, menu).thenAcceptAsync(inventoryDataOnClose -> {
final UserData updatedUserData = new UserData(data.getStatusData(),
menu.canEdit ? inventoryDataOnClose : data.getInventoryData(),
data.getEnderChestData(), data.getPotionEffectData(), data.getAdvancementData(),
data.getStatisticData(), data.getLocationData(),
data.getPersistentDataContainerData());
plugin.getDatabase().setUserData(user, updatedUserData).join();
});
}, () -> plugin.getLocales().getLocale(args.length == 2 ? "error_invalid_version_uuid"
: "error_no_data_to_display").ifPresent(player::sendMessage));
}, () -> plugin.getLocales().getLocale("error_invalid_player").ifPresent(player::sendMessage));
});
dataToView.ifPresentOrElse(versionedUserData -> {
final UserData data = versionedUserData.userData();
final InventoryEditorMenu menu = InventoryEditorMenu.createInventoryMenu(
data.getInventoryData(), user, player);
plugin.getLocales().getLocale("viewing_inventory_of", user.username)
.ifPresent(player::sendMessage);
plugin.getDataEditor().openInventoryMenu(player, menu).thenAcceptAsync(inventoryDataOnClose -> {
if (!menu.canEdit) {
return;
}
final UserData updatedUserData = new UserData(data.getStatusData(),
inventoryDataOnClose,
data.getEnderChestData(), data.getPotionEffectsData(), data.getAdvancementData(),
data.getStatisticsData(), data.getLocationData(),
data.getPersistentDataContainerData());
plugin.getDatabase().setUserData(user, updatedUserData, DataSaveCause.INVSEE_COMMAND_EDIT).join();
});
}, () -> plugin.getLocales().getLocale(args.length == 2 ? "error_invalid_version_uuid"
: "error_no_data_to_display").ifPresent(player::sendMessage));
}, () -> plugin.getLocales().getLocale("error_invalid_player").ifPresent(player::sendMessage)));
}
}

@ -119,6 +119,7 @@ public class Settings {
CHECK_FOR_UPDATES("check_for_updates", OptionType.BOOLEAN, true),
CLUSTER_ID("cluster_id", OptionType.STRING, ""),
DEBUG_LOGGING("debug_logging", OptionType.BOOLEAN, true),
DATABASE_HOST("database.credentials.host", OptionType.STRING, "localhost"),
DATABASE_PORT("database.credentials.port", OptionType.INTEGER, 3306),
@ -142,6 +143,7 @@ public class Settings {
SYNCHRONIZATION_MAX_USER_DATA_RECORDS("synchronization.max_user_data_records", OptionType.INTEGER, 5),
SYNCHRONIZATION_SAVE_ON_WORLD_SAVE("synchronization.save_on_world_save", OptionType.BOOLEAN, true),
SYNCHRONIZATION_COMPRESS_DATA("synchronization.compress_data", OptionType.BOOLEAN, true),
SYNCHRONIZATION_NETWORK_LATENCY_MILLISECONDS("synchronization.network_latency_milliseconds", OptionType.INTEGER, 500),
SYNCHRONIZATION_SYNC_INVENTORIES("synchronization.features.inventories", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_ENDER_CHESTS("synchronization.features.ender_chests", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_HEALTH("synchronization.features.health", OptionType.BOOLEAN, true),

@ -8,14 +8,25 @@ import org.jetbrains.annotations.NotNull;
public interface DataAdapter {
/**
* Converts {@link UserData} to a byte array.
* Converts {@link UserData} to a byte array
*
* @param data The {@link UserData} to adapt.
* @param data The {@link UserData} to adapt
* @return The byte array.
* @throws DataAdaptionException If an error occurred during adaptation.
*/
byte[] toBytes(@NotNull UserData data) throws DataAdaptionException;
/**
* Serializes {@link UserData} to a JSON string.
*
* @param data The {@link UserData} to serialize
* @param pretty Whether to pretty print the JSON.
* @return The output json string.
* @throws DataAdaptionException If an error occurred during adaptation.
*/
@NotNull
String toJson(@NotNull UserData data, boolean pretty) throws DataAdaptionException;
/**
* Converts a byte array to {@link UserData}.
*

@ -3,7 +3,7 @@ package net.william278.husksync.data;
/**
* Indicates an error occurred during base-64 serialization and deserialization of data.
* </p>
* For example, an exception deserializing {@link InventoryData} item stack or {@link PotionEffectData} potion effect arrays
* For example, an exception deserializing {@link ItemData} item stack or {@link PotionEffectData} potion effect arrays
*/
public class DataDeserializationException extends RuntimeException {
protected DataDeserializationException(String message, Throwable cause) {

@ -0,0 +1,53 @@
package net.william278.husksync.data;
import org.jetbrains.annotations.NotNull;
/**
* Identifies the cause of a player data save.
*
* @implNote This enum is saved in the database. Cause names have a max length of 32 characters.
*/
public enum DataSaveCause {
/**
* Indicates data saved when a player disconnected from the server (either to change servers, or to log off)
*/
DISCONNECT,
/**
* Indicates data saved when the world saved
*/
WORLD_SAVE,
/**
* Indicates data saved when the server shut down
*/
SERVER_SHUTDOWN,
/**
* Indicates data was saved by editing inventory contents via the {@code /invsee} command
*/
INVSEE_COMMAND_EDIT,
/**
* Indicates data was saved by editing Ender Chest contents via the {@code /echest} command
*/
ECHEST_COMMAND_EDIT,
/**
* Indicates data was saved by an API call
*/
API,
/**
* Indicates data was saved by an unknown cause.
* </p>
* This should not be used and is only used for error handling purposes.
*/
UNKNOWN;
@NotNull
public static DataSaveCause getCauseByName(@NotNull String name) {
for (DataSaveCause cause : values()) {
if (cause.name().equalsIgnoreCase(name)) {
return cause;
}
}
return UNKNOWN;
}
}

@ -1,25 +0,0 @@
package net.william278.husksync.data;
import com.google.gson.annotations.SerializedName;
import org.jetbrains.annotations.NotNull;
/**
* Stores information about a player's inventory or ender chest
*/
public class InventoryData {
/**
* A base64 string of platform-serialized inventory data
*/
@SerializedName("serialized_inventory")
public String serializedInventory;
public InventoryData(@NotNull final String serializedInventory) {
this.serializedInventory = serializedInventory;
}
@SuppressWarnings("unused")
protected InventoryData() {
}
}

@ -0,0 +1,25 @@
package net.william278.husksync.data;
import com.google.gson.annotations.SerializedName;
import org.jetbrains.annotations.NotNull;
/**
* Stores information about the contents of a player's inventory or Ender Chest.
*/
public class ItemData {
/**
* A Base-64 string of platform-serialized items
*/
@SerializedName("serialized_items")
public String serializedItems;
public ItemData(@NotNull final String serializedItems) {
this.serializedItems = serializedItems;
}
@SuppressWarnings("unused")
protected ItemData() {
}
}

@ -10,7 +10,12 @@ public class JsonDataAdapter implements DataAdapter {
@Override
public byte[] toBytes(@NotNull UserData data) throws DataAdaptionException {
return new GsonBuilder().create().toJson(data).getBytes(StandardCharsets.UTF_8);
return toJson(data, false).getBytes(StandardCharsets.UTF_8);
}
@Override
public @NotNull String toJson(@NotNull UserData data, boolean pretty) throws DataAdaptionException {
return (pretty ? new GsonBuilder().setPrettyPrinting() : new GsonBuilder()).create().toJson(data);
}
@Override

@ -11,8 +11,8 @@ public class PotionEffectData {
@SerializedName("serialized_potion_effects")
public String serializedPotionEffects;
public PotionEffectData(@NotNull final String serializedInventory) {
this.serializedPotionEffects = serializedInventory;
public PotionEffectData(@NotNull final String serializedPotionEffects) {
this.serializedPotionEffects = serializedPotionEffects;
}
@SuppressWarnings("unused")

@ -0,0 +1,51 @@
package net.william278.husksync.data;
import net.william278.husksync.config.Settings;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
/**
* Flags for setting {@link StatusData}, indicating which elements should be synced
*/
public enum StatusDataFlag {
SET_HEALTH(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HEALTH),
SET_MAX_HEALTH(Settings.ConfigOption.SYNCHRONIZATION_SYNC_MAX_HEALTH),
SET_HUNGER(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HUNGER),
SET_EXPERIENCE(Settings.ConfigOption.SYNCHRONIZATION_SYNC_EXPERIENCE),
SET_GAME_MODE(Settings.ConfigOption.SYNCHRONIZATION_SYNC_GAME_MODE),
SET_FLYING(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION),
SET_SELECTED_ITEM_SLOT(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES);
private final Settings.ConfigOption configOption;
StatusDataFlag(@NotNull Settings.ConfigOption configOption) {
this.configOption = configOption;
}
/**
* Returns all status data flags
*
* @return all status data flags as a list
*/
@NotNull
@SuppressWarnings("unused")
public static List<StatusDataFlag> getAll() {
return Arrays.stream(StatusDataFlag.values()).toList();
}
/**
* Returns all status data flags that are enabled for setting as per the {@link Settings}
*
* @param settings the settings to use for determining which flags are enabled
* @return all status data flags that are enabled for setting
*/
@NotNull
public static List<StatusDataFlag> getFromSettings(@NotNull Settings settings) {
return Arrays.stream(StatusDataFlag.values()).filter(
flag -> settings.getBooleanValue(flag.configOption)).toList();
}
}

@ -10,6 +10,13 @@ import java.util.List;
*/
public class UserData {
/**
* Indicates the version of the {@link UserData} format being used.
* </p>
* This value is to be incremented whenever the format changes.
*/
private static final int CURRENT_FORMAT_VERSION = 1;
/**
* Stores the user's status data, including health, food, etc.
*/
@ -20,13 +27,13 @@ public class UserData {
* Stores the user's inventory contents
*/
@SerializedName("inventory")
protected InventoryData inventoryData;
protected ItemData inventoryData;
/**
* Stores the user's ender chest contents
*/
@SerializedName("ender_chest")
protected InventoryData enderChestData;
protected ItemData enderChestData;
/**
* Store's the user's potion effects
@ -58,8 +65,14 @@ public class UserData {
@SerializedName("persistent_data_container")
protected PersistentDataContainerData persistentDataContainerData;
public UserData(@NotNull StatusData statusData, @NotNull InventoryData inventoryData,
@NotNull InventoryData enderChestData, @NotNull PotionEffectData potionEffectData,
/**
* Stores the version of the data format being used
*/
@SerializedName("format_version")
protected int formatVersion;
public UserData(@NotNull StatusData statusData, @NotNull ItemData inventoryData,
@NotNull ItemData enderChestData, @NotNull PotionEffectData potionEffectData,
@NotNull List<AdvancementData> advancementData, @NotNull StatisticsData statisticData,
@NotNull LocationData locationData, @NotNull PersistentDataContainerData persistentDataContainerData) {
this.statusData = statusData;
@ -70,6 +83,7 @@ public class UserData {
this.statisticData = statisticData;
this.locationData = locationData;
this.persistentDataContainerData = persistentDataContainerData;
this.formatVersion = CURRENT_FORMAT_VERSION;
}
// Empty constructor to facilitate json serialization
@ -81,15 +95,15 @@ public class UserData {
return statusData;
}
public InventoryData getInventoryData() {
public ItemData getInventoryData() {
return inventoryData;
}
public InventoryData getEnderChestData() {
public ItemData getEnderChestData() {
return enderChestData;
}
public PotionEffectData getPotionEffectData() {
public PotionEffectData getPotionEffectsData() {
return potionEffectData;
}
@ -97,7 +111,7 @@ public class UserData {
return advancementData;
}
public StatisticsData getStatisticData() {
public StatisticsData getStatisticsData() {
return statisticData;
}

@ -6,17 +6,20 @@ import java.util.Date;
import java.util.UUID;
/**
* Represents a uniquely versioned and timestamped snapshot of a user's data
* Represents a uniquely versioned and timestamped snapshot of a user's data, including why it was saved.
*
* @param versionUUID The unique identifier for this user data version
* @param versionTimestamp An epoch milliseconds timestamp of when this data was created
* @param userData The {@link UserData} that has been versioned
* @param cause The {@link DataSaveCause} that caused this data to be saved
*/
public record VersionedUserData(@NotNull UUID versionUUID, @NotNull Date versionTimestamp,
@NotNull UserData userData) implements Comparable<VersionedUserData> {
@NotNull DataSaveCause cause, @NotNull UserData userData) implements Comparable<VersionedUserData> {
/**
* Version {@link UserData} into a {@link VersionedUserData}, assigning it a random {@link UUID} and the current timestamp {@link Date}
* </p>
* Note that this method will set {@code cause} to {@link DataSaveCause#API}
*
* @param userData The {@link UserData} to version
* @return A new {@link VersionedUserData}
@ -24,7 +27,7 @@ public record VersionedUserData(@NotNull UUID versionUUID, @NotNull Date version
* Database implementations should instead use their own UUID generation functions.
*/
public static VersionedUserData version(@NotNull UserData userData) {
return new VersionedUserData(UUID.randomUUID(), new Date(), userData);
return new VersionedUserData(UUID.randomUUID(), new Date(), DataSaveCause.API, userData);
}
/**

@ -1,8 +1,10 @@
package net.william278.husksync.database;
import net.william278.husksync.data.DataAdapter;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.UserData;
import net.william278.husksync.data.VersionedUserData;
import net.william278.husksync.event.EventCannon;
import net.william278.husksync.player.User;
import net.william278.husksync.util.Logger;
import net.william278.husksync.util.ResourceReader;
@ -51,6 +53,20 @@ public abstract class Database {
return dataAdapter;
}
/**
* {@link EventCannon} implementation used for firing events
*/
private final EventCannon eventCannon;
/**
* Returns the {@link EventCannon} used to fire events
*
* @return instance of the {@link EventCannon} implementation
*/
protected EventCannon getEventCannon() {
return eventCannon;
}
/**
* Logger instance used for database error logging
*/
@ -71,12 +87,14 @@ public abstract class Database {
private final ResourceReader resourceReader;
protected Database(@NotNull String playerTableName, @NotNull String dataTableName, final int maxUserDataRecords,
@NotNull ResourceReader resourceReader, @NotNull DataAdapter dataAdapter, @NotNull Logger logger) {
@NotNull ResourceReader resourceReader, @NotNull DataAdapter dataAdapter,
@NotNull EventCannon eventCannon, @NotNull Logger logger) {
this.playerTableName = playerTableName;
this.dataTableName = dataTableName;
this.maxUserDataRecords = maxUserDataRecords;
this.resourceReader = resourceReader;
this.dataAdapter = dataAdapter;
this.eventCannon = eventCannon;
this.logger = logger;
}
@ -159,7 +177,7 @@ public abstract class Database {
protected abstract CompletableFuture<Void> pruneUserDataRecords(@NotNull User user);
/**
* Add user data to the database<p>
* Save user data to the database<p>
* This will remove the oldest data for the user if the amount of data exceeds the limit as configured
*
* @param user The user to add data for
@ -167,7 +185,7 @@ public abstract class Database {
* @return A future returning void when complete
* @see VersionedUserData#version(UserData)
*/
public abstract CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData);
public abstract CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData, @NotNull DataSaveCause dataSaveCause);
/**
* Close the database connection

@ -2,19 +2,16 @@ package net.william278.husksync.database;
import com.zaxxer.hikari.HikariDataSource;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataAdapter;
import net.william278.husksync.data.DataAdaptionException;
import net.william278.husksync.data.UserData;
import net.william278.husksync.data.VersionedUserData;
import net.william278.husksync.data.*;
import net.william278.husksync.event.DataSaveEvent;
import net.william278.husksync.event.EventCannon;
import net.william278.husksync.player.User;
import net.william278.husksync.util.Logger;
import net.william278.husksync.util.ResourceReader;
import org.jetbrains.annotations.NotNull;
import org.xerial.snappy.Snappy;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.sql.*;
import java.util.*;
import java.util.Date;
@ -55,11 +52,11 @@ public class MySqlDatabase extends Database {
private HikariDataSource connectionPool;
public MySqlDatabase(@NotNull Settings settings, @NotNull ResourceReader resourceReader, @NotNull Logger logger,
@NotNull DataAdapter dataAdapter) {
@NotNull DataAdapter dataAdapter, @NotNull EventCannon eventCannon) {
super(settings.getStringValue(Settings.ConfigOption.DATABASE_PLAYERS_TABLE_NAME),
settings.getStringValue(Settings.ConfigOption.DATABASE_DATA_TABLE_NAME),
settings.getIntegerValue(Settings.ConfigOption.SYNCHRONIZATION_MAX_USER_DATA_RECORDS),
resourceReader, dataAdapter, logger);
resourceReader, dataAdapter, eventCannon, logger);
this.mySqlHost = settings.getStringValue(Settings.ConfigOption.DATABASE_HOST);
this.mySqlPort = settings.getIntegerValue(Settings.ConfigOption.DATABASE_PORT);
this.mySqlDatabaseName = settings.getStringValue(Settings.ConfigOption.DATABASE_NAME);
@ -213,7 +210,7 @@ public class MySqlDatabase extends Database {
return CompletableFuture.supplyAsync(() -> {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `version_uuid`, `timestamp`, `data`
SELECT `version_uuid`, `timestamp`, `save_cause`, `data`
FROM `%data_table%`
WHERE `player_uuid`=?
ORDER BY `timestamp` DESC
@ -227,6 +224,7 @@ public class MySqlDatabase extends Database {
return Optional.of(new VersionedUserData(
UUID.fromString(resultSet.getString("version_uuid")),
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
getDataAdapter().fromBytes(dataByteArray)));
}
}
@ -243,7 +241,7 @@ public class MySqlDatabase extends Database {
final List<VersionedUserData> retrievedData = new ArrayList<>();
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `version_uuid`, `timestamp`, `data`
SELECT `version_uuid`, `timestamp`, `save_cause`, `data`
FROM `%data_table%`
WHERE `player_uuid`=?
ORDER BY `timestamp` DESC;"""))) {
@ -256,6 +254,7 @@ public class MySqlDatabase extends Database {
final VersionedUserData data = new VersionedUserData(
UUID.fromString(resultSet.getString("version_uuid")),
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
getDataAdapter().fromBytes(dataByteArray));
retrievedData.add(data);
}
@ -290,20 +289,27 @@ public class MySqlDatabase extends Database {
}
@Override
public CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData) {
public CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData,
@NotNull DataSaveCause saveCause) {
return CompletableFuture.runAsync(() -> {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
INSERT INTO `%data_table%`
(`player_uuid`,`version_uuid`,`timestamp`,`data`)
VALUES (?,UUID(),NOW(),?);"""))) {
statement.setString(1, user.uuid.toString());
statement.setBlob(2, new ByteArrayInputStream(
getDataAdapter().toBytes(userData)));
statement.executeUpdate();
final DataSaveEvent dataSaveEvent = (DataSaveEvent) getEventCannon().fireDataSaveEvent(user,
userData, saveCause).join();
if (!dataSaveEvent.isCancelled()) {
final UserData finalData = dataSaveEvent.getUserData();
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
INSERT INTO `%data_table%`
(`player_uuid`,`version_uuid`,`timestamp`,`save_cause`,`data`)
VALUES (?,UUID(),NOW(),?,?);"""))) {
statement.setString(1, user.uuid.toString());
statement.setString(2, saveCause.name());
statement.setBlob(3, new ByteArrayInputStream(
getDataAdapter().toBytes(finalData)));
statement.executeUpdate();
}
} catch (SQLException | DataAdaptionException e) {
getLogger().log(Level.SEVERE, "Failed to set user data in the database", e);
}
} catch (SQLException | DataAdaptionException e) {
getLogger().log(Level.SEVERE, "Failed to set user data in the database", e);
}
}).thenRun(() -> pruneUserDataRecords(user).join());
}

@ -1,7 +1,6 @@
package net.william278.husksync.editor;
import net.william278.husksync.config.Locales;
import net.william278.husksync.data.InventoryData;
import net.william278.husksync.data.ItemData;
import net.william278.husksync.data.VersionedUserData;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
@ -32,11 +31,11 @@ public class DataEditor {
* @param user The online user to open the editor for
* @param inventoryEditorMenu The {@link InventoryEditorMenu} to open
* @return The inventory editor menu
* @see InventoryEditorMenu#createInventoryMenu(InventoryData, User, OnlineUser)
* @see InventoryEditorMenu#createEnderChestMenu(InventoryData, User, OnlineUser)
* @see InventoryEditorMenu#createInventoryMenu(ItemData, User, OnlineUser)
* @see InventoryEditorMenu#createEnderChestMenu(ItemData, User, OnlineUser)
*/
public CompletableFuture<InventoryData> openInventoryMenu(@NotNull OnlineUser user,
@NotNull InventoryEditorMenu inventoryEditorMenu) {
public CompletableFuture<ItemData> openInventoryMenu(@NotNull OnlineUser user,
@NotNull InventoryEditorMenu inventoryEditorMenu) {
this.openInventoryMenus.put(user.uuid, inventoryEditorMenu);
return inventoryEditorMenu.showInventory(user);
}
@ -45,11 +44,11 @@ public class DataEditor {
* Close an inventory or ender chest editor menu
*
* @param user The online user to close the editor for
* @param inventoryData the {@link InventoryData} contained within the menu at the time of closing
* @param itemData the {@link ItemData} contained within the menu at the time of closing
*/
public void closeInventoryMenu(@NotNull OnlineUser user, @NotNull InventoryData inventoryData) {
public void closeInventoryMenu(@NotNull OnlineUser user, @NotNull ItemData itemData) {
if (this.openInventoryMenus.containsKey(user.uuid)) {
this.openInventoryMenus.get(user.uuid).closeInventory(inventoryData);
this.openInventoryMenus.get(user.uuid).closeInventory(itemData);
}
this.openInventoryMenus.remove(user.uuid);
}

@ -2,7 +2,7 @@ package net.william278.husksync.editor;
import de.themoep.minedown.MineDown;
import net.william278.husksync.command.Permission;
import net.william278.husksync.data.InventoryData;
import net.william278.husksync.data.ItemData;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull;
@ -11,41 +11,41 @@ import java.util.concurrent.CompletableFuture;
public class InventoryEditorMenu {
public final InventoryData inventoryData;
public final ItemData itemData;
public final int slotCount;
public final MineDown menuTitle;
public final boolean canEdit;
private CompletableFuture<InventoryData> inventoryDataCompletableFuture;
private CompletableFuture<ItemData> inventoryDataCompletableFuture;
private InventoryEditorMenu(@NotNull InventoryData inventoryData, int slotCount,
private InventoryEditorMenu(@NotNull ItemData itemData, int slotCount,
@NotNull MineDown menuTitle, boolean canEdit) {
this.inventoryData = inventoryData;
this.itemData = itemData;
this.menuTitle = menuTitle;
this.slotCount = slotCount;
this.canEdit = canEdit;
}
public CompletableFuture<InventoryData> showInventory(@NotNull OnlineUser user) {
public CompletableFuture<ItemData> showInventory(@NotNull OnlineUser user) {
inventoryDataCompletableFuture = new CompletableFuture<>();
user.showMenu(this);
return inventoryDataCompletableFuture;
}
public void closeInventory(@NotNull InventoryData inventoryData) {
inventoryDataCompletableFuture.completeAsync(() -> inventoryData);
public void closeInventory(@NotNull ItemData itemData) {
inventoryDataCompletableFuture.completeAsync(() -> itemData);
}
public static InventoryEditorMenu createInventoryMenu(@NotNull InventoryData inventoryData, @NotNull User dataOwner,
public static InventoryEditorMenu createInventoryMenu(@NotNull ItemData itemData, @NotNull User dataOwner,
@NotNull OnlineUser viewer) {
return new InventoryEditorMenu(inventoryData, 45,
return new InventoryEditorMenu(itemData, 45,
new MineDown(dataOwner.username + "'s Inventory"),
viewer.hasPermission(Permission.COMMAND_EDIT_INVENTORIES.node));
}
public static InventoryEditorMenu createEnderChestMenu(@NotNull InventoryData inventoryData, @NotNull User dataOwner,
public static InventoryEditorMenu createEnderChestMenu(@NotNull ItemData itemData, @NotNull User dataOwner,
@NotNull OnlineUser viewer) {
return new InventoryEditorMenu(inventoryData, 27,
return new InventoryEditorMenu(itemData, 27,
new MineDown(dataOwner.username + "'s Ender Chest"),
viewer.hasPermission(Permission.COMMAND_EDIT_ENDER_CHESTS.node));
}

@ -0,0 +1,9 @@
package net.william278.husksync.event;
public interface CancellableEvent extends Event {
boolean isCancelled();
void setCancelled(boolean cancelled);
}

@ -0,0 +1,20 @@
package net.william278.husksync.event;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.UserData;
import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull;
public interface DataSaveEvent extends CancellableEvent {
@NotNull
UserData getUserData();
void setUserData(@NotNull UserData userData);
@NotNull User getUser();
@NotNull
DataSaveCause getSaveCause();
}

@ -0,0 +1,9 @@
package net.william278.husksync.event;
import java.util.concurrent.CompletableFuture;
public interface Event {
CompletableFuture<Event> fire();
}

@ -0,0 +1,23 @@
package net.william278.husksync.event;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.UserData;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
public abstract class EventCannon {
protected EventCannon() {
}
public abstract CompletableFuture<Event> firePreSyncEvent(@NotNull OnlineUser user, @NotNull UserData userData);
public abstract CompletableFuture<Event> fireDataSaveEvent(@NotNull User user, @NotNull UserData userData,
@NotNull DataSaveCause saveCause);
public abstract void fireSyncCompleteEvent(@NotNull OnlineUser user);
}

@ -0,0 +1,9 @@
package net.william278.husksync.event;
import net.william278.husksync.player.OnlineUser;
public interface PlayerEvent extends Event {
OnlineUser getUser();
}

@ -0,0 +1,13 @@
package net.william278.husksync.event;
import net.william278.husksync.data.UserData;
import org.jetbrains.annotations.NotNull;
public interface PreSyncEvent extends CancellableEvent {
@NotNull
UserData getUserData();
void setUserData(@NotNull UserData userData);
}

@ -0,0 +1,5 @@
package net.william278.husksync.event;
public interface SyncCompleteEvent extends PlayerEvent {
}

@ -2,7 +2,9 @@ package net.william278.husksync.listener;
import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.InventoryData;
import net.william278.husksync.data.ItemData;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.UserData;
import net.william278.husksync.player.OnlineUser;
import org.jetbrains.annotations.NotNull;
@ -14,6 +16,7 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
public abstract class EventListener {
@ -43,45 +46,67 @@ public abstract class EventListener {
return;
}
usersAwaitingSync.add(user.uuid);
CompletableFuture.runAsync(() -> huskSync.getRedisManager().getUserServerSwitch(user).thenAccept(changingServers -> {
if (!changingServers) {
// Fetch from the database if the user isn't changing servers
setUserFromDatabase(user).thenRun(() -> handleSynchronisationCompletion(user));
} else {
final int TIME_OUT_MILLISECONDS = 3200;
CompletableFuture.runAsync(() -> {
final AtomicInteger currentMilliseconds = new AtomicInteger(0);
final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
// Set the user as soon as the source server has set the data to redis
executor.scheduleAtFixedRate(() -> {
if (disabling || currentMilliseconds.get() > TIME_OUT_MILLISECONDS) {
executor.shutdown();
setUserFromDatabase(user).thenRun(() -> handleSynchronisationCompletion(user));
return;
}
huskSync.getRedisManager().getUserData(user).thenAccept(redisUserData ->
redisUserData.ifPresent(redisData -> {
user.setData(redisData, huskSync.getSettings()).join();
CompletableFuture.runAsync(() -> {
try {
// Hold reading data for the network latency threshold, to ensure the source server has set the redis key
Thread.sleep(Math.min(0, huskSync.getSettings().getIntegerValue(Settings.ConfigOption.SYNCHRONIZATION_NETWORK_LATENCY_MILLISECONDS)));
} catch (InterruptedException e) {
huskSync.getLoggingAdapter().log(Level.SEVERE, "An exception occurred handling a player join", e);
} finally {
huskSync.getRedisManager().getUserServerSwitch(user).thenAccept(changingServers -> {
huskSync.getLoggingAdapter().info("Handling server change check " + ((changingServers) ? "true" : "false"));
if (!changingServers) {
huskSync.getLoggingAdapter().info("User is not changing servers");
// Fetch from the database if the user isn't changing servers
setUserFromDatabase(user).thenRun(() -> handleSynchronisationCompletion(user));
} else {
huskSync.getLoggingAdapter().info("User is changing servers, setting from db");
final int TIME_OUT_MILLISECONDS = 3200;
CompletableFuture.runAsync(() -> {
final AtomicInteger currentMilliseconds = new AtomicInteger(0);
final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
// Set the user as soon as the source server has set the data to redis
executor.scheduleAtFixedRate(() -> {
if (user.isOffline()) {
executor.shutdown();
})).join();
currentMilliseconds.addAndGet(200);
}, 0, 200L, TimeUnit.MILLISECONDS);
huskSync.getLoggingAdapter().info("Cancelled sync, user gone offline!");
return;
}
if (disabling || currentMilliseconds.get() > TIME_OUT_MILLISECONDS) {
executor.shutdown();
setUserFromDatabase(user).thenRun(() -> handleSynchronisationCompletion(user));
huskSync.getLoggingAdapter().info("Setting user from db as fallback");
return;
}
huskSync.getRedisManager().getUserData(user).thenAccept(redisUserData ->
redisUserData.ifPresent(redisData -> {
huskSync.getLoggingAdapter().info("Setting user from redis!");
user.setData(redisData, huskSync.getSettings(), huskSync.getEventCannon())
.thenRun(() -> handleSynchronisationCompletion(user)).join();
executor.shutdown();
})).join();
currentMilliseconds.addAndGet(200);
}, 0, 200L, TimeUnit.MILLISECONDS);
});
}
});
}
}));
});
}
private CompletableFuture<Void> setUserFromDatabase(@NotNull OnlineUser user) {
return huskSync.getDatabase().getCurrentUserData(user)
.thenAccept(databaseUserData -> databaseUserData.ifPresent(databaseData -> user
.setData(databaseData.userData(), huskSync.getSettings()).join()));
.thenAccept(databaseUserData -> databaseUserData.ifPresent(databaseData ->
user.setData(databaseData.userData(), huskSync.getSettings(),
huskSync.getEventCannon()).join()));
}
private void handleSynchronisationCompletion(@NotNull OnlineUser user) {
huskSync.getLocales().getLocale("synchronisation_complete").ifPresent(user::sendActionBar);
usersAwaitingSync.remove(user.uuid);
huskSync.getDatabase().ensureUser(user).join();
huskSync.getEventCannon().fireSyncCompleteEvent(user);
}
public final void handlePlayerQuit(@NotNull OnlineUser user) {
@ -89,9 +114,14 @@ public abstract class EventListener {
if (disabling) {
return;
}
// Don't sync players awaiting synchronization
if (usersAwaitingSync.contains(user.uuid)) {
return;
}
huskSync.getRedisManager().setUserServerSwitch(user).thenRun(() -> user.getUserData().thenAccept(
userData -> huskSync.getRedisManager().setUserData(user, userData).thenRun(
() -> huskSync.getDatabase().setUserData(user, userData).join())));
() -> huskSync.getDatabase().setUserData(user, userData, DataSaveCause.DISCONNECT).join())));
usersAwaitingSync.remove(user.uuid);
}
public final void handleWorldSave(@NotNull List<OnlineUser> usersInWorld) {
@ -99,20 +129,20 @@ public abstract class EventListener {
return;
}
CompletableFuture.runAsync(() -> usersInWorld.forEach(user ->
huskSync.getDatabase().setUserData(user, user.getUserData().join()).join()));
huskSync.getDatabase().setUserData(user, user.getUserData().join(), DataSaveCause.WORLD_SAVE).join()));
}
public final void handlePluginDisable() {
disabling = true;
huskSync.getOnlineUsers().stream().filter(user -> !usersAwaitingSync.contains(user.uuid)).forEach(user ->
huskSync.getDatabase().setUserData(user, user.getUserData().join()).join());
huskSync.getDatabase().setUserData(user, user.getUserData().join(), DataSaveCause.SERVER_SHUTDOWN).join());
huskSync.getDatabase().close();
huskSync.getRedisManager().close();
}
public final void handleMenuClose(@NotNull OnlineUser user, @NotNull InventoryData menuInventory) {
public final void handleMenuClose(@NotNull OnlineUser user, @NotNull ItemData menuInventory) {
if (disabling) {
return;
}

@ -4,8 +4,11 @@ import de.themoep.minedown.MineDown;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.*;
import net.william278.husksync.editor.InventoryEditorMenu;
import net.william278.husksync.event.EventCannon;
import net.william278.husksync.event.PreSyncEvent;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@ -29,49 +32,42 @@ public abstract class OnlineUser extends User {
/**
* Set the player's {@link StatusData}
*
* @param statusData the player's {@link StatusData}
* @param setHealth whether to set the player's health
* @param setMaxHealth whether to set the player's max health
* @param setHunger whether to set the player's hunger
* @param setExperience whether to set the player's experience
* @param setGameMode whether to set the player's game mode
* @param statusData the player's {@link StatusData}
* @param statusDataFlags the flags to use for setting the status data
* @return a future returning void when complete
*/
public abstract CompletableFuture<Void> setStatus(@NotNull StatusData statusData,
final boolean setHealth, final boolean setMaxHealth,
final boolean setHunger, final boolean setExperience,
final boolean setGameMode, final boolean setFlying,
final boolean setSelectedItemSlot);
@NotNull List<StatusDataFlag> statusDataFlags);
/**
* Get the player's inventory {@link InventoryData} contents
* Get the player's inventory {@link ItemData} contents
*
* @return The player's inventory {@link InventoryData} contents
* @return The player's inventory {@link ItemData} contents
*/
public abstract CompletableFuture<InventoryData> getInventory();
public abstract CompletableFuture<ItemData> getInventory();
/**
* Set the player's {@link InventoryData}
* Set the player's {@link ItemData}
*
* @param inventoryData The player's {@link InventoryData}
* @param itemData The player's {@link ItemData}
* @return a future returning void when complete
*/
public abstract CompletableFuture<Void> setInventory(@NotNull InventoryData inventoryData);
public abstract CompletableFuture<Void> setInventory(@NotNull ItemData itemData);
/**
* Get the player's ender chest {@link InventoryData} contents
* Get the player's ender chest {@link ItemData} contents
*
* @return The player's ender chest {@link InventoryData} contents
* @return The player's ender chest {@link ItemData} contents
*/
public abstract CompletableFuture<InventoryData> getEnderChest();
public abstract CompletableFuture<ItemData> getEnderChest();
/**
* Set the player's {@link InventoryData}
* Set the player's {@link ItemData}
*
* @param enderChestData The player's {@link InventoryData}
* @param enderChestData The player's {@link ItemData}
* @return a future returning void when complete
*/
public abstract CompletableFuture<Void> setEnderChest(@NotNull InventoryData enderChestData);
public abstract CompletableFuture<Void> setEnderChest(@NotNull ItemData enderChestData);
/**
@ -170,49 +166,40 @@ public abstract class OnlineUser extends User {
* @param settings Plugin settings, for determining what needs setting
* @return a future that will be completed when done
*/
public final CompletableFuture<Void> setData(@NotNull UserData data, @NotNull Settings settings) {
public final CompletableFuture<Void> setData(@NotNull UserData data, @NotNull Settings settings,
@NotNull EventCannon eventCannon) {
return CompletableFuture.runAsync(() -> {
try {
// Don't set offline players
if (isOffline()) {
return;
final PreSyncEvent preSyncEvent = (PreSyncEvent) eventCannon.firePreSyncEvent(this, data).join();
final UserData finalData = preSyncEvent.getUserData();
final List<CompletableFuture<Void>> dataSetOperations = new ArrayList<>() {{
if (!isOffline() && !isDead() && !preSyncEvent.isCancelled()) {
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)) {
add(setInventory(finalData.getInventoryData()));
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ENDER_CHESTS)) {
add(setEnderChest(finalData.getEnderChestData()));
}
add(setStatus(finalData.getStatusData(), StatusDataFlag.getFromSettings(settings)));
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_POTION_EFFECTS)) {
add(setPotionEffects(finalData.getPotionEffectsData()));
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ADVANCEMENTS)) {
add(setAdvancements(finalData.getAdvancementData()));
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_STATISTICS)) {
add(setStatistics(finalData.getStatisticsData()));
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION)) {
add(setLocation(finalData.getLocationData()));
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER)) {
add(setPersistentDataContainer(finalData.getPersistentDataContainerData()));
}
}
// Don't set dead players
if (isDead()) {
return;
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)) {
setInventory(data.getInventoryData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ENDER_CHESTS)) {
setEnderChest(data.getEnderChestData()).join();
}
setStatus(data.getStatusData(), settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HEALTH),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_MAX_HEALTH),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HUNGER),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_EXPERIENCE),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_GAME_MODE),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)).join();
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_POTION_EFFECTS)) {
setPotionEffects(data.getPotionEffectData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ADVANCEMENTS)) {
setAdvancements(data.getAdvancementData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_STATISTICS)) {
setStatistics(data.getStatisticData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER)) {
setPersistentDataContainer(data.getPersistentDataContainerData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION)) {
setLocation(data.getLocationData()).join();
}
} catch (Exception e) {
e.printStackTrace();
}
}};
CompletableFuture.allOf(dataSetOperations.toArray(new CompletableFuture[0])).join();
});
}
/**

@ -4,6 +4,7 @@ import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataAdapter;
import net.william278.husksync.data.UserData;
import net.william278.husksync.player.User;
import net.william278.husksync.util.Logger;
import org.jetbrains.annotations.NotNull;
import org.xerial.snappy.Snappy;
import redis.clients.jedis.Jedis;
@ -13,6 +14,7 @@ import redis.clients.jedis.exceptions.JedisException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Optional;
import java.util.UUID;
@ -29,6 +31,7 @@ public class RedisManager {
private final JedisPoolConfig jedisPoolConfig;
private final DataAdapter dataAdapter;
private final Logger logger;
private final String redisHost;
private final int redisPort;
private final String redisPassword;
@ -36,9 +39,12 @@ public class RedisManager {
private JedisPool jedisPool;
public RedisManager(@NotNull Settings settings, @NotNull DataAdapter dataAdapter) {
public RedisManager(@NotNull Settings settings, @NotNull DataAdapter dataAdapter, @NotNull Logger logger) {
clusterId = settings.getStringValue(Settings.ConfigOption.CLUSTER_ID);
this.dataAdapter = dataAdapter;
this.logger = logger;
// Set redis credentials
this.redisHost = settings.getStringValue(Settings.ConfigOption.REDIS_HOST);
this.redisPort = settings.getIntegerValue(Settings.ConfigOption.REDIS_PORT);
this.redisPassword = settings.getStringValue(Settings.ConfigOption.REDIS_PASSWORD);
@ -87,6 +93,8 @@ public class RedisManager {
jedis.setex(getKey(RedisKeyType.DATA_UPDATE, user.uuid),
RedisKeyType.DATA_UPDATE.timeToLive,
dataAdapter.toBytes(userData));
logger.debug("[" + user.username + "] Set " + RedisKeyType.DATA_UPDATE.name() + " key to redis at: " +
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
}
});
} catch (Exception e) {
@ -100,6 +108,10 @@ public class RedisManager {
try (Jedis jedis = jedisPool.getResource()) {
jedis.setex(getKey(RedisKeyType.SERVER_SWITCH, user.uuid),
RedisKeyType.SERVER_SWITCH.timeToLive, new byte[0]);
logger.debug("[" + user.username + "] Set " + RedisKeyType.SERVER_SWITCH.name() + " key to redis at: " +
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
} catch (Exception e) {
e.printStackTrace();
}
});
}
@ -114,7 +126,8 @@ public class RedisManager {
return CompletableFuture.supplyAsync(() -> {
try (Jedis jedis = jedisPool.getResource()) {
final byte[] key = getKey(RedisKeyType.DATA_UPDATE, user.uuid);
System.out.println("Reading key at " + new Date().getTime());
logger.debug("[" + user.username + "] Read " + RedisKeyType.DATA_UPDATE.name() + " key from redis at: " +
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
final byte[] dataByteArray = jedis.get(key);
if (dataByteArray == null) {
return Optional.empty();
@ -124,6 +137,9 @@ public class RedisManager {
// Use Snappy to decompress the json
return Optional.of(dataAdapter.fromBytes(dataByteArray));
} catch (Exception e) {
e.printStackTrace();
return Optional.empty();
}
});
}
@ -132,13 +148,18 @@ public class RedisManager {
return CompletableFuture.supplyAsync(() -> {
try (Jedis jedis = jedisPool.getResource()) {
final byte[] key = getKey(RedisKeyType.SERVER_SWITCH, user.uuid);
final byte[] compressedJson = jedis.get(key);
if (compressedJson == null) {
logger.debug("[" + user.username + "] Read " + RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " +
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
final byte[] readData = jedis.get(key);
if (readData == null) {
return false;
}
// Consume the key (delete from redis)
jedis.del(key);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
});
}

@ -1,20 +1,34 @@
package net.william278.husksync.util;
import org.jetbrains.annotations.NotNull;
import java.util.logging.Level;
/**
* An abstract, cross-platform representation of a logger
*/
public interface Logger {
public abstract class Logger {
private boolean debug;
public abstract void log(@NotNull Level level, @NotNull String message, @NotNull Exception e);
public abstract void log(@NotNull Level level, @NotNull String message);
void log(Level level, String message, Exception e);
public abstract void info(@NotNull String message);
void log(Level level, String message);
public abstract void severe(@NotNull String message);
void info(String message);
public final void debug(@NotNull String message) {
if (debug) {
log(Level.INFO, "[DEBUG] " + message);
}
}
void severe(String message);
public abstract void config(@NotNull String message);
void config(String message);
public final void showDebugLogs(boolean debug) {
this.debug = debug;
}
}

@ -7,6 +7,7 @@
language: 'en-gb'
check_for_updates: true
cluster_id: ''
debug_logging: true
database:
credentials:
@ -37,6 +38,7 @@ synchronization:
max_user_data_records: 5
save_on_world_save: true
compress_data: true
network_latency_milliseconds: 500
features:
inventories: true
ender_chests: true

@ -10,10 +10,11 @@ CREATE TABLE IF NOT EXISTS `%players_table%`
# Create the player data table if it does not exist
CREATE TABLE IF NOT EXISTS `%data_table%`
(
`version_uuid` char(36) NOT NULL,
`player_uuid` char(36) NOT NULL,
`timestamp` datetime NOT NULL,
`data` mediumblob NOT NULL,
`version_uuid` char(36) NOT NULL,
`player_uuid` char(36) NOT NULL,
`timestamp` datetime NOT NULL,
`save_cause` varchar(32) NOT NULL,
`data` mediumblob NOT NULL,
PRIMARY KEY (`version_uuid`),
FOREIGN KEY (`player_uuid`) REFERENCES `%players_table%` (`uuid`) ON DELETE CASCADE

@ -0,0 +1,75 @@
package net.william278.husksync.data;
import net.william278.husksync.player.DummyPlayer;
import net.william278.husksync.player.OnlineUser;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
public class DataAdaptionTests {
@Test
public void testJsonDataAdapter() {
final OnlineUser dummyUser = DummyPlayer.create();
final UserData dummyUserData = dummyUser.getUserData().join();
final DataAdapter dataAdapter = new JsonDataAdapter();
final byte[] data = dataAdapter.toBytes(dummyUserData);
final UserData deserializedUserData = dataAdapter.fromBytes(data);
boolean isEquals = deserializedUserData.getInventoryData().serializedItems
.equals(dummyUserData.getInventoryData().serializedItems)
&& deserializedUserData.getEnderChestData().serializedItems
.equals(dummyUserData.getEnderChestData().serializedItems)
&& deserializedUserData.getPotionEffectsData().serializedPotionEffects
.equals(dummyUserData.getPotionEffectsData().serializedPotionEffects)
&& deserializedUserData.getStatusData().health == dummyUserData.getStatusData().health
&& deserializedUserData.getStatusData().hunger == dummyUserData.getStatusData().hunger
&& deserializedUserData.getStatusData().saturation == dummyUserData.getStatusData().saturation
&& deserializedUserData.getStatusData().saturationExhaustion == dummyUserData.getStatusData().saturationExhaustion
&& deserializedUserData.getStatusData().selectedItemSlot == dummyUserData.getStatusData().selectedItemSlot
&& deserializedUserData.getStatusData().totalExperience == dummyUserData.getStatusData().totalExperience
&& deserializedUserData.getStatusData().maxHealth == dummyUserData.getStatusData().maxHealth
&& deserializedUserData.getStatusData().healthScale == dummyUserData.getStatusData().healthScale;
Assertions.assertTrue(isEquals);
}
@Test
public void testJsonFormat() {
final OnlineUser dummyUser = DummyPlayer.create();
final UserData dummyUserData = dummyUser.getUserData().join();
final DataAdapter dataAdapter = new JsonDataAdapter();
final byte[] data = dataAdapter.toBytes(dummyUserData);
final String json = new String(data, StandardCharsets.UTF_8);
final String expectedJson = "{\"status\":{\"health\":20.0,\"max_health\":20.0,\"health_scale\":0.0,\"hunger\":20,\"saturation\":5.0,\"saturation_exhaustion\":5.0,\"selected_item_slot\":1,\"total_experience\":100,\"experience_level\":1,\"experience_progress\":1.0,\"game_mode\":\"SURVIVAL\",\"is_flying\":false},\"inventory\":{\"serialized_inventory\":\"\"},\"ender_chest\":{\"serialized_inventory\":\"\"},\"potion_effects\":{\"serialized_potion_effects\":\"\"},\"advancements\":[],\"statistics\":{\"untyped_statistics\":{},\"block_statistics\":{},\"item_statistics\":{},\"entity_statistics\":{}},\"location\":{\"world_name\":\"dummy_world\",\"world_uuid\":\"00000000-0000-0000-0000-000000000000\",\"world_environment\":\"NORMAL\",\"x\":0.0,\"y\":64.0,\"z\":0.0,\"yaw\":90.0,\"pitch\":180.0},\"persistent_data_container\":{\"persistent_data_map\":{}},\"format_version\":1}";
Assertions.assertEquals(expectedJson, json);
}
@Test
public void testCompressedDataAdapter() {
final OnlineUser dummyUser = DummyPlayer.create();
final UserData dummyUserData = dummyUser.getUserData().join();
final DataAdapter dataAdapter = new CompressedDataAdapter();
final byte[] data = dataAdapter.toBytes(dummyUserData);
final UserData deserializedUserData = dataAdapter.fromBytes(data);
boolean isEquals = deserializedUserData.getInventoryData().serializedItems
.equals(dummyUserData.getInventoryData().serializedItems)
&& deserializedUserData.getEnderChestData().serializedItems
.equals(dummyUserData.getEnderChestData().serializedItems)
&& deserializedUserData.getPotionEffectsData().serializedPotionEffects
.equals(dummyUserData.getPotionEffectsData().serializedPotionEffects)
&& deserializedUserData.getStatusData().health == dummyUserData.getStatusData().health
&& deserializedUserData.getStatusData().hunger == dummyUserData.getStatusData().hunger
&& deserializedUserData.getStatusData().saturation == dummyUserData.getStatusData().saturation
&& deserializedUserData.getStatusData().saturationExhaustion == dummyUserData.getStatusData().saturationExhaustion
&& deserializedUserData.getStatusData().selectedItemSlot == dummyUserData.getStatusData().selectedItemSlot
&& deserializedUserData.getStatusData().totalExperience == dummyUserData.getStatusData().totalExperience
&& deserializedUserData.getStatusData().maxHealth == dummyUserData.getStatusData().maxHealth
&& deserializedUserData.getStatusData().healthScale == dummyUserData.getStatusData().healthScale;
Assertions.assertTrue(isEquals);
}
}

@ -0,0 +1,156 @@
package net.william278.husksync.player;
import de.themoep.minedown.MineDown;
import net.william278.husksync.data.*;
import net.william278.husksync.editor.InventoryEditorMenu;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class DummyPlayer extends OnlineUser {
private DummyPlayer(@NotNull UUID uuid, @NotNull String username) {
super(uuid, username);
}
public static DummyPlayer create() {
return new DummyPlayer(UUID.fromString("00000000-0000-0000-0000-000000000000"),
"DummyPlayer");
}
@Override
public CompletableFuture<StatusData> getStatus() {
return CompletableFuture.supplyAsync(() -> new StatusData(20, 20, 0,
20, 5, 5, 1,
100, 1, 1f, "SURVIVAL", false));
}
@Override
public CompletableFuture<Void> setStatus(@NotNull StatusData statusData, @NotNull List<StatusDataFlag> statusDataFlags) {
return CompletableFuture.runAsync(() -> {
// do nothing
});
}
@Override
public CompletableFuture<ItemData> getInventory() {
return CompletableFuture.supplyAsync(() -> new ItemData(""));
}
@Override
public CompletableFuture<Void> setInventory(@NotNull ItemData itemData) {
return CompletableFuture.runAsync(() -> {
// do nothing
});
}
@Override
public CompletableFuture<ItemData> getEnderChest() {
return CompletableFuture.supplyAsync(() -> new ItemData(""));
}
@Override
public CompletableFuture<Void> setEnderChest(@NotNull ItemData enderChestData) {
return CompletableFuture.runAsync(() -> {
// do nothing
});
}
@Override
public CompletableFuture<PotionEffectData> getPotionEffects() {
return CompletableFuture.supplyAsync(() -> new PotionEffectData(""));
}
@Override
public CompletableFuture<Void> setPotionEffects(@NotNull PotionEffectData potionEffectData) {
return CompletableFuture.runAsync(() -> {
// do nothing
});
}
@Override
public CompletableFuture<List<AdvancementData>> getAdvancements() {
return CompletableFuture.supplyAsync(ArrayList::new);
}
@Override
public CompletableFuture<Void> setAdvancements(@NotNull List<AdvancementData> advancementData) {
return CompletableFuture.runAsync(() -> {
// do nothing
});
}
@Override
public CompletableFuture<StatisticsData> getStatistics() {
return CompletableFuture.supplyAsync(() -> new StatisticsData(new HashMap<>(),
new HashMap<>(), new HashMap<>(), new HashMap<>()));
}
@Override
public CompletableFuture<Void> setStatistics(@NotNull StatisticsData statisticsData) {
return CompletableFuture.runAsync(() -> {
// do nothing
});
}
@Override
public CompletableFuture<LocationData> getLocation() {
return CompletableFuture.supplyAsync(() -> new LocationData("dummy_world",
UUID.fromString("00000000-0000-0000-0000-000000000000"),
"NORMAL", 0, 64, 0, 90f, 180f));
}
@Override
public CompletableFuture<Void> setLocation(@NotNull LocationData locationData) {
return CompletableFuture.runAsync(() -> {
// do nothing
});
}
@Override
public CompletableFuture<PersistentDataContainerData> getPersistentDataContainer() {
return CompletableFuture.supplyAsync(() -> new PersistentDataContainerData(new HashMap<>()));
}
@Override
public CompletableFuture<Void> setPersistentDataContainer(@NotNull PersistentDataContainerData persistentDataContainerData) {
return CompletableFuture.runAsync(() -> {
// do nothing
});
}
@Override
public boolean isDead() {
return false;
}
@Override
public boolean isOffline() {
return false;
}
@Override
public void sendMessage(@NotNull MineDown mineDown) {
// do nothing
}
@Override
public void sendActionBar(@NotNull MineDown mineDown) {
// do nothing
}
@Override
public boolean hasPermission(@NotNull String node) {
return true;
}
@Override
public void showMenu(@NotNull InventoryEditorMenu menu) {
// do nothing
}
}
Loading…
Cancel
Save