diff --git a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java index 41a76117..abc129e6 100644 --- a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java +++ b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java @@ -77,6 +77,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { public void onEnable() { // Process initialization stages CompletableFuture.supplyAsync(() -> { + // Set the logging adapter and resource reader this.logger = new BukkitLogger(this.getLogger()); this.resourceReader = new BukkitResourceReader(this); @@ -117,12 +118,19 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { }).thenApply(succeeded -> { // Prepare migrators if (succeeded) { + logger.debug("m0"); availableMigrators = new ArrayList<>(); + logger.debug("m1"); availableMigrators.add(new LegacyMigrator(this)); + logger.debug("m2"); final Plugin mySqlPlayerDataBridge = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge"); + logger.debug("m3"); if (mySqlPlayerDataBridge != null) { + logger.debug("m4"); availableMigrators.add(new MpdbMigrator(this, mySqlPlayerDataBridge)); + logger.debug("m5"); } + logger.debug("m6 - Successfully prepared migrators"); } return succeeded; }).thenApply(succeeded -> { @@ -198,17 +206,22 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { // Check for updates if (succeeded && settings.getBooleanValue(Settings.ConfigOption.CHECK_FOR_UPDATES)) { getLoggingAdapter().log(Level.INFO, "Checking for updates..."); - new UpdateChecker(getVersion(), getLoggingAdapter()).logToConsole(); + new UpdateChecker(getPluginVersion(), getLoggingAdapter()).logToConsole(); } return succeeded; }).thenAccept(succeeded -> { // Handle failed initialization if (!succeeded) { - getLoggingAdapter().log(Level.SEVERE, "Failed to initialize HuskSync. " + "The plugin will now be disabled"); + getLoggingAdapter().log(Level.SEVERE, "Failed to initialize HuskSync. The plugin will now be disabled"); getServer().getPluginManager().disablePlugin(this); } else { - getLoggingAdapter().log(Level.INFO, "Successfully enabled HuskSync v" + getVersion()); + getLoggingAdapter().log(Level.INFO, "Successfully enabled HuskSync v" + getPluginVersion()); } + }).exceptionally(exception -> { + getLoggingAdapter().log(Level.SEVERE, "An exception occurred initializing HuskSync. (" + exception.getMessage() + ") The plugin will now be disabled."); + exception.printStackTrace(); + getServer().getPluginManager().disablePlugin(this); + return null; }); } @@ -217,7 +230,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { if (this.eventListener != null) { this.eventListener.handlePluginDisable(); } - getLoggingAdapter().log(Level.INFO, "Successfully disabled HuskSync v" + getVersion()); + getLoggingAdapter().log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion()); } @Override @@ -281,8 +294,13 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { } @Override - public @NotNull String getVersion() { - return getDescription().getVersion(); + public @NotNull Version getPluginVersion() { + return Version.pluginVersion(getDescription().getVersion()); + } + + @Override + public @NotNull Version getMinecraftVersion() { + return Version.minecraftVersion(Bukkit.getBukkitVersion()); } @Override 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 9e8c73b9..11d7ead1 100644 --- a/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java +++ b/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java @@ -53,12 +53,12 @@ public class BukkitEventListener extends EventListener implements Listener { public void onInventoryClose(@NotNull InventoryCloseEvent event) { if (event.getPlayer() instanceof Player player) { final OnlineUser user = BukkitPlayer.adapt(player); - if (huskSync.getDataEditor().isEditingInventoryData(user)) { + if (plugin.getDataEditor().isEditingInventoryData(user)) { try { BukkitSerializer.serializeItemStackArray(event.getInventory().getContents()).thenAccept( serializedInventory -> super.handleMenuClose(user, new ItemData(serializedInventory))); } catch (DataSerializationException e) { - huskSync.getLoggingAdapter().log(Level.SEVERE, + plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to serialize inventory data during menu close", e); } } diff --git a/bukkit/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java b/bukkit/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java index 16487678..2ca98df5 100644 --- a/bukkit/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java +++ b/bukkit/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java @@ -32,6 +32,8 @@ public class LegacyMigrator extends Migrator { private String sourcePlayersTable; private String sourceDataTable; + private final String minecraftVersion; + public LegacyMigrator(@NotNull HuskSync plugin) { super(plugin); this.hslConverter = HSLConverter.getInstance(); @@ -42,6 +44,7 @@ public class LegacyMigrator extends Migrator { this.sourceDatabase = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_NAME); this.sourcePlayersTable = "husksync_players"; this.sourceDataTable = "husksync_data"; + this.minecraftVersion = plugin.getMinecraftVersion().getWithoutMeta(); } @Override @@ -110,7 +113,7 @@ public class LegacyMigrator extends Migrator { } plugin.getLoggingAdapter().log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the legacy database!"); plugin.getLoggingAdapter().log(Level.INFO, "Converting HuskSync 1.x data to the latest HuskSync user data format..."); - dataToMigrate.forEach(data -> data.toUserData(hslConverter).thenAccept(convertedData -> + dataToMigrate.forEach(data -> data.toUserData(hslConverter, minecraftVersion).thenAccept(convertedData -> plugin.getDatabase().ensureUser(data.user()).thenRun(() -> plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.LEGACY_MIGRATION) .exceptionally(exception -> { @@ -246,7 +249,8 @@ public class LegacyMigrator extends Migrator { @NotNull String serializedAdvancements, @NotNull String serializedLocation) { @NotNull - public CompletableFuture toUserData(@NotNull HSLConverter converter) { + public CompletableFuture toUserData(@NotNull HSLConverter converter, + @NotNull String minecraftVersion) { return CompletableFuture.supplyAsync(() -> { try { final DataSerializer.StatisticData legacyStatisticData = converter @@ -278,7 +282,8 @@ public class LegacyMigrator extends Migrator { new ItemData(serializedInventory), new ItemData(serializedEnderChest), new PotionEffectData(serializedPotionEffects), convertedAdvancements, convertedStatisticData, convertedLocationData, - new PersistentDataContainerData(new HashMap<>())); + new PersistentDataContainerData(new HashMap<>()), + minecraftVersion); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/bukkit/src/main/java/net/william278/husksync/migrator/MpdbMigrator.java b/bukkit/src/main/java/net/william278/husksync/migrator/MpdbMigrator.java index 626a02c1..73a157c3 100644 --- a/bukkit/src/main/java/net/william278/husksync/migrator/MpdbMigrator.java +++ b/bukkit/src/main/java/net/william278/husksync/migrator/MpdbMigrator.java @@ -35,6 +35,7 @@ public class MpdbMigrator extends Migrator { private String sourceInventoryTable; private String sourceEnderChestTable; private String sourceExperienceTable; + private final String minecraftVersion; public MpdbMigrator(@NotNull BukkitHuskSync plugin, @NotNull Plugin mySqlPlayerDataBridge) { super(plugin); @@ -47,6 +48,8 @@ public class MpdbMigrator extends Migrator { this.sourceInventoryTable = "mpdb_inventory"; this.sourceEnderChestTable = "mpdb_enderchest"; this.sourceExperienceTable = "mpdb_experience"; + this.minecraftVersion = plugin.getMinecraftVersion().getWithoutMeta(); + } @Override @@ -106,7 +109,7 @@ public class MpdbMigrator extends Migrator { } plugin.getLoggingAdapter().log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the MySQLPlayerDataBridge database!"); plugin.getLoggingAdapter().log(Level.INFO, "Converting raw MySQLPlayerDataBridge data to HuskSync user data..."); - dataToMigrate.forEach(data -> data.toUserData(mpdbConverter).thenAccept(convertedData -> + dataToMigrate.forEach(data -> data.toUserData(mpdbConverter, minecraftVersion).thenAccept(convertedData -> plugin.getDatabase().ensureUser(data.user()).thenRun(() -> plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.MPDB_MIGRATION)) .exceptionally(exception -> { @@ -257,7 +260,8 @@ public class MpdbMigrator extends Migrator { * @return A {@link CompletableFuture} that will resolve to the converted {@link UserData} object */ @NotNull - public CompletableFuture toUserData(@NotNull MPDBConverter converter) { + public CompletableFuture toUserData(@NotNull MPDBConverter converter, + @NotNull String minecraftVersion) { return CompletableFuture.supplyAsync(() -> { // Combine inventory and armour final Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER); @@ -278,7 +282,8 @@ public class MpdbMigrator extends Migrator { new StatisticsData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()), new LocationData("world", UUID.randomUUID(), "NORMAL", 0, 0, 0, 0f, 0f), - new PersistentDataContainerData(new HashMap<>())); + new PersistentDataContainerData(new HashMap<>()), + minecraftVersion); }); } } 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 2a171b3c..583b665b 100644 --- a/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java +++ b/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java @@ -6,6 +6,7 @@ import net.md_5.bungee.api.chat.BaseComponent; import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.data.*; import net.william278.husksync.editor.ItemEditorMenu; +import net.william278.husksync.util.Version; import org.apache.commons.lang.ArrayUtils; import org.bukkit.*; import org.bukkit.advancement.Advancement; @@ -434,6 +435,12 @@ public class BukkitPlayer extends OnlineUser { } } + @NotNull + @Override + public Version getMinecraftVersion() { + return Version.minecraftVersion(Bukkit.getBukkitVersion()); + } + @Override public boolean hasPermission(@NotNull String node) { return player.hasPermission(node); diff --git a/common/src/main/java/net/william278/husksync/HuskSync.java b/common/src/main/java/net/william278/husksync/HuskSync.java index f51b77d5..5d647491 100644 --- a/common/src/main/java/net/william278/husksync/HuskSync.java +++ b/common/src/main/java/net/william278/husksync/HuskSync.java @@ -10,6 +10,7 @@ import net.william278.husksync.migrator.Migrator; import net.william278.husksync.player.OnlineUser; import net.william278.husksync.redis.RedisManager; import net.william278.husksync.util.Logger; +import net.william278.husksync.util.Version; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -18,32 +19,122 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; +/** + * Abstract implementation of the HuskSync plugin. + */ public interface HuskSync { - @NotNull Set getOnlineUsers(); - - @NotNull Optional getOnlineUser(@NotNull UUID uuid); - - @NotNull Database getDatabase(); - - @NotNull RedisManager getRedisManager(); - - @NotNull DataAdapter getDataAdapter(); - - @NotNull DataEditor getDataEditor(); - - @NotNull EventCannon getEventCannon(); - - @NotNull List getAvailableMigrators(); - - @NotNull Settings getSettings(); - - @NotNull Locales getLocales(); - - @NotNull Logger getLoggingAdapter(); - - @NotNull String getVersion(); - + /** + * Returns a set of online players. + * + * @return a set of online players as {@link OnlineUser} + */ + @NotNull + Set getOnlineUsers(); + + /** + * Returns an online user by UUID if they exist + * + * @param uuid the UUID of the user to get + * @return an online user as {@link OnlineUser} + */ + @NotNull + Optional getOnlineUser(@NotNull UUID uuid); + + /** + * Returns the database implementation + * + * @return the {@link Database} implementation + */ + @NotNull + Database getDatabase(); + + /** + * Returns the redis manager implementation + * + * @return the {@link RedisManager} implementation + */ + + @NotNull + RedisManager getRedisManager(); + + /** + * Returns the data adapter implementation + * + * @return the {@link DataAdapter} implementation + */ + @NotNull + DataAdapter getDataAdapter(); + + /** + * Returns the data editor implementation + * + * @return the {@link DataEditor} implementation + */ + @NotNull + DataEditor getDataEditor(); + + /** + * Returns the event firing cannon + * + * @return the {@link EventCannon} implementation + */ + @NotNull + EventCannon getEventCannon(); + + /** + * Returns a list of available data {@link Migrator}s + * + * @return a list of {@link Migrator}s + */ + @NotNull + List getAvailableMigrators(); + + /** + * Returns the plugin {@link Settings} + * + * @return the {@link Settings} + */ + @NotNull + Settings getSettings(); + + /** + * Returns the plugin {@link Locales} + * + * @return the {@link Locales} + */ + @NotNull + Locales getLocales(); + + /** + * Returns the plugin {@link Logger} + * + * @return the {@link Logger} + */ + @NotNull + Logger getLoggingAdapter(); + + /** + * Returns the plugin version + * + * @return the plugin {@link Version} + */ + @NotNull + Version getPluginVersion(); + + /** + * Returns the Minecraft version implementation + * + * @return the Minecraft {@link Version} + */ + @NotNull + Version getMinecraftVersion(); + + /** + * Reloads the {@link Settings} and {@link Locales} from their respective config files + * + * @return a {@link CompletableFuture} that will be completed when the plugin reload is complete and if it was successful + */ CompletableFuture reload(); } diff --git a/common/src/main/java/net/william278/husksync/api/BaseHuskSyncAPI.java b/common/src/main/java/net/william278/husksync/api/BaseHuskSyncAPI.java index 164da713..fe1fc24f 100644 --- a/common/src/main/java/net/william278/husksync/api/BaseHuskSyncAPI.java +++ b/common/src/main/java/net/william278/husksync/api/BaseHuskSyncAPI.java @@ -3,7 +3,7 @@ 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.data.UserDataSnapshot; import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.User; import org.jetbrains.annotations.NotNull; @@ -74,7 +74,7 @@ public abstract class BaseHuskSyncAPI { if (user instanceof OnlineUser) { return Optional.of(((OnlineUser) user).getUserData().join()); } else { - return plugin.getDatabase().getCurrentUserData(user).join().map(VersionedUserData::userData); + return plugin.getDatabase().getCurrentUserData(user).join().map(UserDataSnapshot::userData); } }); } @@ -108,16 +108,16 @@ public abstract class BaseHuskSyncAPI { } /** - * Returns the saved {@link VersionedUserData} records for the given {@link User} + * Returns the saved {@link UserDataSnapshot} 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, + * @param user the {@link User} to get the {@link UserDataSnapshot} for + * @return future returning a list {@link UserDataSnapshot} 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) { + public final CompletableFuture> getSavedUserData(@NotNull User user) { return CompletableFuture.supplyAsync(() -> plugin.getDatabase().getUserData(user).join()); } 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 d4614a9a..8fa45b5a 100644 --- a/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java +++ b/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java @@ -4,7 +4,7 @@ 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.UserDataSnapshot; import net.william278.husksync.editor.ItemEditorMenu; import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.User; @@ -55,15 +55,15 @@ public class EnderChestCommand extends CommandBase implements TabCompletable { .ifPresent(player::sendMessage))); } - private void showEnderChestMenu(@NotNull OnlineUser player, @NotNull VersionedUserData versionedUserData, + private void showEnderChestMenu(@NotNull OnlineUser player, @NotNull UserDataSnapshot userDataSnapshot, @NotNull User dataOwner, final boolean allowEdit) { CompletableFuture.runAsync(() -> { - final UserData data = versionedUserData.userData(); + final UserData data = userDataSnapshot.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())) + .format(userDataSnapshot.versionTimestamp())) .ifPresent(player::sendMessage); final ItemData enderChestDataOnClose = plugin.getDataEditor().openItemEditorMenu(player, menu).join(); if (!menu.canEdit) { @@ -72,7 +72,8 @@ public class EnderChestCommand extends CommandBase implements TabCompletable { final UserData updatedUserData = new UserData(data.getStatusData(), data.getInventoryData(), enderChestDataOnClose, data.getPotionEffectsData(), data.getAdvancementData(), data.getStatisticsData(), data.getLocationData(), - data.getPersistentDataContainerData()); + data.getPersistentDataContainerData(), + plugin.getMinecraftVersion().getWithoutMeta()); 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 b139b0b0..c83dc3b8 100644 --- a/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java +++ b/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java @@ -34,7 +34,7 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage); return; } - final UpdateChecker updateChecker = new UpdateChecker(plugin.getVersion(), plugin.getLoggingAdapter()); + final UpdateChecker updateChecker = new UpdateChecker(plugin.getPluginVersion(), plugin.getLoggingAdapter()); updateChecker.fetchLatestVersion().thenAccept(latestVersion -> { if (updateChecker.isUpdateAvailable(latestVersion)) { player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| A new update is available:](#00fb9a) [HuskSync " + updateChecker.fetchLatestVersion() + "](#00fb9a bold)" + @@ -57,7 +57,7 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons case "migrate" -> plugin.getLocales().getLocale("error_console_command_only").ifPresent(player::sendMessage); default -> plugin.getLocales().getLocale("error_invalid_syntax", - "/husksync ") + "/husksync ") .ifPresent(player::sendMessage); } } @@ -65,14 +65,14 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons @Override public void onConsoleExecute(@NotNull String[] args) { if (args.length < 1) { - plugin.getLoggingAdapter().log(Level.INFO, "Console usage: \"husksync \""); + plugin.getLoggingAdapter().log(Level.INFO, "Console usage: \"husksync \""); return; } switch (args[0].toLowerCase()) { case "update", "version" -> - new UpdateChecker(plugin.getVersion(), plugin.getLoggingAdapter()).logToConsole(); + new UpdateChecker(plugin.getPluginVersion(), plugin.getLoggingAdapter()).logToConsole(); case "info", "about" -> plugin.getLoggingAdapter().log(Level.INFO, plugin.getLocales().stripMineDown( - Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getVersion()))); + Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getPluginVersion().toString()))); case "reload" -> { plugin.reload(); plugin.getLoggingAdapter().log(Level.INFO, "Reloaded config & message files."); @@ -105,7 +105,7 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons }); } default -> plugin.getLoggingAdapter().log(Level.INFO, - "Invalid syntax. Console usage: \"husksync \""); + "Invalid syntax. Console usage: \"husksync \""); } } @@ -129,6 +129,6 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage); return; } - player.sendMessage(new MineDown(Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getVersion()))); + player.sendMessage(new MineDown(Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getPluginVersion().toString()))); } } 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 9237b9e3..3df178be 100644 --- a/common/src/main/java/net/william278/husksync/command/InventoryCommand.java +++ b/common/src/main/java/net/william278/husksync/command/InventoryCommand.java @@ -4,7 +4,7 @@ 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.UserDataSnapshot; import net.william278.husksync.editor.ItemEditorMenu; import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.User; @@ -55,15 +55,15 @@ public class InventoryCommand extends CommandBase implements TabCompletable { .ifPresent(player::sendMessage))); } - private void showInventoryMenu(@NotNull OnlineUser player, @NotNull VersionedUserData versionedUserData, + private void showInventoryMenu(@NotNull OnlineUser player, @NotNull UserDataSnapshot userDataSnapshot, @NotNull User dataOwner, boolean allowEdit) { CompletableFuture.runAsync(() -> { - final UserData data = versionedUserData.userData(); + final UserData data = userDataSnapshot.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())) + .format(userDataSnapshot.versionTimestamp())) .ifPresent(player::sendMessage); final ItemData inventoryDataOnClose = plugin.getDataEditor().openItemEditorMenu(player, menu).join(); if (!menu.canEdit) { @@ -72,7 +72,8 @@ public class InventoryCommand extends CommandBase implements TabCompletable { final UserData updatedUserData = new UserData(data.getStatusData(), inventoryDataOnClose, data.getEnderChestData(), data.getPotionEffectsData(), data.getAdvancementData(), data.getStatisticsData(), data.getLocationData(), - data.getPersistentDataContainerData()); + data.getPersistentDataContainerData(), + plugin.getMinecraftVersion().getWithoutMeta()); 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/UserDataCommand.java b/common/src/main/java/net/william278/husksync/command/UserDataCommand.java index e1ce5adc..f8cce605 100644 --- a/common/src/main/java/net/william278/husksync/command/UserDataCommand.java +++ b/common/src/main/java/net/william278/husksync/command/UserDataCommand.java @@ -14,7 +14,7 @@ import java.util.stream.Collectors; public class UserDataCommand extends CommandBase implements TabCompletable { - private final String[] COMMAND_ARGUMENTS = {"view", "list", "delete", "restore"}; + private final String[] COMMAND_ARGUMENTS = {"view", "list", "delete", "restore", "pin"}; public UserDataCommand(@NotNull HuskSync implementor) { super("userdata", Permission.COMMAND_USER_DATA, implementor, "playerdata"); @@ -24,7 +24,7 @@ public class UserDataCommand extends CommandBase implements TabCompletable { public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) { if (args.length < 1) { plugin.getLocales().getLocale("error_invalid_syntax", - "/userdata [version_uuid]") + "/userdata [version_uuid]") .ifPresent(player::sendMessage); return; } @@ -160,6 +160,47 @@ public class UserDataCommand extends CommandBase implements TabCompletable { .ifPresent(player::sendMessage); } } + case "pin" -> { + if (args.length < 3) { + plugin.getLocales().getLocale("error_invalid_syntax", + "/userdata pin ") + .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( + optionalUserData -> optionalUserData.ifPresentOrElse(userData -> { + if (userData.pinned()) { + plugin.getDatabase().unpinUserData(user, versionUuid).join(); + plugin.getLocales().getLocale("data_unpinned", + versionUuid.toString().split("-")[0], + versionUuid.toString(), + user.username, + user.uuid.toString()) + .ifPresent(player::sendMessage); + } else { + plugin.getDatabase().pinUserData(user, versionUuid).join(); + plugin.getLocales().getLocale("data_pinned", + versionUuid.toString().split("-")[0], + versionUuid.toString(), + user.username, + user.uuid.toString()) + .ifPresent(player::sendMessage); + } + }, () -> 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 pin ") + .ifPresent(player::sendMessage); + } + } } } diff --git a/common/src/main/java/net/william278/husksync/config/Settings.java b/common/src/main/java/net/william278/husksync/config/Settings.java index 32f5412e..3836bd4f 100644 --- a/common/src/main/java/net/william278/husksync/config/Settings.java +++ b/common/src/main/java/net/william278/husksync/config/Settings.java @@ -119,7 +119,7 @@ public class Settings { CHECK_FOR_UPDATES("check_for_updates", OptionType.BOOLEAN, true), CLUSTER_ID("cluster_id", OptionType.STRING, ""), - DEBUG_LOGGING("debug_logging", OptionType.BOOLEAN, true), + DEBUG_LOGGING("debug_logging", OptionType.BOOLEAN, false), DATABASE_HOST("database.credentials.host", OptionType.STRING, "localhost"), DATABASE_PORT("database.credentials.port", OptionType.INTEGER, 3306), diff --git a/common/src/main/java/net/william278/husksync/data/UserData.java b/common/src/main/java/net/william278/husksync/data/UserData.java index 593b9007..33ff08f6 100644 --- a/common/src/main/java/net/william278/husksync/data/UserData.java +++ b/common/src/main/java/net/william278/husksync/data/UserData.java @@ -65,6 +65,12 @@ public class UserData { @SerializedName("persistent_data_container") protected PersistentDataContainerData persistentDataContainerData; + /** + * Stores the version of Minecraft this data was generated in + */ + @SerializedName("minecraft_version") + protected String minecraftVersion; + /** * Stores the version of the data format being used */ @@ -74,7 +80,8 @@ public class UserData { public UserData(@NotNull StatusData statusData, @NotNull ItemData inventoryData, @NotNull ItemData enderChestData, @NotNull PotionEffectData potionEffectData, @NotNull List advancementData, @NotNull StatisticsData statisticData, - @NotNull LocationData locationData, @NotNull PersistentDataContainerData persistentDataContainerData) { + @NotNull LocationData locationData, @NotNull PersistentDataContainerData persistentDataContainerData, + @NotNull String minecraftVersion) { this.statusData = statusData; this.inventoryData = inventoryData; this.enderChestData = enderChestData; @@ -83,6 +90,7 @@ public class UserData { this.statisticData = statisticData; this.locationData = locationData; this.persistentDataContainerData = persistentDataContainerData; + this.minecraftVersion = minecraftVersion; this.formatVersion = CURRENT_FORMAT_VERSION; } @@ -123,4 +131,9 @@ public class UserData { return persistentDataContainerData; } + @NotNull + public String getMinecraftVersion() { + return minecraftVersion; + } + } diff --git a/common/src/main/java/net/william278/husksync/data/VersionedUserData.java b/common/src/main/java/net/william278/husksync/data/UserDataSnapshot.java similarity index 64% rename from common/src/main/java/net/william278/husksync/data/VersionedUserData.java rename to common/src/main/java/net/william278/husksync/data/UserDataSnapshot.java index 00ebab88..1e1ba6ab 100644 --- a/common/src/main/java/net/william278/husksync/data/VersionedUserData.java +++ b/common/src/main/java/net/william278/husksync/data/UserDataSnapshot.java @@ -13,21 +13,23 @@ import java.util.UUID; * @param userData The {@link UserData} that has been versioned * @param cause The {@link DataSaveCause} that caused this data to be saved */ -public record VersionedUserData(@NotNull UUID versionUUID, @NotNull Date versionTimestamp, - @NotNull DataSaveCause cause, @NotNull UserData userData) implements Comparable { +public record UserDataSnapshot(@NotNull UUID versionUUID, @NotNull Date versionTimestamp, + @NotNull DataSaveCause cause, boolean pinned, + @NotNull UserData userData) implements Comparable { /** - * Version {@link UserData} into a {@link VersionedUserData}, assigning it a random {@link UUID} and the current timestamp {@link Date} + * Version {@link UserData} into a {@link UserDataSnapshot}, assigning it a random {@link UUID} and the current timestamp {@link Date} *

* Note that this method will set {@code cause} to {@link DataSaveCause#API} * * @param userData The {@link UserData} to version - * @return A new {@link VersionedUserData} + * @return A new {@link UserDataSnapshot} * @implNote This isn't used to version data that is going to be set to a database to prevent UUID collisions.

* Database implementations should instead use their own UUID generation functions. */ - public static VersionedUserData version(@NotNull UserData userData) { - return new VersionedUserData(UUID.randomUUID(), new Date(), DataSaveCause.API, userData); + public static UserDataSnapshot create(@NotNull UserData userData) { + return new UserDataSnapshot(UUID.randomUUID(), new Date(), + DataSaveCause.API, false, userData); } /** @@ -37,7 +39,7 @@ public record VersionedUserData(@NotNull UUID versionUUID, @NotNull Date version * @return the comparison result; the more recent UserData is greater than the less recent UserData */ @Override - public int compareTo(@NotNull VersionedUserData other) { + public int compareTo(@NotNull UserDataSnapshot other) { return Long.compare(this.versionTimestamp.getTime(), other.versionTimestamp.getTime()); } 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 d639c049..59857130 100644 --- a/common/src/main/java/net/william278/husksync/database/Database.java +++ b/common/src/main/java/net/william278/husksync/database/Database.java @@ -3,7 +3,7 @@ package net.william278.husksync.database; import net.william278.husksync.data.DataAdapter; import net.william278.husksync.data.DataSaveCause; import net.william278.husksync.data.UserData; -import net.william278.husksync.data.VersionedUserData; +import net.william278.husksync.data.UserDataSnapshot; import net.william278.husksync.event.EventCannon; import net.william278.husksync.migrator.Migrator; import net.william278.husksync.player.User; @@ -157,40 +157,41 @@ public abstract class Database { * Get the current uniquely versioned user data for a given user, if it exists. * * @param user the user to get data for - * @return an optional containing the {@link VersionedUserData}, if it exists, or an empty optional if it does not + * @return an optional containing the {@link UserDataSnapshot}, if it exists, or an empty optional if it does not */ - public abstract CompletableFuture> getCurrentUserData(@NotNull User user); + public abstract CompletableFuture> getCurrentUserData(@NotNull User user); /** - * Get all {@link VersionedUserData} entries for a user from the database. + * Get all {@link UserDataSnapshot} entries for a user from the database. * * @param user The user to get data for - * @return A future returning a list of a user's {@link VersionedUserData} entries + * @return A future returning a list of a user's {@link UserDataSnapshot} entries */ - public abstract CompletableFuture> getUserData(@NotNull User user); + public abstract CompletableFuture> getUserData(@NotNull User user); /** - * Gets a specific {@link VersionedUserData} entry for a user from the database, by its UUID. + * Gets a specific {@link UserDataSnapshot} 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 + * @param versionUuid The UUID of the {@link UserDataSnapshot} entry to get + * @return A future returning an optional containing the {@link UserDataSnapshot}, if it exists, or an empty optional if it does not */ - public abstract CompletableFuture> getUserData(@NotNull User user, @NotNull UUID versionUuid); + public abstract CompletableFuture> getUserData(@NotNull User user, @NotNull UUID versionUuid); /** - * (Internal) Prune user data for a given user to the maximum value as configured + * (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 + * @implNote Data snapshots marked as {@code pinned} are exempt from rotation */ - protected abstract CompletableFuture pruneUserData(@NotNull User user); + protected abstract CompletableFuture rotateUserData(@NotNull User user); /** - * Deletes a specific {@link VersionedUserData} entry for a user from the database, by its UUID. + * Deletes a specific {@link UserDataSnapshot} 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 + * @param versionUuid The UUID of the {@link UserDataSnapshot} entry to delete * @return A future returning void when complete */ public abstract CompletableFuture deleteUserData(@NotNull User user, @NotNull UUID versionUuid); @@ -202,10 +203,30 @@ public abstract class Database { * @param user The user to add data for * @param userData The {@link UserData} to set. The implementation should version it with a random UUID and the current timestamp during insertion. * @return A future returning void when complete - * @see VersionedUserData#version(UserData) + * @see UserDataSnapshot#create(UserData) */ public abstract CompletableFuture setUserData(@NotNull User user, @NotNull UserData userData, @NotNull DataSaveCause dataSaveCause); + /** + * Pin a saved {@link UserDataSnapshot} by given version UUID, setting it's {@code pinned} state to {@code true}. + * + * @param user The user to pin the data for + * @param versionUuid The UUID of the user's {@link UserDataSnapshot} entry to pin + * @return A future returning a boolean; {@code true} if the operation completed successfully, {@code false} if it failed + * @see UserDataSnapshot#pinned() + */ + public abstract CompletableFuture pinUserData(@NotNull User user, @NotNull UUID versionUuid); + + /** + * Unpin a saved {@link UserDataSnapshot} by given version UUID, setting it's {@code pinned} state to {@code false}. + * + * @param user The user to unpin the data for + * @param versionUuid The UUID of the user's {@link UserDataSnapshot} entry to unpin + * @return A future returning a boolean; {@code true} if the operation completed successfully, {@code false} if it failed + * @see UserDataSnapshot#pinned() + */ + public abstract CompletableFuture unpinUserData(@NotNull User user, @NotNull UUID versionUuid); + /** * Wipes all {@link UserData} entries from the database. * This should never be used, except when preparing tables for migration. 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 c0c53936..d2eeffc4 100644 --- a/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java +++ b/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java @@ -13,8 +13,8 @@ import org.jetbrains.annotations.NotNull; import java.io.ByteArrayInputStream; import java.io.IOException; import java.sql.*; -import java.util.*; import java.util.Date; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.logging.Level; @@ -206,11 +206,11 @@ public class MySqlDatabase extends Database { } @Override - public CompletableFuture> getCurrentUserData(@NotNull User user) { + public CompletableFuture> getCurrentUserData(@NotNull User user) { return CompletableFuture.supplyAsync(() -> { try (Connection connection = getConnection()) { try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" - SELECT `version_uuid`, `timestamp`, `save_cause`, `data` + SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data` FROM `%user_data_table%` WHERE `player_uuid`=? ORDER BY `timestamp` DESC @@ -221,10 +221,11 @@ public class MySqlDatabase extends Database { final Blob blob = resultSet.getBlob("data"); final byte[] dataByteArray = blob.getBytes(1, (int) blob.length()); blob.free(); - return Optional.of(new VersionedUserData( + return Optional.of(new UserDataSnapshot( UUID.fromString(resultSet.getString("version_uuid")), Date.from(resultSet.getTimestamp("timestamp").toInstant()), DataSaveCause.getCauseByName(resultSet.getString("save_cause")), + resultSet.getBoolean("pinned"), getDataAdapter().fromBytes(dataByteArray))); } } @@ -236,12 +237,12 @@ public class MySqlDatabase extends Database { } @Override - public CompletableFuture> getUserData(@NotNull User user) { + public CompletableFuture> getUserData(@NotNull User user) { return CompletableFuture.supplyAsync(() -> { - final List retrievedData = new ArrayList<>(); + final List retrievedData = new ArrayList<>(); try (Connection connection = getConnection()) { try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" - SELECT `version_uuid`, `timestamp`, `save_cause`, `data` + SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data` FROM `%user_data_table%` WHERE `player_uuid`=? ORDER BY `timestamp` DESC;"""))) { @@ -251,10 +252,11 @@ public class MySqlDatabase extends Database { final Blob blob = resultSet.getBlob("data"); final byte[] dataByteArray = blob.getBytes(1, (int) blob.length()); blob.free(); - final VersionedUserData data = new VersionedUserData( + final UserDataSnapshot data = new UserDataSnapshot( UUID.fromString(resultSet.getString("version_uuid")), Date.from(resultSet.getTimestamp("timestamp").toInstant()), DataSaveCause.getCauseByName(resultSet.getString("save_cause")), + resultSet.getBoolean("pinned"), getDataAdapter().fromBytes(dataByteArray)); retrievedData.add(data); } @@ -268,11 +270,11 @@ public class MySqlDatabase extends Database { } @Override - public CompletableFuture> getUserData(@NotNull User user, @NotNull UUID versionUuid) { + 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` + SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data` FROM `%user_data_table%` WHERE `player_uuid`=? AND `version_uuid`=? ORDER BY `timestamp` DESC @@ -284,10 +286,11 @@ public class MySqlDatabase extends Database { final Blob blob = resultSet.getBlob("data"); final byte[] dataByteArray = blob.getBytes(1, (int) blob.length()); blob.free(); - return Optional.of(new VersionedUserData( + return Optional.of(new UserDataSnapshot( UUID.fromString(resultSet.getString("version_uuid")), Date.from(resultSet.getTimestamp("timestamp").toInstant()), DataSaveCause.getCauseByName(resultSet.getString("save_cause")), + resultSet.getBoolean("pinned"), getDataAdapter().fromBytes(dataByteArray))); } } @@ -299,16 +302,19 @@ public class MySqlDatabase extends Database { } @Override - protected CompletableFuture pruneUserData(@NotNull User user) { - return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(data -> { - if (data.size() > maxUserDataRecords) { + protected CompletableFuture rotateUserData(@NotNull User user) { + return CompletableFuture.runAsync(() -> { + final List unpinnedUserData = getUserData(user).join().stream() + .filter(dataSnapshot -> !dataSnapshot.pinned()).toList(); + if (unpinnedUserData.size() > maxUserDataRecords) { try (Connection connection = getConnection()) { try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" DELETE FROM `%user_data_table%` WHERE `player_uuid`=? + AND `pinned` IS FALSE ORDER BY `timestamp` ASC LIMIT %entry_count%;""".replace("%entry_count%", - Integer.toString(data.size() - maxUserDataRecords))))) { + Integer.toString(unpinnedUserData.size() - maxUserDataRecords))))) { statement.setString(1, user.uuid.toString()); statement.executeUpdate(); } @@ -316,7 +322,7 @@ public class MySqlDatabase extends Database { getLogger().log(Level.SEVERE, "Failed to prune user data from the database", e); } } - })); + }); } @Override @@ -361,7 +367,45 @@ public class MySqlDatabase extends Database { getLogger().log(Level.SEVERE, "Failed to set user data in the database", e); } } - }).thenRun(() -> pruneUserData(user).join()); + }).thenRun(() -> rotateUserData(user).join()); + } + + @Override + public CompletableFuture pinUserData(@NotNull User user, @NotNull UUID versionUuid) { + return CompletableFuture.runAsync(() -> { + try (Connection connection = getConnection()) { + try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" + UPDATE `%user_data_table%` + SET `pinned`=TRUE + WHERE `player_uuid`=? AND `version_uuid`=? + LIMIT 1;"""))) { + statement.setString(1, user.uuid.toString()); + statement.setString(2, versionUuid.toString()); + statement.executeUpdate(); + } + } catch (SQLException e) { + getLogger().log(Level.SEVERE, "Failed to pin user data in the database", e); + } + }); + } + + @Override + public CompletableFuture unpinUserData(@NotNull User user, @NotNull UUID versionUuid) { + return CompletableFuture.runAsync(() -> { + try (Connection connection = getConnection()) { + try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" + UPDATE `%user_data_table%` + SET `pinned`=FALSE + WHERE `player_uuid`=? AND `version_uuid`=? + LIMIT 1;"""))) { + statement.setString(1, user.uuid.toString()); + statement.setString(2, versionUuid.toString()); + statement.executeUpdate(); + } + } catch (SQLException e) { + getLogger().log(Level.SEVERE, "Failed to unpin user data in the database", e); + } + }); } @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 2fb51f91..1179a29e 100644 --- a/common/src/main/java/net/william278/husksync/editor/DataEditor.java +++ b/common/src/main/java/net/william278/husksync/editor/DataEditor.java @@ -4,7 +4,7 @@ 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.data.UserDataSnapshot; import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.User; import org.jetbrains.annotations.NotNull; @@ -13,7 +13,6 @@ import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; /** * Provides methods for displaying and editing user data @@ -74,13 +73,13 @@ public class DataEditor { } /** - * Display a chat menu detailing information about {@link VersionedUserData} + * Display a chat menu detailing information about {@link UserDataSnapshot} * * @param user The online user to display the message to - * @param userData The {@link VersionedUserData} to display information about - * @param dataOwner The {@link User} who owns the {@link VersionedUserData} + * @param userData The {@link UserDataSnapshot} to display information about + * @param dataOwner The {@link User} who owns the {@link UserDataSnapshot} */ - public void displayDataOverview(@NotNull OnlineUser user, @NotNull VersionedUserData userData, + public void displayDataOverview(@NotNull OnlineUser user, @NotNull UserDataSnapshot userData, @NotNull User dataOwner) { locales.getLocale("data_manager_title", userData.versionUUID().toString().split("-")[0], @@ -91,6 +90,9 @@ public class DataEditor { locales.getLocale("data_manager_timestamp", new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss").format(userData.versionTimestamp())) .ifPresent(user::sendMessage); + if (userData.pinned()) { + locales.getLocale("data_manager_pinned").ifPresent(user::sendMessage); + } locales.getLocale("data_manager_cause", userData.cause().name().toLowerCase().replaceAll("_", " ")) .ifPresent(user::sendMessage); @@ -120,7 +122,8 @@ public class DataEditor { } } - private @NotNull String generateAdvancementPreview(@NotNull List advancementData) { + @NotNull + private String generateAdvancementPreview(@NotNull List advancementData) { final StringJoiner joiner = new StringJoiner("\n"); final List advancementsToPreview = advancementData.stream().filter(dataItem -> !dataItem.key.startsWith("minecraft:recipes/")).toList(); @@ -140,13 +143,13 @@ public class DataEditor { } /** - * Display a chat list detailing a player's saved list of {@link VersionedUserData} + * Display a chat list detailing a player's saved list of {@link UserDataSnapshot} * * @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} + * @param userDataList The list of {@link UserDataSnapshot} to display + * @param dataOwner The {@link User} who owns the {@link UserDataSnapshot} */ - public void displayDataList(@NotNull OnlineUser user, @NotNull List userDataList, + public void displayDataList(@NotNull OnlineUser user, @NotNull List userDataList, @NotNull User dataOwner) { locales.getLocale("data_list_title", dataOwner.username, dataOwner.uuid.toString()) @@ -154,7 +157,7 @@ public class DataEditor { final String[] numberedIcons = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳".split(""); for (int i = 0; i < Math.min(20, userDataList.size()); i++) { - final VersionedUserData userData = userDataList.get(i); + final UserDataSnapshot userData = userDataList.get(i); locales.getLocale("data_list_item", numberedIcons[i], DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault()) @@ -162,7 +165,8 @@ public class DataEditor { userData.versionUUID().toString().split("-")[0], userData.versionUUID().toString(), userData.cause().name().toLowerCase().replaceAll("_", " "), - dataOwner.username) + dataOwner.username, + userData.pinned() ? "※" : " ") .ifPresent(user::sendMessage); } } diff --git a/common/src/main/java/net/william278/husksync/hook/PlanDataExtension.java b/common/src/main/java/net/william278/husksync/hook/PlanDataExtension.java index fc1cbb06..5441233a 100644 --- a/common/src/main/java/net/william278/husksync/hook/PlanDataExtension.java +++ b/common/src/main/java/net/william278/husksync/hook/PlanDataExtension.java @@ -8,7 +8,7 @@ import com.djrapitops.plan.extension.annotation.PluginInfo; import com.djrapitops.plan.extension.annotation.StringProvider; import com.djrapitops.plan.extension.icon.Color; import com.djrapitops.plan.extension.icon.Family; -import net.william278.husksync.data.VersionedUserData; +import net.william278.husksync.data.UserDataSnapshot; import net.william278.husksync.database.Database; import net.william278.husksync.player.User; import org.jetbrains.annotations.NotNull; @@ -21,14 +21,16 @@ import java.util.regex.Pattern; @PluginInfo( name = "HuskSync", - iconName = "arrow-right-arrow-left", + iconName = "cube", iconFamily = Family.SOLID, - color = Color.LIGHT_BLUE + color = Color.NONE ) public class PlanDataExtension implements DataExtension { private Database database; + private static final String UNKNOWN_STRING = "N/A"; + //todo add more providers protected PlanDataExtension(@NotNull Database database) { this.database = database; @@ -45,7 +47,7 @@ public class PlanDataExtension implements DataExtension { }; } - private CompletableFuture> getCurrentUserData(@NotNull UUID uuid) { + private CompletableFuture> getCurrentUserData(@NotNull UUID uuid) { return CompletableFuture.supplyAsync(() -> { final Optional optionalUser = database.getUser(uuid).join(); if (optionalUser.isPresent()) { @@ -56,11 +58,11 @@ public class PlanDataExtension implements DataExtension { } @NumberProvider( - text = "Data Sync Time", + text = "Sync Time", description = "The last time the user had their data synced with the server.", iconName = "clock", iconFamily = Family.SOLID, - format = FormatType.TIME_MILLISECONDS, + format = FormatType.DATE_SECOND, priority = 1 ) public long getCurrentDataTimestamp(@NotNull UUID uuid) { @@ -70,7 +72,7 @@ public class PlanDataExtension implements DataExtension { } @StringProvider( - text = "Data Version ID", + text = "Version ID", description = "ID of the data version that the user is currently using.", iconName = "bolt", iconFamily = Family.SOLID, @@ -80,7 +82,7 @@ public class PlanDataExtension implements DataExtension { return getCurrentUserData(uuid).join().map( versionedUserData -> versionedUserData.versionUUID().toString() .split(Pattern.quote("-"))[0]) - .orElse("unknown"); + .orElse(UNKNOWN_STRING); } @NumberProvider( diff --git a/common/src/main/java/net/william278/husksync/listener/EventListener.java b/common/src/main/java/net/william278/husksync/listener/EventListener.java index 1273ab32..3e9187fa 100644 --- a/common/src/main/java/net/william278/husksync/listener/EventListener.java +++ b/common/src/main/java/net/william278/husksync/listener/EventListener.java @@ -4,7 +4,6 @@ import net.william278.husksync.HuskSync; import net.william278.husksync.config.Settings; import net.william278.husksync.data.ItemData; import net.william278.husksync.data.DataSaveCause; -import net.william278.husksync.data.UserData; import net.william278.husksync.player.OnlineUser; import org.jetbrains.annotations.NotNull; @@ -23,7 +22,7 @@ public abstract class EventListener { /** * The plugin instance */ - protected final HuskSync huskSync; + protected final HuskSync plugin; /** * Set of UUIDs current awaiting item synchronization. Events will be cancelled for these users @@ -35,8 +34,8 @@ public abstract class EventListener { */ private boolean disabling; - protected EventListener(@NotNull HuskSync huskSync) { - this.huskSync = huskSync; + protected EventListener(@NotNull HuskSync plugin) { + this.plugin = plugin; this.usersAwaitingSync = new HashSet<>(); this.disabling = false; } @@ -49,18 +48,15 @@ public abstract class EventListener { CompletableFuture.runAsync(() -> { try { // Hold reading data for the network latency threshold, to ensure the source server has set the redis key - Thread.sleep(Math.min(0, huskSync.getSettings().getIntegerValue(Settings.ConfigOption.SYNCHRONIZATION_NETWORK_LATENCY_MILLISECONDS))); + Thread.sleep(Math.max(0, plugin.getSettings().getIntegerValue(Settings.ConfigOption.SYNCHRONIZATION_NETWORK_LATENCY_MILLISECONDS))); } catch (InterruptedException e) { - huskSync.getLoggingAdapter().log(Level.SEVERE, "An exception occurred handling a player join", e); + plugin.getLoggingAdapter().log(Level.SEVERE, "An exception occurred handling a player join", e); } finally { - huskSync.getRedisManager().getUserServerSwitch(user).thenAccept(changingServers -> { - huskSync.getLoggingAdapter().info("Handling server change check " + ((changingServers) ? "true" : "false")); + plugin.getRedisManager().getUserServerSwitch(user).thenAccept(changingServers -> { if (!changingServers) { - huskSync.getLoggingAdapter().info("User is not changing servers"); // Fetch from the database if the user isn't changing servers - setUserFromDatabase(user).thenRun(() -> handleSynchronisationCompletion(user)); + setUserFromDatabase(user).thenAccept(succeeded -> handleSynchronisationCompletion(user, succeeded)); } else { - huskSync.getLoggingAdapter().info("User is changing servers, setting from db"); final int TIME_OUT_MILLISECONDS = 3200; CompletableFuture.runAsync(() -> { final AtomicInteger currentMilliseconds = new AtomicInteger(0); @@ -70,20 +66,19 @@ public abstract class EventListener { executor.scheduleAtFixedRate(() -> { if (user.isOffline()) { executor.shutdown(); - huskSync.getLoggingAdapter().info("Cancelled sync, user gone offline!"); return; } if (disabling || currentMilliseconds.get() > TIME_OUT_MILLISECONDS) { executor.shutdown(); - setUserFromDatabase(user).thenRun(() -> handleSynchronisationCompletion(user)); - huskSync.getLoggingAdapter().info("Setting user from db as fallback"); + setUserFromDatabase(user) + .thenAccept(succeeded -> handleSynchronisationCompletion(user, succeeded)); return; } - huskSync.getRedisManager().getUserData(user).thenAccept(redisUserData -> + plugin.getRedisManager().getUserData(user).thenAccept(redisUserData -> redisUserData.ifPresent(redisData -> { - huskSync.getLoggingAdapter().info("Setting user from redis!"); - user.setData(redisData, huskSync.getSettings(), huskSync.getEventCannon()) - .thenRun(() -> handleSynchronisationCompletion(user)).join(); + user.setData(redisData, plugin.getSettings(), plugin.getEventCannon(), + plugin.getLoggingAdapter(), plugin.getMinecraftVersion()) + .thenAccept(succeeded -> handleSynchronisationCompletion(user, succeeded)).join(); executor.shutdown(); })).join(); currentMilliseconds.addAndGet(200); @@ -95,18 +90,39 @@ public abstract class EventListener { }); } - private CompletableFuture setUserFromDatabase(@NotNull OnlineUser user) { - return huskSync.getDatabase().getCurrentUserData(user) - .thenAccept(databaseUserData -> databaseUserData.ifPresent(databaseData -> - user.setData(databaseData.userData(), huskSync.getSettings(), - huskSync.getEventCannon()).join())); + /** + * Set a user's data from the database + * + * @param user The user to set the data for + * @return Whether the data was successfully set + */ + private CompletableFuture setUserFromDatabase(@NotNull OnlineUser user) { + return plugin.getDatabase().getCurrentUserData(user).thenApply(databaseUserData -> { + if (databaseUserData.isPresent()) { + return user.setData(databaseUserData.get().userData(), plugin.getSettings(), plugin.getEventCannon(), + plugin.getLoggingAdapter(), plugin.getMinecraftVersion()).join(); + } + return true; + }); } - private void handleSynchronisationCompletion(@NotNull OnlineUser user) { - huskSync.getLocales().getLocale("synchronisation_complete").ifPresent(user::sendActionBar); - usersAwaitingSync.remove(user.uuid); - huskSync.getDatabase().ensureUser(user).join(); - huskSync.getEventCannon().fireSyncCompleteEvent(user); + /** + * Handle a player's synchronization completion + * + * @param user The {@link OnlineUser} to handle + * @param succeeded Whether the synchronization succeeded + */ + private void handleSynchronisationCompletion(@NotNull OnlineUser user, boolean succeeded) { + if (succeeded) { + plugin.getLocales().getLocale("synchronisation_complete").ifPresent(user::sendActionBar); + usersAwaitingSync.remove(user.uuid); + plugin.getDatabase().ensureUser(user).join(); + plugin.getEventCannon().fireSyncCompleteEvent(user); + } else { + plugin.getLocales().getLocale("synchronisation_failed") + .ifPresent(user::sendMessage); + plugin.getDatabase().ensureUser(user).join(); + } } public final void handlePlayerQuit(@NotNull OnlineUser user) { @@ -118,42 +134,42 @@ public abstract class EventListener { if (usersAwaitingSync.contains(user.uuid)) { return; } - huskSync.getRedisManager().setUserServerSwitch(user).thenRun(() -> user.getUserData().thenAccept( - userData -> huskSync.getRedisManager().setUserData(user, userData).thenRun( - () -> huskSync.getDatabase().setUserData(user, userData, DataSaveCause.DISCONNECT).join()))); + plugin.getRedisManager().setUserServerSwitch(user).thenRun(() -> user.getUserData().thenAccept( + userData -> plugin.getRedisManager().setUserData(user, userData).thenRun( + () -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.DISCONNECT).join()))); usersAwaitingSync.remove(user.uuid); } public final void handleWorldSave(@NotNull List usersInWorld) { - if (disabling || !huskSync.getSettings().getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SAVE_ON_WORLD_SAVE)) { + if (disabling || !plugin.getSettings().getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SAVE_ON_WORLD_SAVE)) { return; } CompletableFuture.runAsync(() -> usersInWorld.forEach(user -> - huskSync.getDatabase().setUserData(user, user.getUserData().join(), DataSaveCause.WORLD_SAVE).join())); + plugin.getDatabase().setUserData(user, user.getUserData().join(), DataSaveCause.WORLD_SAVE).join())); } public final void handlePluginDisable() { disabling = true; - huskSync.getOnlineUsers().stream().filter(user -> !usersAwaitingSync.contains(user.uuid)).forEach(user -> - huskSync.getDatabase().setUserData(user, user.getUserData().join(), DataSaveCause.SERVER_SHUTDOWN).join()); + plugin.getOnlineUsers().stream().filter(user -> !usersAwaitingSync.contains(user.uuid)).forEach(user -> + plugin.getDatabase().setUserData(user, user.getUserData().join(), DataSaveCause.SERVER_SHUTDOWN).join()); - huskSync.getDatabase().close(); - huskSync.getRedisManager().close(); + plugin.getDatabase().close(); + plugin.getRedisManager().close(); } public final void handleMenuClose(@NotNull OnlineUser user, @NotNull ItemData menuInventory) { if (disabling) { return; } - huskSync.getDataEditor().closeInventoryMenu(user, menuInventory); + plugin.getDataEditor().closeInventoryMenu(user, menuInventory); } public final boolean cancelMenuClick(@NotNull OnlineUser user) { if (disabling) { return true; } - return huskSync.getDataEditor().cancelInventoryEdit(user); + return plugin.getDataEditor().cancelInventoryEdit(user); } public final boolean cancelPlayerEvent(@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 78b5ef30..4404216a 100644 --- a/common/src/main/java/net/william278/husksync/player/OnlineUser.java +++ b/common/src/main/java/net/william278/husksync/player/OnlineUser.java @@ -6,12 +6,16 @@ import net.william278.husksync.data.*; import net.william278.husksync.editor.ItemEditorMenu; import net.william278.husksync.event.EventCannon; import net.william278.husksync.event.PreSyncEvent; +import net.william278.husksync.util.Logger; +import net.william278.husksync.util.Version; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; /** * Represents a logged-in {@link User} @@ -159,16 +163,32 @@ public abstract class OnlineUser extends User { */ public abstract boolean isOffline(); + /** + * Returns the implementing Minecraft server version + * + * @return The Minecraft server version + */ + @NotNull + public abstract Version getMinecraftVersion(); + /** * Set {@link UserData} to a player * * @param data The data to set * @param settings Plugin settings, for determining what needs setting - * @return a future that will be completed when done + * @return a future returning a boolean when complete; if the sync was successful, the future will return {@code true} */ - public final CompletableFuture setData(@NotNull UserData data, @NotNull Settings settings, - @NotNull EventCannon eventCannon) { - return CompletableFuture.runAsync(() -> { + public final CompletableFuture setData(@NotNull UserData data, @NotNull Settings settings, + @NotNull EventCannon eventCannon, @NotNull Logger logger, + @NotNull Version serverMinecraftVersion) { + return CompletableFuture.supplyAsync(() -> { + // Prevent synchronizing newer versions of Minecraft + if (Version.minecraftVersion(data.getMinecraftVersion()).compareTo(serverMinecraftVersion) > 0) { + logger.log(Level.SEVERE, "Cannot set data for player " + username + " with Minecraft version \"" + + data.getMinecraftVersion() + "\" because it is newer than the server's version, \"" + + serverMinecraftVersion + "\""); + return false; + } final PreSyncEvent preSyncEvent = (PreSyncEvent) eventCannon.firePreSyncEvent(this, data).join(); final UserData finalData = preSyncEvent.getUserData(); final List> dataSetOperations = new ArrayList<>() {{ @@ -197,7 +217,14 @@ public abstract class OnlineUser extends User { } } }}; - CompletableFuture.allOf(dataSetOperations.toArray(new CompletableFuture[0])).join(); + // Apply operations in parallel, join when complete + return CompletableFuture.allOf(dataSetOperations.toArray(new CompletableFuture[0])).thenApply(unused -> true) + .exceptionally(exception -> { + // Handle synchronisation exceptions + logger.log(Level.SEVERE, "Failed to set data for player " + username + " (" + exception.getMessage() + ")"); + exception.printStackTrace(); + return false; + }).join(); }); } @@ -240,7 +267,8 @@ public abstract class OnlineUser extends User { return CompletableFuture.supplyAsync( () -> new UserData(getStatus().join(), getInventory().join(), getEnderChest().join(), getPotionEffects().join(), getAdvancements().join(), - getStatistics().join(), getLocation().join(), getPersistentDataContainer().join())); + getStatistics().join(), getLocation().join(), getPersistentDataContainer().join(), + getMinecraftVersion().getWithoutMeta())); } } 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 867d7c80..51ce6ab4 100644 --- a/common/src/main/java/net/william278/husksync/redis/RedisManager.java +++ b/common/src/main/java/net/william278/husksync/redis/RedisManager.java @@ -83,10 +83,16 @@ public class RedisManager { 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); + user.setData(userData, plugin.getSettings(), plugin.getEventCannon(), + plugin.getLoggingAdapter(), plugin.getMinecraftVersion()).thenAccept(succeeded -> { + if (succeeded) { + plugin.getLocales().getLocale("data_update_complete") + .ifPresent(user::sendActionBar); + plugin.getEventCannon().fireSyncCompleteEvent(user); + } else { + plugin.getLocales().getLocale("data_update_failed") + .ifPresent(user::sendMessage); + } }); }); } 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 7ce98f73..4d9f48ad 100644 --- a/common/src/main/java/net/william278/husksync/util/UpdateChecker.java +++ b/common/src/main/java/net/william278/husksync/util/UpdateChecker.java @@ -13,31 +13,31 @@ public class UpdateChecker { private final static int SPIGOT_PROJECT_ID = 97144; private final Logger logger; - private final VersionUtils.Version currentVersion; + private final Version currentVersion; - public UpdateChecker(@NotNull String currentVersion, @NotNull Logger logger) { - this.currentVersion = VersionUtils.Version.of(currentVersion); + public UpdateChecker(@NotNull Version currentVersion, @NotNull Logger logger) { + this.currentVersion = currentVersion; this.logger = logger; } - public CompletableFuture fetchLatestVersion() { + public CompletableFuture fetchLatestVersion() { return CompletableFuture.supplyAsync(() -> { try { final URL url = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + SPIGOT_PROJECT_ID); URLConnection urlConnection = url.openConnection(); - return VersionUtils.Version.of(new BufferedReader(new InputStreamReader(urlConnection.getInputStream())).readLine()); + return Version.pluginVersion(new BufferedReader(new InputStreamReader(urlConnection.getInputStream())).readLine()); } catch (Exception e) { logger.log(Level.WARNING, "Failed to fetch the latest plugin version", e); } - return new VersionUtils.Version(); + return new Version(); }); } - public boolean isUpdateAvailable(@NotNull VersionUtils.Version latestVersion) { + public boolean isUpdateAvailable(@NotNull Version latestVersion) { return latestVersion.compareTo(currentVersion) > 0; } - public VersionUtils.Version getCurrentVersion() { + public Version getCurrentVersion() { return currentVersion; } diff --git a/common/src/main/java/net/william278/husksync/util/Version.java b/common/src/main/java/net/william278/husksync/util/Version.java new file mode 100644 index 00000000..55e1317c --- /dev/null +++ b/common/src/main/java/net/william278/husksync/util/Version.java @@ -0,0 +1,84 @@ +package net.william278.husksync.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.StringJoiner; +import java.util.regex.Pattern; + +public class Version implements Comparable { + private final static String VERSION_SEPARATOR = "."; + private final static String MINECRAFT_META_SEPARATOR = "-"; + private final static String PLUGIN_META_SEPARATOR = "+"; + + private int[] versions = new int[]{}; + @NotNull + private String metadata = ""; + @NotNull + private String metaSeparator = ""; + + protected Version() { + } + + private Version(@NotNull String version, @NotNull String metaSeparator) { + this.parse(version, metaSeparator); + this.metaSeparator = metaSeparator; + } + + @NotNull + public static Version pluginVersion(@NotNull String versionString) { + return new Version(versionString, PLUGIN_META_SEPARATOR); + } + + @NotNull + public static Version minecraftVersion(@NotNull String versionString) { + return new Version(versionString, MINECRAFT_META_SEPARATOR); + } + + private void parse(@NotNull String version, @NotNull String metaSeparator) { + int metaIndex = version.indexOf(metaSeparator); + if (metaIndex > 0) { + this.metadata = version.substring(metaIndex + 1); + version = version.substring(0, metaIndex); + } + String[] versions = version.split(Pattern.quote(VERSION_SEPARATOR)); + this.versions = Arrays.stream(versions).mapToInt(Integer::parseInt).toArray(); + } + + @Override + public int compareTo(@NotNull Version other) { + int length = Math.max(this.versions.length, other.versions.length); + for (int i = 0; i < length; i++) { + int a = i < this.versions.length ? this.versions[i] : 0; + int b = i < other.versions.length ? other.versions[i] : 0; + + if (a < b) return -1; + if (a > b) return 1; + } + + return 0; + } + + @Override + public String toString() { + final StringJoiner joiner = new StringJoiner(VERSION_SEPARATOR); + for (int version : this.versions) { + joiner.add(String.valueOf(version)); + } + return joiner + this.metaSeparator + this.metadata; + } + + @NotNull + public String getWithoutMeta() { + final StringJoiner joiner = new StringJoiner(VERSION_SEPARATOR); + for (int version : this.versions) { + joiner.add(String.valueOf(version)); + } + return joiner.toString(); + } + + @NotNull + public String getMetadata() { + return this.metadata; + } +} diff --git a/common/src/main/java/net/william278/husksync/util/VersionUtils.java b/common/src/main/java/net/william278/husksync/util/VersionUtils.java deleted file mode 100644 index 2081ed2c..00000000 --- a/common/src/main/java/net/william278/husksync/util/VersionUtils.java +++ /dev/null @@ -1,61 +0,0 @@ -package net.william278.husksync.util; - -import java.util.Arrays; - -public class VersionUtils { - - private final static char META_SEPARATOR = '+'; - private final static String VERSION_SEPARATOR = "\\."; - - - public static class Version implements Comparable { - public int[] versions = new int[]{}; - public String metadata = ""; - - public Version() { - } - - public Version(String version) { - this.parse(version); - } - - public static Version of(String version) { - return new Version(version); - } - - private void parse(String version) { - int metaIndex = version.indexOf(META_SEPARATOR); - if (metaIndex > 0) { - this.metadata = version.substring(metaIndex + 1); - version = version.substring(0, metaIndex); - } - String[] versions = version.split(VERSION_SEPARATOR); - this.versions = Arrays.stream(versions).mapToInt(Integer::parseInt).toArray(); - } - - @Override - public int compareTo(Version version) { - int length = Math.max(this.versions.length, version.versions.length); - for (int i = 0; i < length; i++) { - int a = i < this.versions.length ? this.versions[i] : 0; - int b = i < version.versions.length ? version.versions[i] : 0; - - if (a < b) return -1; - if (a > b) return 1; - } - - return 0; - } - - @Override - public String toString() { - StringBuilder stringBuffer = new StringBuilder(); - for (int version : this.versions) { - stringBuffer.append(version).append('.'); - } - stringBuffer.deleteCharAt(stringBuffer.length() - 1); - return stringBuffer.append('+').append(this.metadata).toString(); - } - } - -} \ No newline at end of file diff --git a/common/src/main/resources/config.yml b/common/src/main/resources/config.yml index 82b17e4b..7914f761 100644 --- a/common/src/main/resources/config.yml +++ b/common/src/main/resources/config.yml @@ -7,7 +7,7 @@ language: 'en-gb' check_for_updates: true cluster_id: '' -debug_logging: true +debug_logging: false database: credentials: diff --git a/common/src/main/resources/database/mysql_schema.sql b/common/src/main/resources/database/mysql_schema.sql index e3b60c6f..459bfd69 100644 --- a/common/src/main/resources/database/mysql_schema.sql +++ b/common/src/main/resources/database/mysql_schema.sql @@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS `%users_table%` CREATE TABLE IF NOT EXISTS `%user_data_table%` ( `version_uuid` char(36) NOT NULL UNIQUE, - `player_uuid` char(36) NOT NULL UNIQUE, + `player_uuid` char(36) NOT NULL, `timestamp` datetime NOT NULL, `save_cause` varchar(32) NOT NULL, `pinned` boolean NOT NULL DEFAULT FALSE, diff --git a/common/src/main/resources/locales/en-gb.yml b/common/src/main/resources/locales/en-gb.yml index 6ca00306..1b250f15 100644 --- a/common/src/main/resources/locales/en-gb.yml +++ b/common/src/main/resources/locales/en-gb.yml @@ -1,4 +1,5 @@ synchronisation_complete: '[⏵ Data synchronised!](#00fb9a)' +synchronisation_failed: '[⏵ Failed to synchronise your data! Please contact an administrator.](#ff7e5e)' 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 a player by that name.](#ff7e5e)' @@ -12,15 +13,19 @@ 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_update_failed: '[🔔 Failed to update your data! Please contact an administrator.](#ff7e5e)' 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_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\\n&8When the data was saved)' +data_manager_pinned: '[※ Snapshot pinned](#d8ff2b show_text=&7Pinned:\\n&8This user data snapshot won''t be automatically rotated.)' +data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\\n&8What 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_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements you have progress in:\\n&8%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\\n&8⚠ 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%)\\n' -data_manager_management_buttons: '[Manage:](gray) [[❌ Delete…]](#ff3300 show_text=&7Click to delete this snapshot of user data.\\nThis will not affect the user''s current data.\\n&#ff3300&⚠ This cannot be undone! run_command=/userdata delete %1% %2%) [[⏪ Restore…]](#00fb9a show_text=&7Click to restore this user data.\\nThis will set the user''s data to this snapshot.\\n&#ff3300&⚠ %1%''s current data will be overwritten! run_command=/userdata restore %1% %2%)\\n' +data_manager_management_buttons: '[Manage:](gray) [[❌ Delete…]](#ff3300 show_text=&7Click to delete this snapshot of user data.\\n&8This will not affect the user''s current data.\\n&#ff3300&⚠ This cannot be undone! run_command=/userdata delete %1% %2%) [[⏪ Restore…]](#00fb9a show_text=&7Click to restore this user data.\\n&8This will set the user''s data to this snapshot.\\n&#ff3300&⚠ %1%''s current data will be overwritten! run_command=/userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click to pin or unpin this user data snapshot\\n&8Pinned snapshots won''t be automatically rotated run_command=/userdata pin %1% %2%)\\n' data_manager_advancements_preview_remaining: '&7and %1% more…' data_list_title: '[List of](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)[''s user data snapshots:](#00fb9a)\\n' -data_list_item: '[%1%](gray show_text=&7Snapshot %3% run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Version timestamp&7\\n&8When the data was saved run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Version UUID:&7\\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Save cause\\n&8What 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 +data_list_item: '[%1%](gray show_text=&7Data snapshot %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Pinned:\\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\\n&8When the data was saved run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Version UUID:&7\\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Save cause:\\n&8What 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&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)' +data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\\n&8%4%)' +data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)' +data_unpinned: '[※ Successfully unpinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)' \ No newline at end of file