9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-25 17:49:20 +00:00

Userdata command, API expansion, editor interfaces

This commit is contained in:
William
2022-07-08 01:18:01 +01:00
parent 1c9d74f925
commit b7709f2d6c
43 changed files with 1044 additions and 446 deletions

View File

@@ -0,0 +1,137 @@
package net.william278.husksync.api;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.UserData;
import net.william278.husksync.data.VersionedUserData;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
* The base implementation of the HuskSync API, containing cross-platform API calls.
* </p>
* This class should not be used directly, but rather through platform-specific extending API classes.
*/
@SuppressWarnings("unused")
public abstract class BaseHuskSyncAPI {
/**
* <b>(Internal use only)</b> - Instance of the implementing plugin.
*/
protected final HuskSync plugin;
protected BaseHuskSyncAPI(@NotNull HuskSync plugin) {
this.plugin = plugin;
}
/**
* Returns a {@link User} by the given player's account {@link UUID}, if they exist.
*
* @param uuid the unique id of the player to get the {@link User} instance for
* @return future returning the {@link User} instance for the given player's unique id if they exist, otherwise an empty {@link Optional}
* @apiNote The player does not have to be online
* @since 2.0
*/
public final CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid) {
return plugin.getDatabase().getUser(uuid);
}
/**
* Returns a {@link User} by the given player's username (case-insensitive), if they exist.
*
* @param username the username of the {@link User} instance for
* @return future returning the {@link User} instance for the given player's username if they exist,
* otherwise an empty {@link Optional}
* @apiNote The player does not have to be online, though their username has to be the username
* they had when they last joined the server.
* @since 2.0
*/
public final CompletableFuture<Optional<User>> getUser(@NotNull String username) {
return plugin.getDatabase().getUserByName(username);
}
/**
* Returns a {@link User}'s current {@link UserData}
*
* @param user the {@link User} to get the {@link UserData} for
* @return future returning the {@link UserData} for the given {@link User} if they exist, otherwise an empty {@link Optional}
* @apiNote If the user is not online on the implementing bukkit server,
* the {@link UserData} returned will be their last database-saved UserData.
* </p>
* Because of this, if the user is online on another server on the network,
* then the {@link UserData} returned by this method will <i>not necessarily reflective of
* their current state</i>
* @since 2.0
*/
public final CompletableFuture<Optional<UserData>> getUserData(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> {
if (user instanceof OnlineUser) {
return Optional.of(((OnlineUser) user).getUserData().join());
} else {
return plugin.getDatabase().getCurrentUserData(user).join().map(VersionedUserData::userData);
}
});
}
/**
* Sets the {@link UserData} to the database for the given {@link User}.
* </p>
* If the user is online and on the same cluster, their data will be updated in game.
*
* @param user the {@link User} to set the {@link UserData} for
* @param userData the {@link UserData} to set for the given {@link User}
* @return future returning void when complete
* @since 2.0
*/
public final CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData) {
return CompletableFuture.runAsync(() ->
plugin.getDatabase().setUserData(user, userData, DataSaveCause.API)
.thenRun(() -> plugin.getRedisManager().sendUserDataUpdate(user, userData).join()));
}
/**
* Saves the {@link UserData} of an {@link OnlineUser} to the database
*
* @param user the {@link OnlineUser} to save the {@link UserData} of
* @return future returning void when complete
* @since 2.0
*/
public final CompletableFuture<Void> saveUserData(@NotNull OnlineUser user) {
return CompletableFuture.runAsync(() -> user.getUserData().thenAccept(userData ->
plugin.getDatabase().setUserData(user, userData, DataSaveCause.API).join()));
}
/**
* Returns the saved {@link VersionedUserData} records for the given {@link User}
*
* @param user the {@link User} to get the {@link VersionedUserData} for
* @return future returning a list {@link VersionedUserData} for the given {@link User} if they exist,
* otherwise an empty {@link Optional}
* @apiNote The length of the list of VersionedUserData will correspond to the configured
* {@code max_user_data_records} config option
* @since 2.0
*/
public final CompletableFuture<List<VersionedUserData>> getSavedUserData(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> plugin.getDatabase().getUserData(user).join());
}
/**
* Returns the JSON string representation of the given {@link UserData}
*
* @param userData the {@link UserData} to get the JSON string representation of
* @param prettyPrint whether to pretty print the JSON string
* @return the JSON string representation of the given {@link UserData}
* @since 2.0
*/
@NotNull
public final String getUserDataJson(@NotNull UserData userData, boolean prettyPrint) {
return plugin.getDataAdapter().toJson(userData, prettyPrint);
}
}

View File

@@ -1,66 +0,0 @@
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;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public class EchestCommand extends CommandBase {
public EchestCommand(@NotNull HuskSync implementor) {
super("echest", Permission.COMMAND_VIEW_INVENTORIES, implementor, "openechest");
}
@Override
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
if (args.length == 0 || args.length > 2) {
plugin.getLocales().getLocale("error_invalid_syntax", "/echest <player>")
.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();
}
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)));
}
}

View File

@@ -0,0 +1,82 @@
package net.william278.husksync.command;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.ItemData;
import net.william278.husksync.data.UserData;
import net.william278.husksync.data.VersionedUserData;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.editor.ItemEditorMenu;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class EnderChestCommand extends CommandBase {
public EnderChestCommand(@NotNull HuskSync implementor) {
super("enderchest", Permission.COMMAND_ENDER_CHEST, implementor, "echest", "openechest");
}
@Override
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
if (args.length == 0 || args.length > 2) {
plugin.getLocales().getLocale("error_invalid_syntax", "/enderchest <player>")
.ifPresent(player::sendMessage);
return;
}
plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAccept(optionalUser ->
optionalUser.ifPresentOrElse(user -> {
if (args.length == 2) {
// View user data by specified UUID
try {
final UUID versionUuid = UUID.fromString(args[1]);
plugin.getDatabase().getUserData(user).thenAccept(userDataList -> userDataList.stream()
.filter(userData -> userData.versionUUID().equals(versionUuid)).findFirst().ifPresentOrElse(
userData -> showEnderChestMenu(player, userData, user, userDataList.stream().sorted().findFirst()
.map(VersionedUserData::versionUUID).orElse(UUID.randomUUID()).equals(versionUuid)),
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage)));
} catch (IllegalArgumentException e) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/enderchest <player> [version_uuid]").ifPresent(player::sendMessage);
}
} else {
// View latest user data
plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse(
versionedUserData -> showEnderChestMenu(player, versionedUserData, user, true),
() -> plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(player::sendMessage)));
}
}, () -> plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(player::sendMessage)));
}
private void showEnderChestMenu(@NotNull OnlineUser player, @NotNull VersionedUserData versionedUserData,
@NotNull User dataOwner, final boolean allowEdit) {
CompletableFuture.runAsync(() -> {
final UserData data = versionedUserData.userData();
final ItemEditorMenu menu = ItemEditorMenu.createEnderChestMenu(data.getEnderChestData(),
dataOwner, player, plugin.getLocales(), allowEdit);
plugin.getLocales().getLocale("viewing_ender_chest_of", dataOwner.username,
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
.format(versionedUserData.versionTimestamp()))
.ifPresent(player::sendMessage);
final ItemData enderChestDataOnClose = plugin.getDataEditor().openItemEditorMenu(player, menu).join();
if (!menu.canEdit) {
return;
}
final UserData updatedUserData = new UserData(data.getStatusData(), data.getInventoryData(),
enderChestDataOnClose, data.getPotionEffectsData(), data.getAdvancementData(),
data.getStatisticsData(), data.getLocationData(),
data.getPersistentDataContainerData());
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.ENDER_CHEST_COMMAND_EDIT).join();
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
});
}
}

View File

@@ -50,10 +50,11 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
return;
}
plugin.reload();
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| Reloaded config & message files.]((#00fb9a)"));
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| Reloaded config & message files.](#00fb9a)"));
}
default ->
plugin.getLocales().getLocale("error_invalid_syntax", "/husksync <update/info/reload>").ifPresent(player::sendMessage);
default -> plugin.getLocales().getLocale("error_invalid_syntax",
"/husksync <update/info/reload>")
.ifPresent(player::sendMessage);
}
}
@@ -75,8 +76,8 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
case "migrate" -> {
//todo - MPDB migrator
}
default ->
plugin.getLoggingAdapter().log(Level.INFO, "Invalid syntax. Console usage: \"husksync <update/info/reload/migrate>\"");
default -> plugin.getLoggingAdapter().log(Level.INFO,
"Invalid syntax. Console usage: \"husksync <update/info/reload/migrate>\"");
}
}

View File

@@ -0,0 +1,81 @@
package net.william278.husksync.command;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.ItemData;
import net.william278.husksync.data.UserData;
import net.william278.husksync.data.VersionedUserData;
import net.william278.husksync.editor.ItemEditorMenu;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull;
import java.text.DateFormat;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class InventoryCommand extends CommandBase {
public InventoryCommand(@NotNull HuskSync implementor) {
super("inventory", Permission.COMMAND_INVENTORY, implementor, "invsee", "openinv");
}
@Override
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
if (args.length == 0 || args.length > 2) {
plugin.getLocales().getLocale("error_invalid_syntax", "/inventory <player>")
.ifPresent(player::sendMessage);
return;
}
plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAccept(optionalUser ->
optionalUser.ifPresentOrElse(user -> {
if (args.length == 2) {
// View user data by specified UUID
try {
final UUID versionUuid = UUID.fromString(args[1]);
plugin.getDatabase().getUserData(user).thenAccept(userDataList -> userDataList.stream()
.filter(userData -> userData.versionUUID().equals(versionUuid)).findFirst().ifPresentOrElse(
userData -> showInventoryMenu(player, userData, user, userDataList.stream().sorted().findFirst()
.map(VersionedUserData::versionUUID).orElse(UUID.randomUUID()).equals(versionUuid)),
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage)));
} catch (IllegalArgumentException e) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/inventory <player> [version_uuid]").ifPresent(player::sendMessage);
}
} else {
// View latest user data
plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse(
versionedUserData -> showInventoryMenu(player, versionedUserData, user, true),
() -> plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(player::sendMessage)));
}
}, () -> plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(player::sendMessage)));
}
private void showInventoryMenu(@NotNull OnlineUser player, @NotNull VersionedUserData versionedUserData,
@NotNull User dataOwner, boolean allowEdit) {
CompletableFuture.runAsync(() -> {
final UserData data = versionedUserData.userData();
final ItemEditorMenu menu = ItemEditorMenu.createInventoryMenu(data.getInventoryData(),
dataOwner, player, plugin.getLocales(), allowEdit);
plugin.getLocales().getLocale("viewing_inventory_of", dataOwner.username,
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
.format(versionedUserData.versionTimestamp()))
.ifPresent(player::sendMessage);
final ItemData inventoryDataOnClose = plugin.getDataEditor().openItemEditorMenu(player, menu).join();
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(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND_EDIT).join();
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
});
}
}

View File

@@ -1,66 +0,0 @@
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;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public class InvseeCommand extends CommandBase {
public InvseeCommand(@NotNull HuskSync implementor) {
super("invsee", Permission.COMMAND_VIEW_INVENTORIES, implementor, "openinv");
}
@Override
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
if (args.length == 0 || args.length > 2) {
plugin.getLocales().getLocale("error_invalid_syntax", "/invsee <player>")
.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();
}
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)));
}
}

View File

@@ -30,45 +30,51 @@ public enum Permission {
/**
* Lets the user save a player's data {@code /husksync save (player)}
*/
COMMAND_HUSKSYNC_SAVE("husksync.command.husksync.save", DefaultAccess.OPERATORS),
COMMAND_HUSKSYNC_SAVE("husksync.command.husksync.save", DefaultAccess.OPERATORS), // todo
/**
* Lets the user save all online player data {@code /husksync saveall}
*/
COMMAND_HUSKSYNC_SAVE_ALL("husksync.command.husksync.saveall", DefaultAccess.OPERATORS),
/**
* Lets the user view a player's backup data {@code /husksync backup (player)}
*/
COMMAND_HUSKSYNC_BACKUPS("husksync.command.husksync.backups", DefaultAccess.OPERATORS),
/**
* Lets the user restore a player's backup data {@code /husksync backup (player) restore (backup_uuid)}
*/
COMMAND_HUSKSYNC_BACKUPS_RESTORE("husksync.command.husksync.backups.restore", DefaultAccess.OPERATORS),
COMMAND_HUSKSYNC_SAVE_ALL("husksync.command.husksync.saveall", DefaultAccess.OPERATORS), //todo
/*
* /invsee command permissions
* /inventory command permissions
*/
/**
* Lets the user use the {@code /invsee (player)} command and view offline players' inventories
* Lets the user use the {@code /inventory (player)} command and view offline players' inventories
*/
COMMAND_VIEW_INVENTORIES("husksync.command.invsee", DefaultAccess.OPERATORS),
COMMAND_INVENTORY("husksync.command.inventory", DefaultAccess.OPERATORS),
/**
* Lets the user edit the contents of offline players' inventories
*/
COMMAND_EDIT_INVENTORIES("husksync.command.invsee.edit", DefaultAccess.OPERATORS),
COMMAND_INVENTORY_EDIT("husksync.command.inventory.edit", DefaultAccess.OPERATORS),
/*
* /echest command permissions
* /enderchest command permissions
*/
/**
* Lets the user use the {@code /echest (player)} command and view offline players' ender chests
* Lets the user use the {@code /enderchest (player)} command and view offline players' ender chests
*/
COMMAND_VIEW_ENDER_CHESTS("husksync.command.echest", DefaultAccess.OPERATORS),
COMMAND_ENDER_CHEST("husksync.command.enderchest", DefaultAccess.OPERATORS),
/**
* Lets the user edit the contents of offline players' ender chests
*/
COMMAND_EDIT_ENDER_CHESTS("husksync.command.echest.edit", DefaultAccess.OPERATORS);
COMMAND_ENDER_CHEST_EDIT("husksync.command.enderchest.edit", DefaultAccess.OPERATORS),
/*
* /userdata command permissions
*/
/**
* Lets the user view user data {@code /userdata view/list (player) (version_uuid)}
*/
COMMAND_USER_DATA("husksync.command.userdata", DefaultAccess.OPERATORS),
/**
* Lets the user restore and delete user data {@code /userdata restore/delete (player) (version_uuid)}
*/
COMMAND_USER_DATA_MANAGE("husksync.command.userdata.manage", DefaultAccess.OPERATORS);
public final String node;
public final DefaultAccess defaultAccess;

View File

@@ -0,0 +1,108 @@
package net.william278.husksync.command;
import net.william278.husksync.HuskSync;
import net.william278.husksync.player.OnlineUser;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class UserDataCommand extends CommandBase implements TabCompletable {
private final String[] COMMAND_ARGUMENTS = {"view", "list", "delete", "restore"};
public UserDataCommand(@NotNull HuskSync implementor) {
super("userdata", Permission.COMMAND_USER_DATA, implementor, "playerdata");
}
@Override
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
if (args.length < 1) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata <view|list|delete|restore> <username> [version_uuid]")
.ifPresent(player::sendMessage);
return;
}
switch (args[0].toLowerCase()) {
case "view" -> {
if (args.length < 2) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata view <username> [version_uuid]")
.ifPresent(player::sendMessage);
return;
}
final String username = args[1];
if (args.length >= 3) {
try {
final UUID versionUuid = UUID.fromString(args[2]);
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
optionalUser -> optionalUser.ifPresentOrElse(
user -> plugin.getDatabase().getUserData(user).thenAccept(
userDataList -> userDataList.stream().filter(versionedUserData -> versionedUserData
.versionUUID().equals(versionUuid))
.findFirst().ifPresentOrElse(userData ->
plugin.getDataEditor()
.displayDataOverview(player, userData, user),
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage))),
() -> plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(player::sendMessage))));
} catch (IllegalArgumentException e) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata view <username> [version_uuid]")
.ifPresent(player::sendMessage);
}
} else {
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
optionalUser -> optionalUser.ifPresentOrElse(
user -> plugin.getDatabase().getCurrentUserData(user).thenAccept(
latestData -> latestData.ifPresentOrElse(
userData -> plugin.getDataEditor()
.displayDataOverview(player, userData, user),
() -> plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(player::sendMessage))),
() -> plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(player::sendMessage))));
}
}
case "list" -> {
if (args.length < 2) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata list <username>")
.ifPresent(player::sendMessage);
return;
}
final String username = args[1];
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
optionalUser -> optionalUser.ifPresentOrElse(
user -> plugin.getDatabase().getUserData(user).thenAccept(dataList -> {
if (dataList.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(player::sendMessage);
return;
}
plugin.getDataEditor().displayDataList(player, dataList, user);
}),
() -> plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(player::sendMessage))));
}
case "delete" -> {
}
case "restore" -> {
}
}
}
@Override
public List<String> onTabComplete(@NotNull OnlineUser player, @NotNull String[] args) {
return Arrays.stream(COMMAND_ARGUMENTS)
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
.sorted().collect(Collectors.toList());
}
}

View File

@@ -1,13 +0,0 @@
package net.william278.husksync.data;
/**
* Indicates an error occurred during base-64 serialization and deserialization of data.
* </p>
* 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) {
super(message, cause);
}
}

View File

@@ -1,45 +1,81 @@
package net.william278.husksync.data;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.api.BaseHuskSyncAPI;
import net.william278.husksync.player.User;
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.
* @implNote This enum is saved in the database.
* </p>
* 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)
*
* @since 2.0
*/
DISCONNECT,
/**
* Indicates data saved when the world saved
*
* @since 2.0
*/
WORLD_SAVE,
/**
* Indicates data saved when the server shut down
*
* @since 2.0
*/
SERVER_SHUTDOWN,
/**
* Indicates data was saved by editing inventory contents via the {@code /invsee} command
* Indicates data was saved by editing inventory contents via the {@code /inventory} command
*
* @since 2.0
*/
INVSEE_COMMAND_EDIT,
INVENTORY_COMMAND_EDIT,
/**
* Indicates data was saved by editing Ender Chest contents via the {@code /echest} command
* Indicates data was saved by editing Ender Chest contents via the {@code /enderchest} command
*
* @since 2.0
*/
ECHEST_COMMAND_EDIT,
ENDER_CHEST_COMMAND_EDIT,
/**
* Indicates data was saved by restoring it from a previous version
*
* @since 2.0
*/
BACKUP_RESTORE,
/**
* Indicates data was saved by an API call
*
* @see BaseHuskSyncAPI#saveUserData(OnlineUser)
* @see BaseHuskSyncAPI#setUserData(User, UserData)
* @since 2.0
*/
API,
MPDB_IMPORT,
LEGACY_IMPORT,
MANUAL_IMPORT,
/**
* Indicates data was saved by an unknown cause.
* </p>
* This should not be used and is only used for error handling purposes.
*
* @since 2.0
*/
UNKNOWN;
/**
* Returns a {@link DataSaveCause} by name.
*
* @return the {@link DataSaveCause} or {@link #UNKNOWN} if the name is not valid.
*/
@NotNull
public static DataSaveCause getCauseByName(@NotNull String name) {
for (DataSaveCause cause : values()) {

View File

@@ -0,0 +1,15 @@
package net.william278.husksync.data;
import org.jetbrains.annotations.NotNull;
/**
* Indicates an error occurred during Base-64 serialization and deserialization of data.
* </p>
* For example, an exception deserializing {@link ItemData} item stack or {@link PotionEffectData} potion effect arrays
*/
public class DataSerializationException extends RuntimeException {
protected DataSerializationException(@NotNull String message, @NotNull Throwable cause) {
super(message, cause);
}
}

View File

@@ -3,7 +3,6 @@ package net.william278.husksync.data;
import com.google.gson.annotations.SerializedName;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
/**
@@ -15,7 +14,7 @@ public class StatisticsData {
* Map of untyped statistic names to their values
*/
@SerializedName("untyped_statistics")
public Map<String, Integer> untypedStatistic;
public Map<String, Integer> untypedStatistics;
/**
* Map of block type statistics to a map of material types to values
@@ -35,11 +34,11 @@ public class StatisticsData {
@SerializedName("entity_statistics")
public Map<String, Map<String, Integer>> entityStatistics;
public StatisticsData(@NotNull Map<String, Integer> untypedStatistic,
public StatisticsData(@NotNull Map<String, Integer> untypedStatistics,
@NotNull Map<String, Map<String, Integer>> blockStatistics,
@NotNull Map<String, Map<String, Integer>> itemStatistics,
@NotNull Map<String, Map<String, Integer>> entityStatistics) {
this.untypedStatistic = untypedStatistic;
this.untypedStatistics = untypedStatistics;
this.blockStatistics = blockStatistics;
this.itemStatistics = itemStatistics;
this.entityStatistics = entityStatistics;

View File

@@ -55,7 +55,7 @@ public class MySqlDatabase extends Database {
@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),
Math.max(1, Math.min(20, settings.getIntegerValue(Settings.ConfigOption.SYNCHRONIZATION_MAX_USER_DATA_RECORDS))),
resourceReader, dataAdapter, eventCannon, logger);
this.mySqlHost = settings.getStringValue(Settings.ConfigOption.DATABASE_HOST);
this.mySqlPort = settings.getIntegerValue(Settings.ConfigOption.DATABASE_PORT);

View File

@@ -1,13 +1,17 @@
package net.william278.husksync.editor;
import net.william278.husksync.command.Permission;
import net.william278.husksync.config.Locales;
import net.william278.husksync.data.AdvancementData;
import net.william278.husksync.data.ItemData;
import net.william278.husksync.data.VersionedUserData;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.UUID;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.CompletableFuture;
/**
@@ -19,31 +23,33 @@ public class DataEditor {
* Map of currently open inventory and ender chest data editors
*/
@NotNull
protected final HashMap<UUID, InventoryEditorMenu> openInventoryMenus;
protected final HashMap<UUID, ItemEditorMenu> openInventoryMenus;
public DataEditor() {
private final Locales locales;
public DataEditor(@NotNull Locales locales) {
this.openInventoryMenus = new HashMap<>();
this.locales = locales;
}
/**
* Open an inventory or ender chest editor menu
*
* @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(ItemData, User, OnlineUser)
* @see InventoryEditorMenu#createEnderChestMenu(ItemData, User, OnlineUser)
* @param user The online user to open the editor for
* @param itemEditorMenu The {@link ItemEditorMenu} to open
* @see ItemEditorMenu#createInventoryMenu(ItemData, User, OnlineUser, Locales, boolean)
* @see ItemEditorMenu#createEnderChestMenu(ItemData, User, OnlineUser, Locales, boolean)
*/
public CompletableFuture<ItemData> openInventoryMenu(@NotNull OnlineUser user,
@NotNull InventoryEditorMenu inventoryEditorMenu) {
this.openInventoryMenus.put(user.uuid, inventoryEditorMenu);
return inventoryEditorMenu.showInventory(user);
public CompletableFuture<ItemData> openItemEditorMenu(@NotNull OnlineUser user,
@NotNull ItemEditorMenu itemEditorMenu) {
this.openInventoryMenus.put(user.uuid, itemEditorMenu);
return itemEditorMenu.showInventory(user);
}
/**
* Close an inventory or ender chest editor menu
*
* @param user The online user to close the editor for
* @param user The online user to close the editor for
* @param itemData the {@link ItemData} contained within the menu at the time of closing
*/
public void closeInventoryMenu(@NotNull OnlineUser user, @NotNull ItemData itemData) {
@@ -67,7 +73,7 @@ public class DataEditor {
}
/**
* Display a chat message detailing information about {@link VersionedUserData}
* Display a chat menu detailing information about {@link VersionedUserData}
*
* @param user The online user to display the message to
* @param userData The {@link VersionedUserData} to display information about
@@ -75,11 +81,89 @@ public class DataEditor {
*/
public void displayDataOverview(@NotNull OnlineUser user, @NotNull VersionedUserData userData,
@NotNull User dataOwner) {
//todo
locales.getLocale("data_manager_title",
dataOwner.username, dataOwner.uuid.toString())
.ifPresent(user::sendMessage);
locales.getLocale("data_manager_versioning",
new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss").format(userData.versionTimestamp()),
userData.versionUUID().toString().split("-")[0],
userData.versionUUID().toString(),
userData.cause().name().toLowerCase().replaceAll("_", " "))
.ifPresent(user::sendMessage);
locales.getLocale("data_manager_status",
Double.toString(userData.userData().getStatusData().health),
Double.toString(userData.userData().getStatusData().maxHealth),
Double.toString(userData.userData().getStatusData().hunger),
Integer.toString(userData.userData().getStatusData().expLevel),
userData.userData().getStatusData().gameMode.toLowerCase())
.ifPresent(user::sendMessage);
locales.getLocale("data_manager_advancements_statistics",
Integer.toString(userData.userData().getAdvancementData().size()),
generateAdvancementPreview(userData.userData().getAdvancementData()),
String.format("%.2f", (((userData.userData().getStatisticsData().untypedStatistics.getOrDefault(
"PLAY_ONE_MINUTE", 0)) / 20d) / 60d) / 60d))
.ifPresent(user::sendMessage);
if (user.hasPermission(Permission.COMMAND_INVENTORY.node)
&& user.hasPermission(Permission.COMMAND_ENDER_CHEST.node)) {
locales.getLocale("data_manager_item_buttons",
dataOwner.username, userData.versionUUID().toString())
.ifPresent(user::sendMessage);
}
if (user.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
locales.getLocale("data_manager_management_buttons",
dataOwner.username, userData.versionUUID().toString())
.ifPresent(user::sendMessage);
}
}
private @NotNull String generateAdvancementPreview(@NotNull List<AdvancementData> advancementData) {
final StringJoiner joiner = new StringJoiner("\n");
final int PREVIEW_SIZE = 8;
for (int i = 0; i < advancementData.size(); i++) {
joiner.add(advancementData.get(i).key);
if (i >= PREVIEW_SIZE) {
break;
}
}
final int remainingAdvancements = advancementData.size() - PREVIEW_SIZE;
if (remainingAdvancements > 0) {
joiner.add(locales.getRawLocale("data_manager_advancement_preview_remaining",
Integer.toString(remainingAdvancements)).orElse("+" + remainingAdvancements + ""));
}
return joiner.toString();
}
/**
* Display a chat list detailing a player's saved list of {@link VersionedUserData}
*
* @param user The online user to display the message to
* @param userDataList The list of {@link VersionedUserData} to display
* @param dataOwner The {@link User} who owns the {@link VersionedUserData}
*/
public void displayDataList(@NotNull OnlineUser user, @NotNull List<VersionedUserData> userDataList,
@NotNull User dataOwner) {
locales.getLocale("data_list_title",
dataOwner.username, dataOwner.uuid.toString())
.ifPresent(user::sendMessage);
final String[] numberedIcons = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳" .split("");
for (int i = 0; i < Math.min(20, userDataList.size()); i++) {
final VersionedUserData userData = userDataList.get(i);
locales.getLocale("data_list_item",
numberedIcons[i],
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
.format(userData.versionTimestamp()),
userData.versionUUID().toString().split("-")[0],
userData.versionUUID().toString(),
userData.cause().name().toLowerCase().replaceAll("_", " "),
dataOwner.username)
.ifPresent(user::sendMessage);
}
}
/**
* Returns whether the user has an inventory editor menu open
*
* @param user {@link OnlineUser} to check
* @return {@code true} if the user has an inventory editor open; {@code false} otherwise
*/

View File

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

View File

@@ -0,0 +1,56 @@
package net.william278.husksync.editor;
import de.themoep.minedown.MineDown;
import net.william278.husksync.command.Permission;
import net.william278.husksync.config.Locales;
import net.william278.husksync.data.ItemData;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
public class ItemEditorMenu {
public final ItemData itemData;
public final int slotCount;
public final MineDown menuTitle;
public boolean canEdit;
private CompletableFuture<ItemData> inventoryDataCompletableFuture;
private ItemEditorMenu(@NotNull ItemData itemData, int slotCount,
@NotNull MineDown menuTitle, boolean canEdit) {
this.itemData = itemData;
this.menuTitle = menuTitle;
this.slotCount = slotCount;
this.canEdit = canEdit;
}
public CompletableFuture<ItemData> showInventory(@NotNull OnlineUser user) {
inventoryDataCompletableFuture = new CompletableFuture<>();
user.showMenu(this);
return inventoryDataCompletableFuture;
}
public void closeInventory(@NotNull ItemData itemData) {
inventoryDataCompletableFuture.complete(itemData);
}
public static ItemEditorMenu createInventoryMenu(@NotNull ItemData itemData, @NotNull User dataOwner,
@NotNull OnlineUser viewer, @NotNull Locales locales,
boolean canEdit) {
return new ItemEditorMenu(itemData, 45,
locales.getLocale("inventory_viewer_menu_title", dataOwner.username).orElse(new MineDown("")),
viewer.hasPermission(Permission.COMMAND_INVENTORY_EDIT.node) && canEdit);
}
public static ItemEditorMenu createEnderChestMenu(@NotNull ItemData itemData, @NotNull User dataOwner,
@NotNull OnlineUser viewer, @NotNull Locales locales,
boolean canEdit) {
return new ItemEditorMenu(itemData, 27,
locales.getLocale("ender_chest_viewer_menu_title", dataOwner.username).orElse(new MineDown("")),
viewer.hasPermission(Permission.COMMAND_ENDER_CHEST_EDIT.node) && canEdit);
}
}

View File

@@ -8,16 +8,39 @@ import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
/**
* Used to fire plugin {@link Event}s
*/
public abstract class EventCannon {
protected EventCannon() {
}
/**
* Fires a {@link PreSyncEvent}
*
* @param user The user to fire the event for
* @param userData The user data to fire the event with
* @return A future that will be completed when the event is fired
*/
public abstract CompletableFuture<Event> firePreSyncEvent(@NotNull OnlineUser user, @NotNull UserData userData);
/**
* Fires a {@link DataSaveEvent}
*
* @param user The user to fire the event for
* @param userData The user data to fire the event with
* @return A future that will be completed when the event is fired
*/
public abstract CompletableFuture<Event> fireDataSaveEvent(@NotNull User user, @NotNull UserData userData,
@NotNull DataSaveCause saveCause);
@NotNull DataSaveCause saveCause);
/**
* Fires a {@link SyncCompleteEvent}
*
* @param user The user to fire the event for
*/
public abstract void fireSyncCompleteEvent(@NotNull OnlineUser user);
}

View File

@@ -3,7 +3,7 @@ package net.william278.husksync.player;
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.editor.ItemEditorMenu;
import net.william278.husksync.event.EventCannon;
import net.william278.husksync.event.PreSyncEvent;
import org.jetbrains.annotations.NotNull;
@@ -225,11 +225,11 @@ public abstract class OnlineUser extends User {
public abstract boolean hasPermission(@NotNull String node);
/**
* Show the player a {@link InventoryEditorMenu} GUI
* Show the player a {@link ItemEditorMenu} GUI
*
* @param menu The {@link InventoryEditorMenu} interface to show
* @param menu The {@link ItemEditorMenu} interface to show
*/
public abstract void showMenu(@NotNull InventoryEditorMenu menu);
public abstract void showMenu(@NotNull ItemEditorMenu menu);
/**
* Get the player's current {@link UserData}

View File

@@ -0,0 +1,20 @@
package net.william278.husksync.redis;
import org.jetbrains.annotations.NotNull;
public enum RedisKeyType {
CACHE(60 * 60 * 24),
DATA_UPDATE(10),
SERVER_SWITCH(10);
public final int timeToLive;
RedisKeyType(int timeToLive) {
this.timeToLive = timeToLive;
}
@NotNull
public String getKeyPrefix() {
return RedisManager.KEY_NAMESPACE.toLowerCase() + ":" + RedisManager.clusterId.toLowerCase() + ":" + name().toLowerCase();
}
}

View File

@@ -1,20 +1,16 @@
package net.william278.husksync.redis;
import net.william278.husksync.HuskSync;
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;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.*;
import redis.clients.jedis.exceptions.JedisException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Optional;
import java.util.UUID;
@@ -25,30 +21,25 @@ import java.util.concurrent.CompletableFuture;
*/
public class RedisManager {
private static final String KEY_NAMESPACE = "husksync:";
private static String clusterId = "";
protected static final String KEY_NAMESPACE = "husksync:";
protected static String clusterId = "";
private final HuskSync plugin;
private final JedisPoolConfig jedisPoolConfig;
private final DataAdapter dataAdapter;
private final Logger logger;
private final String redisHost;
private final int redisPort;
private final String redisPassword;
private final boolean redisUseSsl;
private JedisPool jedisPool;
public RedisManager(@NotNull Settings settings, @NotNull DataAdapter dataAdapter, @NotNull Logger logger) {
clusterId = settings.getStringValue(Settings.ConfigOption.CLUSTER_ID);
this.dataAdapter = dataAdapter;
this.logger = logger;
public RedisManager(@NotNull HuskSync plugin) {
this.plugin = plugin;
clusterId = plugin.getSettings().getStringValue(Settings.ConfigOption.CLUSTER_ID);
// 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);
this.redisUseSsl = settings.getBooleanValue(Settings.ConfigOption.REDIS_USE_SSL);
this.redisHost = plugin.getSettings().getStringValue(Settings.ConfigOption.REDIS_HOST);
this.redisPort = plugin.getSettings().getIntegerValue(Settings.ConfigOption.REDIS_PORT);
this.redisPassword = plugin.getSettings().getStringValue(Settings.ConfigOption.REDIS_PASSWORD);
this.redisUseSsl = plugin.getSettings().getBooleanValue(Settings.ConfigOption.REDIS_USE_SSL);
// Configure the jedis pool
this.jedisPoolConfig = new JedisPoolConfig();
@@ -74,10 +65,50 @@ public class RedisManager {
} catch (JedisException e) {
return false;
}
CompletableFuture.runAsync(this::subscribe);
return true;
});
}
private void subscribe() {
try (final Jedis subscriber = redisPassword.isBlank() ? new Jedis(redisHost, redisPort, 0, redisUseSsl) :
new Jedis(redisHost, redisPort, DefaultJedisClientConfig.builder()
.password(redisPassword).timeoutMillis(0).ssl(redisUseSsl).build())) {
subscriber.connect();
subscriber.subscribe(new JedisPubSub() {
@Override
public void onMessage(@NotNull String channel, @NotNull String message) {
RedisMessageType.getTypeFromChannel(channel).ifPresent(messageType -> {
if (messageType == RedisMessageType.UPDATE_USER_DATA) {
final RedisMessage redisMessage = RedisMessage.fromJson(message);
plugin.getOnlineUser(redisMessage.targetUserUuid).ifPresent(user -> {
final UserData userData = plugin.getDataAdapter().fromBytes(redisMessage.data);
user.setData(userData, plugin.getSettings(), plugin.getEventCannon()).thenRun(() -> {
plugin.getLocales().getLocale("data_update_complete")
.ifPresent(user::sendActionBar);
plugin.getEventCannon().fireSyncCompleteEvent(user);
});
});
}
});
}
}, Arrays.stream(RedisMessageType.values()).map(RedisMessageType::getMessageChannel).toArray(String[]::new));
}
}
protected void sendMessage(@NotNull String channel, @NotNull String message) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.publish(channel, message);
}
}
public CompletableFuture<Void> sendUserDataUpdate(@NotNull User user, @NotNull UserData userData) {
return CompletableFuture.runAsync(() -> {
final RedisMessage redisMessage = new RedisMessage(user.uuid, plugin.getDataAdapter().toBytes(userData));
redisMessage.dispatch(this, RedisMessageType.UPDATE_USER_DATA);
});
}
/**
* Set a user's data to the Redis server
*
@@ -92,9 +123,10 @@ public class RedisManager {
// Set the user's data as a compressed byte array of the json using Snappy
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()));
plugin.getDataAdapter().toBytes(userData));
plugin.getLoggingAdapter().debug("[" + user.username + "] Set " + RedisKeyType.DATA_UPDATE.name()
+ " key to redis at: " +
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
}
});
} catch (Exception e) {
@@ -108,8 +140,9 @@ 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()));
plugin.getLoggingAdapter().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();
}
@@ -126,8 +159,9 @@ public class RedisManager {
return CompletableFuture.supplyAsync(() -> {
try (Jedis jedis = jedisPool.getResource()) {
final byte[] key = getKey(RedisKeyType.DATA_UPDATE, user.uuid);
logger.debug("[" + user.username + "] Read " + RedisKeyType.DATA_UPDATE.name() + " key from redis at: " +
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
plugin.getLoggingAdapter().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();
@@ -136,7 +170,7 @@ public class RedisManager {
jedis.del(key);
// Use Snappy to decompress the json
return Optional.of(dataAdapter.fromBytes(dataByteArray));
return Optional.of(plugin.getDataAdapter().fromBytes(dataByteArray));
} catch (Exception e) {
e.printStackTrace();
return Optional.empty();
@@ -148,8 +182,9 @@ public class RedisManager {
return CompletableFuture.supplyAsync(() -> {
try (Jedis jedis = jedisPool.getResource()) {
final byte[] key = getKey(RedisKeyType.SERVER_SWITCH, user.uuid);
logger.debug("[" + user.username + "] Read " + RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " +
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
plugin.getLoggingAdapter().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;
@@ -176,21 +211,4 @@ public class RedisManager {
return (keyType.getKeyPrefix() + ":" + uuid).getBytes(StandardCharsets.UTF_8);
}
public enum RedisKeyType {
CACHE(60 * 60 * 24),
DATA_UPDATE(10),
SERVER_SWITCH(10);
public final int timeToLive;
RedisKeyType(int timeToLive) {
this.timeToLive = timeToLive;
}
@NotNull
public String getKeyPrefix() {
return KEY_NAMESPACE.toLowerCase() + ":" + clusterId.toLowerCase() + ":" + name().toLowerCase();
}
}
}

View File

@@ -0,0 +1,33 @@
package net.william278.husksync.redis;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class RedisMessage {
public UUID targetUserUuid;
public byte[] data;
public RedisMessage(@NotNull UUID targetUserUuid, byte[] message) {
this.targetUserUuid = targetUserUuid;
this.data = message;
}
public RedisMessage() {
}
public void dispatch(@NotNull RedisManager redisManager, @NotNull RedisMessageType type) {
CompletableFuture.runAsync(() -> redisManager.sendMessage(type.getMessageChannel(),
new GsonBuilder().create().toJson(this)));
}
@NotNull
public static RedisMessage fromJson(@NotNull String json) throws JsonSyntaxException {
return new GsonBuilder().create().fromJson(json, RedisMessage.class);
}
}

View File

@@ -0,0 +1,23 @@
package net.william278.husksync.redis;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Optional;
public enum RedisMessageType {
UPDATE_USER_DATA;
@NotNull
public String getMessageChannel() {
return RedisManager.KEY_NAMESPACE.toLowerCase() + ":" + RedisManager.clusterId.toLowerCase()
+ ":" + name().toLowerCase();
}
public static Optional<RedisMessageType> getTypeFromChannel(@NotNull String messageChannel) {
return Arrays.stream(values()).filter(messageType -> messageType.getMessageChannel()
.equalsIgnoreCase(messageChannel)).findFirst();
}
}