mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-26 18:19:10 +00:00
Events & API work, save DataSaveCauses as part of versioning
This commit is contained in:
@@ -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();
|
||||
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<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 {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user