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