diff --git a/api/src/main/java/net/william278/husksync/api/HuskSyncAPI.java b/api/src/main/java/net/william278/husksync/api/HuskSyncAPI.java index 83f24d5e..468cddeb 100644 --- a/api/src/main/java/net/william278/husksync/api/HuskSyncAPI.java +++ b/api/src/main/java/net/william278/husksync/api/HuskSyncAPI.java @@ -10,32 +10,27 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.potion.PotionEffect; import org.jetbrains.annotations.NotNull; -import java.util.List; import java.util.Optional; -import java.util.UUID; import java.util.concurrent.CompletableFuture; /** - * The HuskSync API for the Bukkit platform, providing methods to access and modify player {@link UserData} held by {@link User}s. + * The HuskSync API implementation for the Bukkit platform, providing methods to access and modify player {@link UserData} held by {@link User}s. *

* Retrieve an instance of the API class via {@link #getInstance()}. */ @SuppressWarnings("unused") -public class HuskSyncAPI { +public class HuskSyncAPI extends BaseHuskSyncAPI { /** - * (Internal use only) - Instance of the API class. + * (Internal use only) - Instance of the API class */ private static final HuskSyncAPI INSTANCE = new HuskSyncAPI(); - /** - * (Internal use only) - Instance of the implementing plugin. - */ - private static final BukkitHuskSync PLUGIN = BukkitHuskSync.getInstance(); /** - * (Internal use only) - Constructor. + * (Internal use only) - Constructor, instantiating the API */ private HuskSyncAPI() { + super(BukkitHuskSync.getInstance()); } /** @@ -51,7 +46,8 @@ public class HuskSyncAPI { * Returns a {@link User} instance for the given bukkit {@link Player}. * * @param player the bukkit player to get the {@link User} instance for - * @return the {@link User} instance for the given bukkit player + * @return the {@link User} instance for the given bukkit {@link Player} + * @since 2.0 */ @NotNull public OnlineUser getUser(@NotNull Player player) { @@ -59,71 +55,49 @@ public class HuskSyncAPI { } /** - * Returns a {@link User} by the given player's account {@link UUID}, if they exist. - * - * @param uuid the unique id of the player to get the {@link User} instance for - * @return future returning the {@link User} instance for the given player's unique id if they exist, otherwise an empty {@link Optional} - * @apiNote The player does not have to be online - */ - public CompletableFuture> getUser(@NotNull UUID uuid) { - return PLUGIN.getDatabase().getUser(uuid); - } - - /** - * Returns a {@link User} by the given player's username (case-insensitive), if they exist. + * Set the inventory in the database of the given {@link User} to the given {@link ItemStack} contents * - * @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. + * @param user the {@link User} to set the inventory of + * @param inventoryContents the {@link ItemStack} contents to set the inventory to + * @return future returning void when complete + * @since 2.0 */ - public CompletableFuture> getUser(@NotNull String username) { - return PLUGIN.getDatabase().getUserByName(username); + public CompletableFuture setInventoryData(@NotNull User user, @NotNull ItemStack[] inventoryContents) { + return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData -> + userData.ifPresent(data -> serializeItemStackArray(inventoryContents) + .thenAccept(serializedInventory -> { + data.getInventoryData().serializedItems = serializedInventory; + setUserData(user, data).join(); + })))); } /** - * Returns a {@link User}'s current {@link UserData} + * Set the inventory in the database of the given {@link User} to the given {@link BukkitInventoryMap} contents * - * @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.

- * If the user happens to be online on another server on the network, - * then the {@link UserData} returned here may not be reflective of their actual current UserData. + * @param user the {@link User} to set the inventory of + * @param inventoryMap the {@link BukkitInventoryMap} contents to set the inventory to + * @return future returning void when complete + * @since 2.0 */ - public CompletableFuture> 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); - } - }); + public CompletableFuture setInventoryData(@NotNull User user, @NotNull BukkitInventoryMap inventoryMap) { + return setInventoryData(user, inventoryMap.getContents()); } /** - * Returns the saved {@link VersionedUserData} records for the given {@link User} + * Set the Ender Chest in the database of the given {@link User} to the given {@link ItemStack} contents * - * @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 + * @param user the {@link User} to set the Ender Chest of + * @param enderChestContents the {@link ItemStack} contents to set the Ender Chest to + * @return future returning void when complete + * @since 2.0 */ - public CompletableFuture> getSavedUserData(@NotNull User user) { - return CompletableFuture.supplyAsync(() -> PLUGIN.getDatabase().getUserData(user).join()); - } - - /** - * Returns the JSON string representation of the given {@link UserData} - * - * @param userData the {@link UserData} to get the JSON string representation of - * @param prettyPrint whether to pretty print the JSON string - * @return the JSON string representation of the given {@link UserData} - */ - @NotNull - public String getUserDataJson(@NotNull UserData userData, boolean prettyPrint) { - return PLUGIN.getDataAdapter().toJson(userData, prettyPrint); + public CompletableFuture setEnderChestData(@NotNull User user, @NotNull ItemStack[] enderChestContents) { + return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData -> + userData.ifPresent(data -> serializeItemStackArray(enderChestContents) + .thenAccept(serializedInventory -> { + data.getEnderChestData().serializedItems = serializedInventory; + setUserData(user, data).join(); + })))); } /** @@ -132,10 +106,11 @@ public class HuskSyncAPI { * @param user the {@link User} to get the {@link BukkitInventoryMap} for * @return future returning the {@link BukkitInventoryMap} for the given {@link User} if they exist, * otherwise an empty {@link Optional} + * @since 2.0 */ public CompletableFuture> getPlayerInventory(@NotNull User user) { return CompletableFuture.supplyAsync(() -> getUserData(user).join() - .map(userData -> BukkitSerializer.deserializeInventory(userData + .map(userData -> deserializeInventory(userData .getInventoryData().serializedItems).join())); } @@ -145,10 +120,11 @@ public class HuskSyncAPI { * @param user the {@link User} to get the Ender Chest contents of * @return future returning the {@link ItemStack} array of Ender Chest items for the user if they exist, * otherwise an empty {@link Optional} + * @since 2.0 */ public CompletableFuture> getPlayerEnderChest(@NotNull User user) { return CompletableFuture.supplyAsync(() -> getUserData(user).join() - .map(userData -> BukkitSerializer.deserializeItemStackArray(userData + .map(userData -> deserializeItemStackArray(userData .getEnderChestData().serializedItems).join())); } @@ -157,25 +133,71 @@ public class HuskSyncAPI { * * @param serializedItemStackArray The Base-64 encoded inventory array string. * @return The deserialized {@link ItemStack} array. - * @throws DataDeserializationException If an error occurs during deserialization. + * @throws DataSerializationException If an error occurs during deserialization. + * @since 2.0 */ public CompletableFuture deserializeItemStackArray(@NotNull String serializedItemStackArray) - throws DataDeserializationException { + throws DataSerializationException { return CompletableFuture.supplyAsync(() -> BukkitSerializer .deserializeItemStackArray(serializedItemStackArray).join()); } + /** + * Deserialize a serialized {@link ItemStack} array of player inventory contents into a {@link BukkitInventoryMap} + * + * @param serializedInventory The serialized {@link ItemStack} array of player inventory contents. + * @return A {@link BukkitInventoryMap} of the deserialized {@link ItemStack} contents array + * @throws DataSerializationException If an error occurs during deserialization. + * @since 2.0 + */ + public CompletableFuture deserializeInventory(@NotNull String serializedInventory) + throws DataSerializationException { + return CompletableFuture.supplyAsync(() -> BukkitSerializer + .deserializeInventory(serializedInventory).join()); + } + + /** + * Serialize an {@link ItemStack} array into a Base-64 encoded string. + * + * @param itemStacks The {@link ItemStack} array to serialize. + * @return The serialized Base-64 encoded string. + * @throws DataSerializationException If an error occurs during serialization. + * @see #deserializeItemStackArray(String) + * @see ItemData + * @since 2.0 + */ + public CompletableFuture serializeItemStackArray(@NotNull ItemStack[] itemStacks) + throws DataSerializationException { + return CompletableFuture.supplyAsync(() -> BukkitSerializer.serializeItemStackArray(itemStacks).join()); + } + /** * Deserialize a Base-64 encoded potion effect array string into a {@link PotionEffect} array. * * @param serializedPotionEffectArray The Base-64 encoded potion effect array string. * @return The deserialized {@link PotionEffect} array. - * @throws DataDeserializationException If an error occurs during deserialization. + * @throws DataSerializationException If an error occurs during deserialization. + * @since 2.0 */ public CompletableFuture deserializePotionEffectArray(@NotNull String serializedPotionEffectArray) - throws DataDeserializationException { + throws DataSerializationException { return CompletableFuture.supplyAsync(() -> BukkitSerializer - .deserializePotionEffects(serializedPotionEffectArray).join()); + .deserializePotionEffectArray(serializedPotionEffectArray).join()); + } + + /** + * Serialize a {@link PotionEffect} array into a Base-64 encoded string. + * + * @param potionEffects The {@link PotionEffect} array to serialize. + * @return The serialized Base-64 encoded string. + * @throws DataSerializationException If an error occurs during serialization. + * @see #deserializePotionEffectArray(String) + * @see PotionEffectData + * @since 2.0 + */ + public CompletableFuture serializePotionEffectArray(@NotNull PotionEffect[] potionEffects) + throws DataSerializationException { + return CompletableFuture.supplyAsync(() -> BukkitSerializer.serializePotionEffectArray(potionEffects).join()); } } diff --git a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java index 7c5b86ab..e727d59f 100644 --- a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java +++ b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java @@ -124,7 +124,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { }).thenApply(succeeded -> { // Prepare data editor if (succeeded) { - dataEditor = new DataEditor(); + dataEditor = new DataEditor(locales); } return succeeded; }).thenApply(succeeded -> { @@ -149,7 +149,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { }).thenApply(succeeded -> { // Establish connection to the Redis server if (succeeded) { - this.redisManager = new RedisManager(settings, dataAdapter, logger); + this.redisManager = new RedisManager(this); getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the Redis server..."); return this.redisManager.initialize().thenApply(initialized -> { if (!initialized) { diff --git a/bukkit/src/main/java/net/william278/husksync/command/BukkitCommandType.java b/bukkit/src/main/java/net/william278/husksync/command/BukkitCommandType.java index b8bd22b1..1162c50d 100644 --- a/bukkit/src/main/java/net/william278/husksync/command/BukkitCommandType.java +++ b/bukkit/src/main/java/net/william278/husksync/command/BukkitCommandType.java @@ -7,9 +7,11 @@ import org.jetbrains.annotations.NotNull; * Commands available on the Bukkit HuskSync implementation */ public enum BukkitCommandType { + HUSKSYNC_COMMAND(new HuskSyncCommand(BukkitHuskSync.getInstance())), - HUSKSYNC_INVSEE(new InvseeCommand(BukkitHuskSync.getInstance())), - HUSKSYNC_ECHEST(new EchestCommand(BukkitHuskSync.getInstance())); + INVENTORY_COMMAND(new InventoryCommand(BukkitHuskSync.getInstance())), + ENDER_CHEST_COMMAND(new EnderChestCommand(BukkitHuskSync.getInstance())), + USERDATA_COMMAND(new UserDataCommand(BukkitHuskSync.getInstance())); public final CommandBase commandBase; diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java index 5ae9f8d4..28032ab5 100644 --- a/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java +++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java @@ -22,8 +22,8 @@ public class BukkitSerializer { * @param inventoryContents The contents of the inventory * @return The serialized inventory contents */ - public static CompletableFuture serializeItemStackArray(ItemStack[] inventoryContents) - throws DataDeserializationException { + public static CompletableFuture serializeItemStackArray(@NotNull ItemStack[] inventoryContents) + throws DataSerializationException { return CompletableFuture.supplyAsync(() -> { // Return an empty string if there is no inventory item data to serialize if (inventoryContents.length == 0) { @@ -45,7 +45,7 @@ public class BukkitSerializer { // Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion return Base64Coder.encodeLines(byteOutputStream.toByteArray()); } catch (IOException e) { - throw new DataDeserializationException("Failed to serialize item stack data", e); + throw new DataSerializationException("Failed to serialize item stack data", e); } }); } @@ -53,25 +53,25 @@ public class BukkitSerializer { /** * Returns a {@link BukkitInventoryMap} from a serialized array of ItemStacks representing the contents of a player's inventory. * - * @param serializedPlayerInventory The serialized {@link ItemStack[]} inventory array + * @param serializedPlayerInventory The serialized {@link ItemStack} inventory array * @return The deserialized ItemStacks, mapped for convenience as a {@link BukkitInventoryMap} - * @throws DataDeserializationException If the serialized item stack array could not be deserialized + * @throws DataSerializationException If the serialized item stack array could not be deserialized */ public static CompletableFuture deserializeInventory(@NotNull String serializedPlayerInventory) - throws DataDeserializationException { + throws DataSerializationException { return CompletableFuture.supplyAsync(() -> new BukkitInventoryMap(deserializeItemStackArray(serializedPlayerInventory).join())); } /** * Returns an array of ItemStacks from serialized inventory data. * - * @param serializeItemStackArray The serialized {@link ItemStack[]} array + * @param serializeItemStackArray The serialized {@link ItemStack} array * @return The deserialized array of {@link ItemStack}s - * @throws DataDeserializationException If the serialized item stack array could not be deserialized + * @throws DataSerializationException If the serialized item stack array could not be deserialized * @implNote Empty slots will be represented by {@code null} */ - public static CompletableFuture deserializeItemStackArray(String serializeItemStackArray) - throws DataDeserializationException { + public static CompletableFuture deserializeItemStackArray(@NotNull String serializeItemStackArray) + throws DataSerializationException { return CompletableFuture.supplyAsync(() -> { // Return empty array if there is no inventory data (set the player as having an empty inventory) if (serializeItemStackArray.isEmpty()) { @@ -95,7 +95,7 @@ public class BukkitSerializer { return inventoryContents; } } catch (IOException | ClassNotFoundException e) { - throw new DataDeserializationException("Failed to deserialize item stack data", e); + throw new DataSerializationException("Failed to deserialize item stack data", e); } }); } @@ -107,7 +107,7 @@ public class BukkitSerializer { * @return The serialized {@link ItemStack} */ @Nullable - private static Map serializeItemStack(ItemStack item) { + private static Map serializeItemStack(@Nullable ItemStack item) { return item != null ? item.serialize() : null; } @@ -119,7 +119,7 @@ public class BukkitSerializer { */ @SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning @Nullable - private static ItemStack deserializeItemStack(Object serializedItemStack) { + private static ItemStack deserializeItemStack(@Nullable Object serializedItemStack) { return serializedItemStack != null ? ItemStack.deserialize((Map) serializedItemStack) : null; } @@ -129,7 +129,7 @@ public class BukkitSerializer { * @param potionEffects The potion effect array * @return The serialized potion effects */ - public static CompletableFuture serializePotionEffects(PotionEffect[] potionEffects) throws DataDeserializationException { + public static CompletableFuture serializePotionEffectArray(@NotNull PotionEffect[] potionEffects) throws DataSerializationException { return CompletableFuture.supplyAsync(() -> { // Return an empty string if there are no effects to serialize if (potionEffects.length == 0) { @@ -151,7 +151,7 @@ public class BukkitSerializer { // Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion return Base64Coder.encodeLines(byteOutputStream.toByteArray()); } catch (IOException e) { - throw new DataDeserializationException("Failed to serialize potion effect data", e); + throw new DataSerializationException("Failed to serialize potion effect data", e); } }); } @@ -159,10 +159,10 @@ public class BukkitSerializer { /** * Returns an array of ItemStacks from serialized potion effect data * - * @param potionEffectData The serialized {@link PotionEffect[]} array + * @param potionEffectData The serialized {@link PotionEffect} array * @return The {@link PotionEffect}s */ - public static CompletableFuture deserializePotionEffects(String potionEffectData) throws DataDeserializationException { + public static CompletableFuture deserializePotionEffectArray(@NotNull String potionEffectData) throws DataSerializationException { return CompletableFuture.supplyAsync(() -> { // Return empty array if there is no potion effect data (don't apply any effects to the player) if (potionEffectData.isEmpty()) { @@ -186,7 +186,7 @@ public class BukkitSerializer { return potionEffects; } } catch (IOException | ClassNotFoundException e) { - throw new DataDeserializationException("Failed to deserialize potion effects", e); + throw new DataSerializationException("Failed to deserialize potion effects", e); } }); } @@ -198,7 +198,7 @@ public class BukkitSerializer { * @return The serialized {@link ItemStack} */ @Nullable - private static Map serializePotionEffect(PotionEffect potionEffect) { + private static Map serializePotionEffect(@Nullable PotionEffect potionEffect) { return potionEffect != null ? potionEffect.serialize() : null; } @@ -210,7 +210,7 @@ public class BukkitSerializer { */ @SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning @Nullable - private static PotionEffect deserializePotionEffect(Object serializedPotionEffect) { + private static PotionEffect deserializePotionEffect(@Nullable Object serializedPotionEffect) { return serializedPotionEffect != null ? new PotionEffect((Map) serializedPotionEffect) : null; } diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitDataSavePlayerEvent.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitDataSaveEvent.java similarity index 83% rename from bukkit/src/main/java/net/william278/husksync/event/BukkitDataSavePlayerEvent.java rename to bukkit/src/main/java/net/william278/husksync/event/BukkitDataSaveEvent.java index 3eb12c51..974613d2 100644 --- a/bukkit/src/main/java/net/william278/husksync/event/BukkitDataSavePlayerEvent.java +++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitDataSaveEvent.java @@ -7,15 +7,15 @@ import org.bukkit.event.Cancellable; import org.bukkit.event.HandlerList; import org.jetbrains.annotations.NotNull; -public class BukkitDataSavePlayerEvent extends BukkitEvent implements DataSaveEvent, Cancellable { +public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, Cancellable { private static final HandlerList HANDLER_LIST = new HandlerList(); private boolean cancelled = false; private UserData userData; private final User user; private final DataSaveCause saveCause; - protected BukkitDataSavePlayerEvent(@NotNull User user, @NotNull UserData userData, - @NotNull DataSaveCause saveCause) { + protected BukkitDataSaveEvent(@NotNull User user, @NotNull UserData userData, + @NotNull DataSaveCause saveCause) { this.user = user; this.userData = userData; this.saveCause = saveCause; diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitEvent.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitEvent.java index 1160449c..a4be0305 100644 --- a/bukkit/src/main/java/net/william278/husksync/event/BukkitEvent.java +++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitEvent.java @@ -6,19 +6,28 @@ import net.william278.husksync.player.OnlineUser; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.Event; +import org.bukkit.event.player.PlayerEvent; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; public abstract class BukkitEvent extends Event implements net.william278.husksync.event.Event { + protected BukkitEvent() { + } + @Override public CompletableFuture fire() { final CompletableFuture eventFireFuture = new CompletableFuture<>(); - Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> { - Bukkit.getServer().getPluginManager().callEvent(this); + // Don't fire events while the server is shutting down + if (!BukkitHuskSync.getInstance().isEnabled()) { eventFireFuture.complete(this); - }); + } else { + Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> { + Bukkit.getServer().getPluginManager().callEvent(this); + eventFireFuture.complete(this); + }); + } return eventFireFuture; } diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitEventCannon.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitEventCannon.java index eacdc30e..a210c868 100644 --- a/bukkit/src/main/java/net/william278/husksync/event/BukkitEventCannon.java +++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitEventCannon.java @@ -22,12 +22,12 @@ public class BukkitEventCannon extends EventCannon { @Override public CompletableFuture fireDataSaveEvent(@NotNull User user, @NotNull UserData userData, @NotNull DataSaveCause saveCause) { - return new BukkitDataSavePlayerEvent(user, userData, saveCause).fire(); + return new BukkitDataSaveEvent(user, userData, saveCause).fire(); } @Override public void fireSyncCompleteEvent(@NotNull OnlineUser user) { - new BukkitSyncCompletePlayerEvent(((BukkitPlayer) user).getPlayer()).fire(); + new BukkitSyncCompleteEvent(((BukkitPlayer) user).getPlayer()).fire(); } } diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitPlayerEvent.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitPlayerEvent.java index f889939f..045bff6e 100644 --- a/bukkit/src/main/java/net/william278/husksync/event/BukkitPlayerEvent.java +++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitPlayerEvent.java @@ -9,11 +9,12 @@ import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; -public abstract class BukkitPlayerEvent extends org.bukkit.event.player.PlayerEvent implements PlayerEvent { +public abstract class BukkitPlayerEvent extends BukkitEvent implements PlayerEvent { + protected final Player player; - public BukkitPlayerEvent(@NotNull Player who) { - super(who); + protected BukkitPlayerEvent(@NotNull Player player) { + this.player = player; } @Override diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitPreSyncEvent.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitPreSyncEvent.java index 4289fc0d..b54fed9b 100644 --- a/bukkit/src/main/java/net/william278/husksync/event/BukkitPreSyncEvent.java +++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitPreSyncEvent.java @@ -28,11 +28,6 @@ public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEven this.cancelled = cancelled; } - @Override - public OnlineUser getUser() { - return BukkitPlayer.adapt(player); - } - @Override public @NotNull UserData getUserData() { return userData; diff --git a/bukkit/src/main/java/net/william278/husksync/event/BukkitSyncCompletePlayerEvent.java b/bukkit/src/main/java/net/william278/husksync/event/BukkitSyncCompleteEvent.java similarity index 63% rename from bukkit/src/main/java/net/william278/husksync/event/BukkitSyncCompletePlayerEvent.java rename to bukkit/src/main/java/net/william278/husksync/event/BukkitSyncCompleteEvent.java index 7e4181c5..c15bd0f1 100644 --- a/bukkit/src/main/java/net/william278/husksync/event/BukkitSyncCompletePlayerEvent.java +++ b/bukkit/src/main/java/net/william278/husksync/event/BukkitSyncCompleteEvent.java @@ -6,18 +6,13 @@ import org.bukkit.entity.Player; import org.bukkit.event.HandlerList; import org.jetbrains.annotations.NotNull; -public class BukkitSyncCompletePlayerEvent extends BukkitPlayerEvent implements SyncCompleteEvent { +public class BukkitSyncCompleteEvent extends BukkitPlayerEvent implements SyncCompleteEvent { private static final HandlerList HANDLER_LIST = new HandlerList(); - protected BukkitSyncCompletePlayerEvent(@NotNull Player player) { + protected BukkitSyncCompleteEvent(@NotNull Player player) { super(player); } - @Override - public OnlineUser getUser() { - return BukkitPlayer.adapt(player); - } - @NotNull @Override public HandlerList getHandlers() { diff --git a/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java b/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java index e691df8a..9e8c73b9 100644 --- a/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java +++ b/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java @@ -2,7 +2,7 @@ package net.william278.husksync.listener; import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.data.BukkitSerializer; -import net.william278.husksync.data.DataDeserializationException; +import net.william278.husksync.data.DataSerializationException; import net.william278.husksync.data.ItemData; import net.william278.husksync.player.BukkitPlayer; import net.william278.husksync.player.OnlineUser; @@ -57,7 +57,7 @@ public class BukkitEventListener extends EventListener implements Listener { try { BukkitSerializer.serializeItemStackArray(event.getInventory().getContents()).thenAccept( serializedInventory -> super.handleMenuClose(user, new ItemData(serializedInventory))); - } catch (DataDeserializationException e) { + } catch (DataSerializationException e) { huskSync.getLoggingAdapter().log(Level.SEVERE, "Failed to serialize inventory data during menu close", e); } diff --git a/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java b/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java index e6d2d80c..ab76e47b 100644 --- a/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java +++ b/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java @@ -5,7 +5,7 @@ import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.chat.BaseComponent; import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.data.*; -import net.william278.husksync.editor.InventoryEditorMenu; +import net.william278.husksync.editor.ItemEditorMenu; import org.apache.commons.lang.ArrayUtils; import org.bukkit.*; import org.bukkit.advancement.Advancement; @@ -158,13 +158,13 @@ public class BukkitPlayer extends OnlineUser { @Override public CompletableFuture getPotionEffects() { - return BukkitSerializer.serializePotionEffects(player.getActivePotionEffects() + return BukkitSerializer.serializePotionEffectArray(player.getActivePotionEffects() .toArray(new PotionEffect[0])).thenApply(PotionEffectData::new); } @Override public CompletableFuture setPotionEffects(@NotNull PotionEffectData potionEffectData) { - return BukkitSerializer.deserializePotionEffects(potionEffectData.serializedPotionEffects) + return BukkitSerializer.deserializePotionEffectArray(potionEffectData.serializedPotionEffects) .thenApplyAsync(effects -> { final CompletableFuture potionEffectsSetFuture = new CompletableFuture<>(); Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> { @@ -328,8 +328,8 @@ public class BukkitPlayer extends OnlineUser { public CompletableFuture setStatistics(@NotNull StatisticsData statisticsData) { return CompletableFuture.runAsync(() -> { // Set untyped statistics - for (String statistic : statisticsData.untypedStatistic.keySet()) { - player.setStatistic(Statistic.valueOf(statistic), statisticsData.untypedStatistic.get(statistic)); + for (String statistic : statisticsData.untypedStatistics.keySet()) { + player.setStatistic(Statistic.valueOf(statistic), statisticsData.untypedStatistics.get(statistic)); } // Set block statistics @@ -440,7 +440,7 @@ public class BukkitPlayer extends OnlineUser { } @Override - public void showMenu(@NotNull InventoryEditorMenu menu) { + public void showMenu(@NotNull ItemEditorMenu menu) { BukkitSerializer.deserializeItemStackArray(menu.itemData.serializedItems).thenAccept(inventoryContents -> { final Inventory inventory = Bukkit.createInventory(player, menu.slotCount, BaseComponent.toLegacyText(menu.menuTitle.toComponent())); diff --git a/bukkit/src/main/resources/plugin.yml b/bukkit/src/main/resources/plugin.yml index 651b14b8..7dedb160 100644 --- a/bukkit/src/main/resources/plugin.yml +++ b/bukkit/src/main/resources/plugin.yml @@ -10,8 +10,14 @@ libraries: - 'mysql:mysql-connector-java:8.0.29' commands: husksync: - usage: '/husksync ' - invsee: - usage: '/invsee ' - echest: - usage: '/echest ' \ No newline at end of file + usage: '/husksync ' + description: 'Manage the HuskSync plugin' + inventory: + usage: '/inventory [version_uuid]' + description: 'View & edit a player''s inventory' + enderchest: + usage: '/enderchest [version_uuid]' + description: 'View & edit a player''s Ender Chest' + userdata: + usage: '/userdata [version_uuid]' + description: 'View, manage & restore player userdata' \ No newline at end of file diff --git a/common/src/main/java/net/william278/husksync/api/BaseHuskSyncAPI.java b/common/src/main/java/net/william278/husksync/api/BaseHuskSyncAPI.java new file mode 100644 index 00000000..164da713 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/api/BaseHuskSyncAPI.java @@ -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. + *

+ * This class should not be used directly, but rather through platform-specific extending API classes. + */ +@SuppressWarnings("unused") +public abstract class BaseHuskSyncAPI { + + /** + * (Internal use only) - 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> 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> 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. + *

+ * Because of this, if the user is online on another server on the network, + * then the {@link UserData} returned by this method will not necessarily reflective of + * their current state + * @since 2.0 + */ + public final CompletableFuture> 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}. + *

+ * 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 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 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> 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); + } + +} diff --git a/common/src/main/java/net/william278/husksync/command/EchestCommand.java b/common/src/main/java/net/william278/husksync/command/EchestCommand.java deleted file mode 100644 index 7484af58..00000000 --- a/common/src/main/java/net/william278/husksync/command/EchestCommand.java +++ /dev/null @@ -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 ") - .ifPresent(player::sendMessage); - return; - } - plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAcceptAsync(optionalUser -> - optionalUser.ifPresentOrElse(user -> { - List userData = plugin.getDatabase().getUserData(user).join(); - Optional 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 [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))); - } - -} diff --git a/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java b/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java new file mode 100644 index 00000000..94e6f5f2 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java @@ -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 ") + .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 [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(); + }); + } + +} diff --git a/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java b/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java index 73c2db55..416f309b 100644 --- a/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java +++ b/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java @@ -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 ").ifPresent(player::sendMessage); + default -> plugin.getLocales().getLocale("error_invalid_syntax", + "/husksync ") + .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 \""); + default -> plugin.getLoggingAdapter().log(Level.INFO, + "Invalid syntax. Console usage: \"husksync \""); } } diff --git a/common/src/main/java/net/william278/husksync/command/InventoryCommand.java b/common/src/main/java/net/william278/husksync/command/InventoryCommand.java new file mode 100644 index 00000000..b248ed1e --- /dev/null +++ b/common/src/main/java/net/william278/husksync/command/InventoryCommand.java @@ -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 ") + .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 [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(); + }); + } + +} diff --git a/common/src/main/java/net/william278/husksync/command/InvseeCommand.java b/common/src/main/java/net/william278/husksync/command/InvseeCommand.java deleted file mode 100644 index 3f667ed6..00000000 --- a/common/src/main/java/net/william278/husksync/command/InvseeCommand.java +++ /dev/null @@ -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 ") - .ifPresent(player::sendMessage); - return; - } - plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAcceptAsync(optionalUser -> - optionalUser.ifPresentOrElse(user -> { - List userData = plugin.getDatabase().getUserData(user).join(); - Optional 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 [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))); - } - -} diff --git a/common/src/main/java/net/william278/husksync/command/Permission.java b/common/src/main/java/net/william278/husksync/command/Permission.java index 3fd07b72..f8f9cb89 100644 --- a/common/src/main/java/net/william278/husksync/command/Permission.java +++ b/common/src/main/java/net/william278/husksync/command/Permission.java @@ -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), + COMMAND_HUSKSYNC_SAVE_ALL("husksync.command.husksync.saveall", DefaultAccess.OPERATORS), //todo + + /* + * /inventory command permissions + */ + /** - * Lets the user view a player's backup data {@code /husksync backup (player)} + * Lets the user use the {@code /inventory (player)} command and view offline players' inventories */ - COMMAND_HUSKSYNC_BACKUPS("husksync.command.husksync.backups", DefaultAccess.OPERATORS), + COMMAND_INVENTORY("husksync.command.inventory", DefaultAccess.OPERATORS), /** - * Lets the user restore a player's backup data {@code /husksync backup (player) restore (backup_uuid)} + * Lets the user edit the contents of offline players' inventories */ - COMMAND_HUSKSYNC_BACKUPS_RESTORE("husksync.command.husksync.backups.restore", DefaultAccess.OPERATORS), + COMMAND_INVENTORY_EDIT("husksync.command.inventory.edit", DefaultAccess.OPERATORS), /* - * /invsee command permissions + * /enderchest command permissions */ /** - * Lets the user use the {@code /invsee (player)} command and view offline players' inventories + * Lets the user use the {@code /enderchest (player)} command and view offline players' ender chests */ - COMMAND_VIEW_INVENTORIES("husksync.command.invsee", DefaultAccess.OPERATORS), + COMMAND_ENDER_CHEST("husksync.command.enderchest", DefaultAccess.OPERATORS), /** - * Lets the user edit the contents of offline players' inventories + * Lets the user edit the contents of offline players' ender chests */ - COMMAND_EDIT_INVENTORIES("husksync.command.invsee.edit", DefaultAccess.OPERATORS), + COMMAND_ENDER_CHEST_EDIT("husksync.command.enderchest.edit", DefaultAccess.OPERATORS), /* - * /echest command permissions + * /userdata command permissions */ /** - * Lets the user use the {@code /echest (player)} command and view offline players' ender chests + * Lets the user view user data {@code /userdata view/list (player) (version_uuid)} */ - COMMAND_VIEW_ENDER_CHESTS("husksync.command.echest", DefaultAccess.OPERATORS), + COMMAND_USER_DATA("husksync.command.userdata", DefaultAccess.OPERATORS), /** - * Lets the user edit the contents of offline players' ender chests + * Lets the user restore and delete user data {@code /userdata restore/delete (player) (version_uuid)} */ - COMMAND_EDIT_ENDER_CHESTS("husksync.command.echest.edit", DefaultAccess.OPERATORS); + COMMAND_USER_DATA_MANAGE("husksync.command.userdata.manage", DefaultAccess.OPERATORS); + public final String node; public final DefaultAccess defaultAccess; diff --git a/common/src/main/java/net/william278/husksync/command/UserDataCommand.java b/common/src/main/java/net/william278/husksync/command/UserDataCommand.java new file mode 100644 index 00000000..557d49d4 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/command/UserDataCommand.java @@ -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 [version_uuid]") + .ifPresent(player::sendMessage); + return; + } + + switch (args[0].toLowerCase()) { + case "view" -> { + if (args.length < 2) { + plugin.getLocales().getLocale("error_invalid_syntax", + "/userdata view [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 [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 ") + .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 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()); + } +} diff --git a/common/src/main/java/net/william278/husksync/data/DataDeserializationException.java b/common/src/main/java/net/william278/husksync/data/DataDeserializationException.java deleted file mode 100644 index a529ef1e..00000000 --- a/common/src/main/java/net/william278/husksync/data/DataDeserializationException.java +++ /dev/null @@ -1,13 +0,0 @@ -package net.william278.husksync.data; - -/** - * Indicates an error occurred during base-64 serialization and deserialization of data. - *

- * 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); - } - -} diff --git a/common/src/main/java/net/william278/husksync/data/DataSaveCause.java b/common/src/main/java/net/william278/husksync/data/DataSaveCause.java index c833fc4a..1a760246 100644 --- a/common/src/main/java/net/william278/husksync/data/DataSaveCause.java +++ b/common/src/main/java/net/william278/husksync/data/DataSaveCause.java @@ -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. + *

+ * 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. *

* 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()) { diff --git a/common/src/main/java/net/william278/husksync/data/DataSerializationException.java b/common/src/main/java/net/william278/husksync/data/DataSerializationException.java new file mode 100644 index 00000000..204fea8d --- /dev/null +++ b/common/src/main/java/net/william278/husksync/data/DataSerializationException.java @@ -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. + *

+ * 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); + } + +} diff --git a/common/src/main/java/net/william278/husksync/data/StatisticsData.java b/common/src/main/java/net/william278/husksync/data/StatisticsData.java index c0d17d30..e477b123 100644 --- a/common/src/main/java/net/william278/husksync/data/StatisticsData.java +++ b/common/src/main/java/net/william278/husksync/data/StatisticsData.java @@ -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 untypedStatistic; + public Map 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> entityStatistics; - public StatisticsData(@NotNull Map untypedStatistic, + public StatisticsData(@NotNull Map untypedStatistics, @NotNull Map> blockStatistics, @NotNull Map> itemStatistics, @NotNull Map> entityStatistics) { - this.untypedStatistic = untypedStatistic; + this.untypedStatistics = untypedStatistics; this.blockStatistics = blockStatistics; this.itemStatistics = itemStatistics; this.entityStatistics = entityStatistics; diff --git a/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java b/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java index 8f9c34cf..7034a5dc 100644 --- a/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java +++ b/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java @@ -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); diff --git a/common/src/main/java/net/william278/husksync/editor/DataEditor.java b/common/src/main/java/net/william278/husksync/editor/DataEditor.java index fe7287f6..55149e9d 100644 --- a/common/src/main/java/net/william278/husksync/editor/DataEditor.java +++ b/common/src/main/java/net/william278/husksync/editor/DataEditor.java @@ -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 openInventoryMenus; + protected final HashMap 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 openInventoryMenu(@NotNull OnlineUser user, - @NotNull InventoryEditorMenu inventoryEditorMenu) { - this.openInventoryMenus.put(user.uuid, inventoryEditorMenu); - return inventoryEditorMenu.showInventory(user); + public CompletableFuture 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) { + 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 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 */ diff --git a/common/src/main/java/net/william278/husksync/editor/InventoryEditorMenu.java b/common/src/main/java/net/william278/husksync/editor/InventoryEditorMenu.java deleted file mode 100644 index 1e097643..00000000 --- a/common/src/main/java/net/william278/husksync/editor/InventoryEditorMenu.java +++ /dev/null @@ -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 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 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)); - } - -} diff --git a/common/src/main/java/net/william278/husksync/editor/ItemEditorMenu.java b/common/src/main/java/net/william278/husksync/editor/ItemEditorMenu.java new file mode 100644 index 00000000..2b862a42 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/editor/ItemEditorMenu.java @@ -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 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 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); + } + +} diff --git a/common/src/main/java/net/william278/husksync/event/EventCannon.java b/common/src/main/java/net/william278/husksync/event/EventCannon.java index dd6c7e7e..4329fd6b 100644 --- a/common/src/main/java/net/william278/husksync/event/EventCannon.java +++ b/common/src/main/java/net/william278/husksync/event/EventCannon.java @@ -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 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 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); } diff --git a/common/src/main/java/net/william278/husksync/player/OnlineUser.java b/common/src/main/java/net/william278/husksync/player/OnlineUser.java index 88c860e6..78b5ef30 100644 --- a/common/src/main/java/net/william278/husksync/player/OnlineUser.java +++ b/common/src/main/java/net/william278/husksync/player/OnlineUser.java @@ -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} diff --git a/common/src/main/java/net/william278/husksync/redis/RedisKeyType.java b/common/src/main/java/net/william278/husksync/redis/RedisKeyType.java new file mode 100644 index 00000000..1b38412a --- /dev/null +++ b/common/src/main/java/net/william278/husksync/redis/RedisKeyType.java @@ -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(); + } +} diff --git a/common/src/main/java/net/william278/husksync/redis/RedisManager.java b/common/src/main/java/net/william278/husksync/redis/RedisManager.java index 7e1939cc..867d7c80 100644 --- a/common/src/main/java/net/william278/husksync/redis/RedisManager.java +++ b/common/src/main/java/net/william278/husksync/redis/RedisManager.java @@ -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 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(); - } - } - } diff --git a/common/src/main/java/net/william278/husksync/redis/RedisMessage.java b/common/src/main/java/net/william278/husksync/redis/RedisMessage.java new file mode 100644 index 00000000..6fd8d993 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/redis/RedisMessage.java @@ -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); + } + +} \ No newline at end of file diff --git a/common/src/main/java/net/william278/husksync/redis/RedisMessageType.java b/common/src/main/java/net/william278/husksync/redis/RedisMessageType.java new file mode 100644 index 00000000..b35884be --- /dev/null +++ b/common/src/main/java/net/william278/husksync/redis/RedisMessageType.java @@ -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 getTypeFromChannel(@NotNull String messageChannel) { + return Arrays.stream(values()).filter(messageType -> messageType.getMessageChannel() + .equalsIgnoreCase(messageChannel)).findFirst(); + } + +} \ No newline at end of file diff --git a/common/src/main/resources/locales/de-de.yml b/common/src/main/resources/locales/de-de.yml index 1813606b..06175667 100644 --- a/common/src/main/resources/locales/de-de.yml +++ b/common/src/main/resources/locales/de-de.yml @@ -1,5 +1,5 @@ synchronisation_complete: '[Daten synchronisiert!](#00fb9a)' -viewing_inventory_of: '[Einsicht in das Inventar von](#00fb9a) [%1%](#00fb9a bold)' +viewing_inventory_of: '[Viewing the inventory of](#00fb9a) [%1%](#00fb9a bold) [as of %2%](#00fb9a)' viewing_ender_chest_of: '[Einsicht in die Endertruhe von](#00fb9a) [%1%](#00fb9a bold)' reload_complete: '[HuskSync](#00fb9a bold) [| Die Konfigurations- und Meldungsdateien wurden aktualisiert.](#00fb9a)' error_invalid_syntax: '[Fehler:](#ff3300) [Falsche Syntax. Nutze: %1%](#ff7e5e)' @@ -12,3 +12,8 @@ error_cannot_view_own_ender_chest: '[Fehler:](#ff3300) [Du kannst nicht auf dein error_console_command_only: '[Fehler:](#ff3300) [Dieser Befehl kann nur über die %1% Konsole ausgeführt werden](#ff7e5e)' error_no_servers_proxied: '[Fehler:](#ff3300) [Vorgang konnte nicht verarbeitet werden; Es sind keine Server online, auf denen HuskSync installiert ist. Bitte stelle sicher, dass HuskSync sowohl auf dem Proxy-Server als auch auf allen Servern installiert ist, zwischen denen du Daten synchronisieren möchtest.](#ff7e5e)' error_invalid_cluster: '[Fehler:](#ff3300) [Bitte gib die ID eines gültigen Clusters an.](#ff7e5e)' + +error_no_data_to_display: '[Error:](#ff3300) [Could not find any user data to display.](#ff7e5e)' +error_invalid_version_uuid: '[Error:](#ff3300) [Could not find any user data for that version UUID.](#ff7e5e)' +inventory_viewer_menu_title: '�fb9a&%1%''s Inventory' +ender_chest_viewer_menu_title: '�fb9a&%1%''s Ender Chest' diff --git a/common/src/main/resources/locales/en-gb.yml b/common/src/main/resources/locales/en-gb.yml index 3d04172b..8832b5a0 100644 --- a/common/src/main/resources/locales/en-gb.yml +++ b/common/src/main/resources/locales/en-gb.yml @@ -1,6 +1,4 @@ -synchronisation_complete: '[Data synchronised!](#00fb9a)' -viewing_inventory_of: '[Viewing the inventory of](#00fb9a) [%1%](#00fb9a bold)' -viewing_ender_chest_of: '[Viewing the ender chest of](#00fb9a) [%1%](#00fb9a bold)' +synchronisation_complete: '[⏵ Data synchronised!](#00fb9a)' reload_complete: '[HuskSync](#00fb9a bold) [| Reloaded config and message files.](#00fb9a)' error_invalid_syntax: '[Error:](#ff3300) [Incorrect syntax. Usage: %1%](#ff7e5e)' error_invalid_player: '[Error:](#ff3300) [Could not find that player.](#ff7e5e)' @@ -14,4 +12,20 @@ error_no_servers_proxied: '[Error:](#ff3300) [Failed to process operation; no se error_invalid_cluster: '[Error:](#ff3300) [Please specify the ID of a valid cluster.](#ff7e5e)' error_no_data_to_display: '[Error:](#ff3300) [Could not find any user data to display.](#ff7e5e)' -error_invalid_version_uuid: '[Error:](#ff3300) [Could not find any user data for that version UUID.](#ff7e5e)' \ No newline at end of file +error_invalid_version_uuid: '[Error:](#ff3300) [Could not find any user data for that version UUID.](#ff7e5e)' +inventory_viewer_menu_title: '&0%1%''s Inventory' +ender_chest_viewer_menu_title: '&0%1%''s Ender Chest' +inventory_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold) [''s inventory as of ⌚ %2%](#00fb9a)' +ender_chest_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold) [''s Ender Chest as of ⌚ %2%](#00fb9a)' +data_update_complete: '[🔔 Your data has been updated!](#00fb9a)' + +data_manager_title: '[Viewing user data snapshot for](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)[:](#00fb9a)' +data_manager_versioning: '[⌚ %1%](gray show_text=&7Version timestamp: &7When the data was saved) [⚡ %2%](gray show_text=&7Version UUID: &7%3%) [⚑ %4%](gray show_text=&7Save cause: &7What caused the data to be saved)' +data_manager_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Health points) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hunger points) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)' +data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7⚠ Based on in-game statistics)' +data_manager_item_buttons: '[[🪣 Inventory…]](color=#a17b5f-#f5b98c show_text=&7Click to view run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Click to view run_command=/enderchest %1% %2%)' +data_manager_management_buttons: '[Manage:](gray) [[❌ Delete…]](#ff3300 show_text=&7Click to delete this user data run_command=/userdata delete %1% %2%) [[⏪ Restore…]](#00fb9a show_text=&7Click to restore this user data. &#ff3300&⚠ Warning: %1%''s current data will be overwritten! run_command=/userdata delete %1% %2%)' +data_manager_advancement_preview_remaining: '&7+%1% more…' + +data_list_title: '[List of](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)[''s user data snapshots:](#00fb9a)' +data_list_item: '[%1%](gray run_command=/userdata view %6% %4%) [⌚ %2%](color=#ffc43b-#f5c962 show_text=&7Version timestamp&7When the data was saved run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Version UUID:&7%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Save cause&7What caused the data to be saved run_command=/userdata view %6% %4%)' \ No newline at end of file diff --git a/common/src/main/resources/locales/es-es.yml b/common/src/main/resources/locales/es-es.yml index fd644330..cabeaf6e 100644 --- a/common/src/main/resources/locales/es-es.yml +++ b/common/src/main/resources/locales/es-es.yml @@ -1,5 +1,5 @@ synchronisation_complete: '[Datos sincronizados!](#00fb9a)' -viewing_inventory_of: '[Viendo el inventario de](#00fb9a) [%1%](#00fb9a bold)' +viewing_inventory_of: '[Viewing the inventory of](#00fb9a) [%1%](#00fb9a bold) [as of %2%](#00fb9a)' viewing_ender_chest_of: '[Viendo el Ender Chest de](#00fb9a) [%1%](#00fb9a bold)' reload_complete: '[HuskSync](#00fb9a bold) [| Se ha reiniciado la configuración y los archivos de los mensajes.](#00fb9a)' error_invalid_syntax: '[Error:](#ff3300) [Sintaxis incorrecta. Uso: %1%](#ff7e5e)' @@ -12,3 +12,8 @@ error_cannot_view_own_ender_chest: '[Error:](#ff3300) [No puedes acceder a tu En error_console_command_only: '[Error:](#ff3300) [Ese comando solo puede ser ejecutado desde la %1% consola](#ff7e5e)' error_no_servers_proxied: '[Error:](#ff3300) [Ha ocurrido un error mientras se procesaba la acción; no hay servidores online con HusckSync instalado. Por favor, asegúrate que HuskSync está instalado tanto en el proxy como en todos los servidores entre los que quieres sincronizar datos.](#ff7e5e)' error_invalid_cluster: '[Error:](#ff3300) [Por favor, especifica la ID de un cluster válido.](#ff7e5e)' + +error_no_data_to_display: '[Error:](#ff3300) [Could not find any user data to display.](#ff7e5e)' +error_invalid_version_uuid: '[Error:](#ff3300) [Could not find any user data for that version UUID.](#ff7e5e)' +inventory_viewer_menu_title: '�fb9a&%1%''s Inventory' +ender_chest_viewer_menu_title: '�fb9a&%1%''s Ender Chest' \ No newline at end of file diff --git a/common/src/main/resources/locales/ja-jp.yml b/common/src/main/resources/locales/ja-jp.yml index 370a1567..d0d002fb 100644 --- a/common/src/main/resources/locales/ja-jp.yml +++ b/common/src/main/resources/locales/ja-jp.yml @@ -1,5 +1,5 @@ synchronisation_complete: '[データが同期されました!](#00fb9a)' -viewing_inventory_of: '[%1%](#00fb9a bold) [のインベントリを表示します](#00fb9a) ' +viewing_inventory_of: '[Viewing the inventory of](#00fb9a) [%1%](#00fb9a bold) [as of %2%](#00fb9a)' viewing_ender_chest_of: '[%1%](#00fb9a bold) [のエンダーチェストを表示します](#00fb9a) ' reload_complete: '[HuskSync](#00fb9a bold) [| 設定ファイルとメッセージファイルを再読み込みしました。](#00fb9a)' error_invalid_syntax: '[Error:](#ff3300) [構文が正しくありません。使用法: %1%](#ff7e5e)' @@ -12,3 +12,8 @@ error_cannot_view_own_ender_chest: '[Error:](#ff3300) [自分のエンダーチ error_console_command_only: '[Error:](#ff3300) [そのコマンドは%1%コンソールからのみ実行できます](#ff7e5e)' error_no_servers_proxied: '[Error:](#ff3300) [操作の処理に失敗; HuskSyncがインストールされているサーバーがオンラインになっていません。プロキシサーバーとデータを同期させたいすべてのサーバーにHuskSyncがインストールされていることを確認してください。](#ff7e5e)' error_invalid_cluster: '[Error:](#ff3300) [有効なクラスターのIDを指定してください。](#ff7e5e)' + +error_no_data_to_display: '[Error:](#ff3300) [Could not find any user data to display.](#ff7e5e)' +error_invalid_version_uuid: '[Error:](#ff3300) [Could not find any user data for that version UUID.](#ff7e5e)' +inventory_viewer_menu_title: '�fb9a&%1%''s Inventory' +ender_chest_viewer_menu_title: '�fb9a&%1%''s Ender Chest' \ No newline at end of file diff --git a/common/src/main/resources/locales/uk-ua.yml b/common/src/main/resources/locales/uk-ua.yml index 58c5e838..c59bd225 100644 --- a/common/src/main/resources/locales/uk-ua.yml +++ b/common/src/main/resources/locales/uk-ua.yml @@ -1,5 +1,5 @@ synchronisation_complete: '[Дані синхронізовано!](#00fb9a)' -viewing_inventory_of: '[Переглядання інвентару](#00fb9a) [%1%](#00fb9a bold)' +viewing_inventory_of: '[Viewing the inventory of](#00fb9a) [%1%](#00fb9a bold) [as of %2%](#00fb9a)' viewing_ender_chest_of: '[Переглядання скрині енду](#00fb9a) [%1%](#00fb9a bold)' reload_complete: '[HuskSync](#00fb9a bold) [| Перезавантажено конфіґ та файли повідомлень.](#00fb9a)' error_invalid_syntax: '[Помилка:](#ff3300) [Неправильний синтакс. Використання: %1%](#ff7e5e)' @@ -11,4 +11,9 @@ error_cannot_view_own_inventory: '[Помилка:](#ff3300) [Ви не може error_cannot_view_own_ender_chest: '[Помилка:](#ff3300) [Ви не можете переглядати власну скриню енду!](#ff7e5e)' error_console_command_only: '[Помилка:](#ff3300) [Ця команда може бути використана лише з допомогою %1% консолі](#ff7e5e)' error_no_servers_proxied: '[Помилка:](#ff3300) [Не вдалося опрацювати операцію; не знайдено жодного сервера із встановленим HuskSync. Запевніться, будьласка, що HuskSync встановлено на Проксі та усіх серверах між якими ви хочете синхроніхувати дані.](#ff7e5e)' -error_invalid_cluster: '[Помилка:](#ff3300) [Зазнчте будь ласка ID слушного кластеру.](#ff7e5e)' \ No newline at end of file +error_invalid_cluster: '[Помилка:](#ff3300) [Зазнчте будь ласка ID слушного кластеру.](#ff7e5e)' + +error_no_data_to_display: '[Error:](#ff3300) [Could not find any user data to display.](#ff7e5e)' +error_invalid_version_uuid: '[Error:](#ff3300) [Could not find any user data for that version UUID.](#ff7e5e)' +inventory_viewer_menu_title: '�fb9a&%1%''s Inventory' +ender_chest_viewer_menu_title: '�fb9a&%1%''s Ender Chest' \ No newline at end of file diff --git a/common/src/main/resources/locales/zh-cn.yml b/common/src/main/resources/locales/zh-cn.yml index 529f91aa..c739503f 100644 --- a/common/src/main/resources/locales/zh-cn.yml +++ b/common/src/main/resources/locales/zh-cn.yml @@ -1,5 +1,5 @@ synchronisation_complete: '[数据同步完成](#00fb9a)' -viewing_inventory_of: '[查看玩家背包:](#00fb9a) [%1%](#00fb9a bold)' +viewing_inventory_of: '[Viewing the inventory of](#00fb9a) [%1%](#00fb9a bold) [as of %2%](#00fb9a)' viewing_ender_chest_of: '[查看玩家末影箱:](#00fb9a) [%1%](#00fb9a bold)' reload_complete: '[HuskSync](#00fb9a bold) [| 配置与语言文件重载完成.](#00fb9a)' error_invalid_syntax: '[错误:](#ff3300) [格式错误. 使用方法: %1%](#ff7e5e)' @@ -12,3 +12,8 @@ error_cannot_view_own_ender_chest: '[错误:](#ff3300) [你不能查看和编辑 error_console_command_only: '[错误:](#ff3300) [该命令只能由 %1% 控制台执行](#ff7e5e)' error_no_servers_proxied: '[错误:](#ff3300) [操作处理失败; 没有任何安装了 HuskSync 的后端服务器在线. 请确认 HuskSync 已在 BungeeCord/Velocity 等代理服务器和所有你希望互相同步数据的后端服务器间安装.](#ff7e5e)' error_invalid_cluster: '[错误:](#ff3300) [请指定一个有效的集群(cluster) ID.](#ff7e5e)' + +error_no_data_to_display: '[Error:](#ff3300) [Could not find any user data to display.](#ff7e5e)' +error_invalid_version_uuid: '[Error:](#ff3300) [Could not find any user data for that version UUID.](#ff7e5e)' +inventory_viewer_menu_title: '�fb9a&%1%''s Inventory' +ender_chest_viewer_menu_title: '�fb9a&%1%''s Ender Chest' \ No newline at end of file diff --git a/common/src/main/resources/locales/zh-tw.yml b/common/src/main/resources/locales/zh-tw.yml index 0aa3afaa..984449a5 100644 --- a/common/src/main/resources/locales/zh-tw.yml +++ b/common/src/main/resources/locales/zh-tw.yml @@ -1,5 +1,5 @@ synchronisation_complete: '[資料已同步!!](#00fb9a)' -viewing_inventory_of: '[查看](#00fb9a) [%1%](#00fb9a bold) [的背包](#00fb9a)' +viewing_inventory_of: '[Viewing the inventory of](#00fb9a) [%1%](#00fb9a bold) [as of %2%](#00fb9a)' viewing_ender_chest_of: '[查看](#00fb9a) [%1%](#00fb9a bold) [的終界箱](#00fb9a)' reload_complete: '[HuskSync](#00fb9a bold) [| 已重新載入配置和訊息文件](#00fb9a)' error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法: %1%](#ff7e5e)' @@ -11,4 +11,9 @@ error_cannot_view_own_inventory: '[錯誤:](#ff3300) [您無法查看自己的 error_cannot_view_own_ender_chest: '[錯誤:](#ff3300) [你無法查看自己的終界箱!](#ff7e5e)' error_console_command_only: '[錯誤:](#ff3300) [該指令只能通過 %1% 控制台運行](#ff7e5e)' error_no_servers_proxied: '[錯誤:](#ff3300) [處理操作失敗: 沒有安裝 HuskSync 的伺服器在線。 請確保在 Proxy 伺服器和您希望在其他同步數據的所有伺服器上都安裝了 HuskSync。](#ff7e5e)' -error_invalid_cluster: '[錯誤:](#ff3300) [請提供有效的 Cluster ID](#ff7e5e)' \ No newline at end of file +error_invalid_cluster: '[錯誤:](#ff3300) [請提供有效的 Cluster ID](#ff7e5e)' + +error_no_data_to_display: '[Error:](#ff3300) [Could not find any user data to display.](#ff7e5e)' +error_invalid_version_uuid: '[Error:](#ff3300) [Could not find any user data for that version UUID.](#ff7e5e)' +inventory_viewer_menu_title: '�fb9a&%1%''s Inventory' +ender_chest_viewer_menu_title: '�fb9a&%1%''s Ender Chest' \ No newline at end of file diff --git a/common/src/test/java/net/william278/husksync/player/DummyPlayer.java b/common/src/test/java/net/william278/husksync/player/DummyPlayer.java index 8843276e..1365a4f4 100644 --- a/common/src/test/java/net/william278/husksync/player/DummyPlayer.java +++ b/common/src/test/java/net/william278/husksync/player/DummyPlayer.java @@ -2,7 +2,7 @@ package net.william278.husksync.player; import de.themoep.minedown.MineDown; import net.william278.husksync.data.*; -import net.william278.husksync.editor.InventoryEditorMenu; +import net.william278.husksync.editor.ItemEditorMenu; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -149,7 +149,7 @@ public class DummyPlayer extends OnlineUser { } @Override - public void showMenu(@NotNull InventoryEditorMenu menu) { + public void showMenu(@NotNull ItemEditorMenu menu) { // do nothing }