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 ab76e47b..2a171b3c 100644 --- a/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java +++ b/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java @@ -455,12 +455,12 @@ public class BukkitPlayer extends OnlineUser { @Override public void sendActionBar(@NotNull MineDown mineDown) { - player.spigot().sendMessage(ChatMessageType.ACTION_BAR, mineDown.toComponent()); + player.spigot().sendMessage(ChatMessageType.ACTION_BAR, mineDown.replace().toComponent()); } @Override public void sendMessage(@NotNull MineDown mineDown) { - player.spigot().sendMessage(mineDown.toComponent()); + player.spigot().sendMessage(mineDown.replace().toComponent()); } /** diff --git a/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java b/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java index 94e6f5f2..5422aa82 100644 --- a/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java +++ b/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java @@ -1,22 +1,23 @@ 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.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.List; import java.util.Locale; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; -public class EnderChestCommand extends CommandBase { +public class EnderChestCommand extends CommandBase implements TabCompletable { public EnderChestCommand(@NotNull HuskSync implementor) { super("enderchest", Permission.COMMAND_ENDER_CHEST, implementor, "echest", "openechest"); @@ -35,12 +36,10 @@ public class EnderChestCommand extends CommandBase { // 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))); + plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> data.ifPresentOrElse( + userData -> showEnderChestMenu(player, userData, user, false), + () -> 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); @@ -77,6 +76,14 @@ public class EnderChestCommand extends CommandBase { plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.ENDER_CHEST_COMMAND_EDIT).join(); plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join(); }); + + } + + @Override + public List onTabComplete(@NotNull OnlineUser player, @NotNull String[] args) { + return plugin.getOnlineUsers().stream().map(user -> user.username) + .filter(argument -> argument.startsWith(args.length >= 1 ? args[1] : "")) + .sorted().collect(Collectors.toList()); } } diff --git a/common/src/main/java/net/william278/husksync/command/InventoryCommand.java b/common/src/main/java/net/william278/husksync/command/InventoryCommand.java index b248ed1e..23b0af90 100644 --- a/common/src/main/java/net/william278/husksync/command/InventoryCommand.java +++ b/common/src/main/java/net/william278/husksync/command/InventoryCommand.java @@ -11,11 +11,13 @@ import net.william278.husksync.player.User; import org.jetbrains.annotations.NotNull; import java.text.DateFormat; +import java.util.List; import java.util.Locale; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; -public class InventoryCommand extends CommandBase { +public class InventoryCommand extends CommandBase implements TabCompletable { public InventoryCommand(@NotNull HuskSync implementor) { super("inventory", Permission.COMMAND_INVENTORY, implementor, "invsee", "openinv"); @@ -34,12 +36,10 @@ public class InventoryCommand extends CommandBase { // 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))); + plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> data.ifPresentOrElse( + userData -> showInventoryMenu(player, userData, user, false), + () -> 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); @@ -78,4 +78,10 @@ public class InventoryCommand extends CommandBase { }); } + @Override + public List onTabComplete(@NotNull OnlineUser player, @NotNull String[] args) { + return plugin.getOnlineUsers().stream().map(user -> user.username) + .filter(argument -> argument.startsWith(args.length >= 1 ? args[1] : "")) + .sorted().collect(Collectors.toList()); + } } diff --git a/common/src/main/java/net/william278/husksync/command/UserDataCommand.java b/common/src/main/java/net/william278/husksync/command/UserDataCommand.java index 557d49d4..e1ce5adc 100644 --- a/common/src/main/java/net/william278/husksync/command/UserDataCommand.java +++ b/common/src/main/java/net/william278/husksync/command/UserDataCommand.java @@ -1,10 +1,12 @@ package net.william278.husksync.command; import net.william278.husksync.HuskSync; +import net.william278.husksync.data.DataSaveCause; import net.william278.husksync.player.OnlineUser; import org.jetbrains.annotations.NotNull; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -41,14 +43,11 @@ public class UserDataCommand extends CommandBase implements TabCompletable { 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))), + user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> + data.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) { @@ -91,18 +90,93 @@ public class UserDataCommand extends CommandBase implements TabCompletable { .ifPresent(player::sendMessage)))); } case "delete" -> { - + // Delete user data by specified UUID + if (args.length < 3) { + plugin.getLocales().getLocale("error_invalid_syntax", + "/userdata delete ") + .ifPresent(player::sendMessage); + return; + } + final String username = args[1]; + try { + final UUID versionUuid = UUID.fromString(args[2]); + CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept( + optionalUser -> optionalUser.ifPresentOrElse( + user -> plugin.getDatabase().deleteUserData(user, versionUuid).thenAccept(deleted -> { + if (deleted) { + plugin.getLocales().getLocale("data_deleted", + versionUuid.toString().split("-")[0], + versionUuid.toString(), + user.username, + user.uuid.toString()) + .ifPresent(player::sendMessage); + } else { + 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 delete ") + .ifPresent(player::sendMessage); + } } case "restore" -> { - + // Get user data by specified uuid and username + if (args.length < 3) { + plugin.getLocales().getLocale("error_invalid_syntax", + "/userdata restore ") + .ifPresent(player::sendMessage); + return; + } + final String username = args[1]; + try { + final UUID versionUuid = UUID.fromString(args[2]); + CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept( + optionalUser -> optionalUser.ifPresentOrElse( + user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> { + if (data.isEmpty()) { + plugin.getLocales().getLocale("error_invalid_version_uuid") + .ifPresent(player::sendMessage); + return; + } + plugin.getDatabase().setUserData(user, data.get().userData(), + DataSaveCause.BACKUP_RESTORE); + plugin.getRedisManager().sendUserDataUpdate(user, data.get().userData()).join(); + plugin.getLocales().getLocale("data_restored", + user.username, + user.uuid.toString(), + versionUuid.toString().split("-")[0], + versionUuid.toString()) + .ifPresent(player::sendMessage); + }), + () -> plugin.getLocales().getLocale("error_invalid_player") + .ifPresent(player::sendMessage)))); + } catch (IllegalArgumentException e) { + plugin.getLocales().getLocale("error_invalid_syntax", + "/userdata restore ") + .ifPresent(player::sendMessage); + } } } } @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()); + switch (args.length) { + case 0, 1 -> { + return Arrays.stream(COMMAND_ARGUMENTS) + .filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : "")) + .sorted().collect(Collectors.toList()); + } + case 2 -> { + return plugin.getOnlineUsers().stream().map(user -> user.username) + .filter(argument -> argument.startsWith(args[1])) + .sorted().collect(Collectors.toList()); + } + } + return Collections.emptyList(); } } diff --git a/common/src/main/java/net/william278/husksync/config/Locales.java b/common/src/main/java/net/william278/husksync/config/Locales.java index 1a6a3c5f..5744188a 100644 --- a/common/src/main/java/net/william278/husksync/config/Locales.java +++ b/common/src/main/java/net/william278/husksync/config/Locales.java @@ -42,7 +42,7 @@ public class Locales { */ public Optional getRawLocale(@NotNull String localeId) { if (rawLocales.containsKey(localeId)) { - return Optional.of(rawLocales.get(localeId)); + return Optional.of(rawLocales.get(localeId).replaceAll(Pattern.quote("\\n"), "\n")); } return Optional.empty(); } diff --git a/common/src/main/java/net/william278/husksync/database/Database.java b/common/src/main/java/net/william278/husksync/database/Database.java index c8a2033a..ef5d6d6c 100644 --- a/common/src/main/java/net/william278/husksync/database/Database.java +++ b/common/src/main/java/net/william278/husksync/database/Database.java @@ -169,12 +169,30 @@ public abstract class Database { public abstract CompletableFuture> getUserData(@NotNull User user); /** - * (Internal) Prune user data records for a given user to the maximum value as configured + * Gets a specific {@link VersionedUserData} entry for a user from the database, by its UUID. + * + * @param user The user to get data for + * @param versionUuid The UUID of the {@link VersionedUserData} entry to get + * @return A future returning an optional containing the {@link VersionedUserData}, if it exists, or an empty optional if it does not + */ + public abstract CompletableFuture> getUserData(@NotNull User user, @NotNull UUID versionUuid); + + /** + * (Internal) Prune user data for a given user to the maximum value as configured * * @param user The user to prune data for * @return A future returning void when complete */ - protected abstract CompletableFuture pruneUserDataRecords(@NotNull User user); + protected abstract CompletableFuture pruneUserData(@NotNull User user); + + /** + * Deletes a specific {@link VersionedUserData} entry for a user from the database, by its UUID. + * + * @param user The user to get data for + * @param versionUuid The UUID of the {@link VersionedUserData} entry to delete + * @return A future returning void when complete + */ + public abstract CompletableFuture deleteUserData(@NotNull User user, @NotNull UUID versionUuid); /** * Save user data to the database

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 7034a5dc..0160d997 100644 --- a/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java +++ b/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java @@ -268,7 +268,38 @@ public class MySqlDatabase extends Database { } @Override - protected CompletableFuture pruneUserDataRecords(@NotNull User user) { + public CompletableFuture> getUserData(@NotNull User user, @NotNull UUID versionUuid) { + return CompletableFuture.supplyAsync(() -> { + try (Connection connection = getConnection()) { + try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" + SELECT `version_uuid`, `timestamp`, `save_cause`, `data` + FROM `%data_table%` + WHERE `player_uuid`=? AND `version_uuid`=? + ORDER BY `timestamp` DESC + LIMIT 1;"""))) { + statement.setString(1, user.uuid.toString()); + statement.setString(2, versionUuid.toString()); + final ResultSet resultSet = statement.executeQuery(); + if (resultSet.next()) { + final Blob blob = resultSet.getBlob("data"); + final byte[] dataByteArray = blob.getBytes(1, (int) blob.length()); + blob.free(); + return Optional.of(new VersionedUserData( + UUID.fromString(resultSet.getString("version_uuid")), + Date.from(resultSet.getTimestamp("timestamp").toInstant()), + DataSaveCause.getCauseByName(resultSet.getString("save_cause")), + getDataAdapter().fromBytes(dataByteArray))); + } + } + } catch (SQLException | DataAdaptionException e) { + getLogger().log(Level.SEVERE, "Failed to fetch specific user data by UUID from the database", e); + } + return Optional.empty(); + }); + } + + @Override + protected CompletableFuture pruneUserData(@NotNull User user) { return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(data -> { if (data.size() > maxUserDataRecords) { try (Connection connection = getConnection()) { @@ -288,6 +319,25 @@ public class MySqlDatabase extends Database { })); } + @Override + public CompletableFuture deleteUserData(@NotNull User user, @NotNull UUID versionUuid) { + return CompletableFuture.supplyAsync(() -> { + try (Connection connection = getConnection()) { + try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" + DELETE FROM `%data_table%` + WHERE `player_uuid`=? AND `version_uuid`=? + LIMIT 1;"""))) { + statement.setString(1, user.uuid.toString()); + statement.setString(2, versionUuid.toString()); + return statement.executeUpdate() > 0; + } + } catch (SQLException e) { + getLogger().log(Level.SEVERE, "Failed to delete specific user data from the database", e); + } + return false; + }); + } + @Override public CompletableFuture setUserData(@NotNull User user, @NotNull UserData userData, @NotNull DataSaveCause saveCause) { @@ -311,7 +361,7 @@ public class MySqlDatabase extends Database { getLogger().log(Level.SEVERE, "Failed to set user data in the database", e); } } - }).thenRun(() -> pruneUserDataRecords(user).join()); + }).thenRun(() -> pruneUserData(user).join()); } @Override 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 55149e9d..4443083d 100644 --- a/common/src/main/java/net/william278/husksync/editor/DataEditor.java +++ b/common/src/main/java/net/william278/husksync/editor/DataEditor.java @@ -82,18 +82,21 @@ public class DataEditor { public void displayDataOverview(@NotNull OnlineUser user, @NotNull VersionedUserData userData, @NotNull User dataOwner) { 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(), + dataOwner.username, + dataOwner.uuid.toString()) + .ifPresent(user::sendMessage); + locales.getLocale("data_manager_timestamp", + new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss").format(userData.versionTimestamp())) + .ifPresent(user::sendMessage); + locales.getLocale("data_manager_cause", 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((int) userData.userData().getStatusData().health), + Integer.toString((int) userData.userData().getStatusData().maxHealth), + Integer.toString(userData.userData().getStatusData().hunger), Integer.toString(userData.userData().getStatusData().expLevel), userData.userData().getStatusData().gameMode.toLowerCase()) .ifPresent(user::sendMessage); @@ -146,7 +149,7 @@ public class DataEditor { dataOwner.username, dataOwner.uuid.toString()) .ifPresent(user::sendMessage); - final String[] numberedIcons = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳" .split(""); + 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", diff --git a/common/src/main/java/net/william278/husksync/util/UpdateChecker.java b/common/src/main/java/net/william278/husksync/util/UpdateChecker.java index f7f488cf..7ce98f73 100644 --- a/common/src/main/java/net/william278/husksync/util/UpdateChecker.java +++ b/common/src/main/java/net/william278/husksync/util/UpdateChecker.java @@ -3,7 +3,6 @@ package net.william278.husksync.util; import org.jetbrains.annotations.NotNull; import java.io.BufferedReader; -import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; @@ -13,7 +12,6 @@ import java.util.logging.Level; public class UpdateChecker { private final static int SPIGOT_PROJECT_ID = 97144; - private final Logger logger; private final VersionUtils.Version currentVersion; @@ -52,7 +50,7 @@ public class UpdateChecker { if (isUpdateAvailable(latestVersion)) { logger.log(Level.WARNING, "A new version of HuskSync is available: v" + latestVersion); } else { - logger.log(Level.INFO, "HuskSync is up-to-date! (Running: v" + currentVersion + ")"); + logger.log(Level.INFO, "HuskSync is up-to-date! (Running: v" + getCurrentVersion().toString() + ")"); } }); } diff --git a/common/src/main/resources/locales/en-gb.yml b/common/src/main/resources/locales/en-gb.yml index 8832b5a0..83bcfa95 100644 --- a/common/src/main/resources/locales/en-gb.yml +++ b/common/src/main/resources/locales/en-gb.yml @@ -10,7 +10,6 @@ error_cannot_view_own_ender_chest: '[Error:](#ff3300) [You can''t access your ow error_console_command_only: '[Error:](#ff3300) [That command can only be run through the %1% console](#ff7e5e)' error_no_servers_proxied: '[Error:](#ff3300) [Failed to process operation; no servers are online that have HuskSync installed. Please ensure HuskSync is installed on both the Proxy server and all servers you wish to synchronise data between.](#ff7e5e)' 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)' inventory_viewer_menu_title: '&0%1%''s Inventory' @@ -18,14 +17,15 @@ 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_title: '[Viewing user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\\n&8%4%)[:](#00fb9a)' +data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\\n&7When the data was saved)' +data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\\n&7What caused the data to be saved)\\n' 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_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%)\\n' +data_manager_management_buttons: '[Manage:](gray) [[❌ Delete…]](#ff3300 show_text=&7Click to delete this user data run_command=/userdata restore %1% %2%) [[⏪ Restore…]](#00fb9a show_text=&7Click to restore this user data.\\n&#ff3300&⚠ Warning: %1%''s current data will be overwritten! run_command=/userdata delete %1% %2%)\\n' 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 +data_list_title: '[List of](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)[''s user data snapshots:](#00fb9a)\\n' +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%)' +data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&7%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&7%4%)' +data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\\n&7%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\\n&7%4%)' \ No newline at end of file