diff --git a/api/build.gradle b/api/build.gradle index 95e32229..b8dd39f9 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -13,10 +13,9 @@ shadowJar { relocate 'org.jetbrains', 'net.william278.husksync.libraries' relocate 'org.intellij', 'net.william278.husksync.libraries' relocate 'com.zaxxer', 'net.william278.husksync.libraries' - relocate 'org.slf4j', 'net.william278.husksync.libraries.slf4j' relocate 'com.google', 'net.william278.husksync.libraries' - //relocate 'org.xerial', 'net.william278.husksync.libraries' relocate 'redis.clients', 'net.william278.husksync.libraries' + relocate 'org.json', 'net.william278.husksync.libraries.json' relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby' relocate 'org.bstats', 'net.william278.husksync.libraries.bstats' diff --git a/bukkit/build.gradle b/bukkit/build.gradle index 3e1356d0..5323faf5 100644 --- a/bukkit/build.gradle +++ b/bukkit/build.gradle @@ -1,28 +1,25 @@ dependencies { implementation project(path: ':common') - implementation 'org.bstats:bstats-bukkit:3.0.0' implementation 'net.william278:mpdbdataconverter:1.0' - implementation 'net.byteflux:libby-bukkit:1.1.5' compileOnly 'redis.clients:jedis:4.2.3' compileOnly 'commons-io:commons-io:2.11.0' compileOnly 'de.themoep:minedown:1.7.1-SNAPSHOT' compileOnly 'dev.dejvokep:boosted-yaml:1.2' compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT' + compileOnly 'com.zaxxer:HikariCP:5.0.1' } shadowJar { relocate 'org.apache', 'net.william278.husksync.libraries' - relocate 'dev.dejvokep', 'net.william278.husksync.libraries' relocate 'de.themoep', 'net.william278.husksync.libraries' relocate 'org.jetbrains', 'net.william278.husksync.libraries' relocate 'org.intellij', 'net.william278.husksync.libraries' relocate 'com.zaxxer', 'net.william278.husksync.libraries' - relocate 'org.slf4j', 'net.william278.husksync.libraries.slf4j' relocate 'com.google', 'net.william278.husksync.libraries' - //relocate 'org.xerial', 'net.william278.husksync.libraries' relocate 'redis.clients', 'net.william278.husksync.libraries' + relocate 'org.json', 'net.william278.husksync.libraries.json' relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby' relocate 'org.bstats', 'net.william278.husksync.libraries.bstats' diff --git a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java index e727d59f..7f8c205d 100644 --- a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java +++ b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java @@ -6,7 +6,9 @@ import dev.dejvokep.boostedyaml.settings.dumper.DumperSettings; import dev.dejvokep.boostedyaml.settings.general.GeneralSettings; import dev.dejvokep.boostedyaml.settings.loader.LoaderSettings; import dev.dejvokep.boostedyaml.settings.updater.UpdaterSettings; -import net.william278.husksync.command.*; +import net.william278.husksync.command.BukkitCommand; +import net.william278.husksync.command.BukkitCommandType; +import net.william278.husksync.command.Permission; import net.william278.husksync.config.Locales; import net.william278.husksync.config.Settings; import net.william278.husksync.data.CompressedDataAdapter; @@ -19,6 +21,9 @@ import net.william278.husksync.event.BukkitEventCannon; import net.william278.husksync.event.EventCannon; import net.william278.husksync.listener.BukkitEventListener; import net.william278.husksync.listener.EventListener; +import net.william278.husksync.migrator.LegacyMigrator; +import net.william278.husksync.migrator.Migrator; +import net.william278.husksync.migrator.MpdbMigrator; import net.william278.husksync.player.BukkitPlayer; import net.william278.husksync.player.OnlineUser; import net.william278.husksync.redis.RedisManager; @@ -27,6 +32,7 @@ import org.bukkit.Bukkit; import org.bukkit.command.PluginCommand; import org.bukkit.entity.Player; import org.bukkit.permissions.PermissionDefault; +import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; @@ -40,24 +46,16 @@ import java.util.stream.Collectors; public class BukkitHuskSync extends JavaPlugin implements HuskSync { private Database database; - private RedisManager redisManager; - private Logger logger; - private ResourceReader resourceReader; - private EventListener eventListener; - private DataAdapter dataAdapter; - private DataEditor dataEditor; - private EventCannon eventCannon; private Settings settings; - private Locales locales; - + private List availableMigrators; private static BukkitHuskSync instance; /** @@ -72,18 +70,6 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { @Override public void onLoad() { instance = this; - /*getLogger().log(Level.INFO, "Loading runtime libraries..."); - final BukkitLibraryManager libraryManager = new BukkitLibraryManager(this); - final Library[] libraries = new Library[]{ - Library.builder().groupId("redis{}clients") - .artifactId("jedis") - .version("4.2.3") - .id("jedis") - .build() - }; - libraryManager.addMavenCentral(); - Arrays.stream(libraries).forEach(libraryManager::loadLibrary); - getLogger().log(Level.INFO, "Successfully loaded runtime libraries.");*/ } @Override @@ -127,6 +113,17 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { dataEditor = new DataEditor(locales); } return succeeded; + }).thenApply(succeeded -> { + // Prepare migrators + if (succeeded) { + availableMigrators = new ArrayList<>(); + availableMigrators.add(new LegacyMigrator(this)); + final Plugin mySqlPlayerDataBridge = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge"); + if (mySqlPlayerDataBridge != null) { + availableMigrators.add(new MpdbMigrator(this, mySqlPlayerDataBridge)); + } + } + return succeeded; }).thenApply(succeeded -> { // Establish connection to the database if (succeeded) { @@ -254,6 +251,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { return eventCannon; } + @NotNull + @Override + public List getAvailableMigrators() { + return null; + } + @Override public @NotNull Settings getSettings() { return settings; diff --git a/bukkit/src/main/java/net/william278/husksync/command/BukkitCommand.java b/bukkit/src/main/java/net/william278/husksync/command/BukkitCommand.java index 0a6962d2..ab64b8b1 100644 --- a/bukkit/src/main/java/net/william278/husksync/command/BukkitCommand.java +++ b/bukkit/src/main/java/net/william278/husksync/command/BukkitCommand.java @@ -48,10 +48,10 @@ public class BukkitCommand implements CommandExecutor, TabExecutor { if (sender instanceof Player player) { this.command.onExecute(BukkitPlayer.adapt(player), args); } else { - if (command instanceof ConsoleExecutable consoleExecutable) { + if (this.command instanceof ConsoleExecutable consoleExecutable) { consoleExecutable.onConsoleExecute(args); } else { - plugin.getLocales().getLocale("error_in_game_only"). + plugin.getLocales().getLocale("error_in_game_command_only"). ifPresent(locale -> sender.spigot().sendMessage(locale.toComponent())); } } diff --git a/bukkit/src/main/java/net/william278/husksync/migrator/MpdbMigrator.java b/bukkit/src/main/java/net/william278/husksync/migrator/MpdbMigrator.java new file mode 100644 index 00000000..444bdd65 --- /dev/null +++ b/bukkit/src/main/java/net/william278/husksync/migrator/MpdbMigrator.java @@ -0,0 +1,243 @@ +package net.william278.husksync.migrator; + +import com.zaxxer.hikari.HikariDataSource; +import net.william278.husksync.BukkitHuskSync; +import net.william278.husksync.config.Settings; +import net.william278.husksync.data.*; +import net.william278.husksync.player.User; +import net.william278.mpdbconverter.MPDBConverter; +import org.bukkit.Bukkit; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +import java.util.regex.Pattern; + +public class MpdbMigrator extends Migrator { + + private final MPDBConverter mpdbConverter; + private String sourceHost; + private int sourcePort; + private String sourceUsername; + private String sourcePassword; + private String sourceDatabase; + private String sourceInventoryTable; + private String sourceEnderChestTable; + private String sourceExperienceTable; + + public MpdbMigrator(@NotNull BukkitHuskSync plugin, @NotNull Plugin mySqlPlayerDataBridge) { + super(plugin); + this.mpdbConverter = MPDBConverter.getInstance(mySqlPlayerDataBridge); + this.sourceHost = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_HOST); + this.sourcePort = plugin.getSettings().getIntegerValue(Settings.ConfigOption.DATABASE_PORT); + this.sourceUsername = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_USERNAME); + this.sourcePassword = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_PASSWORD); + this.sourceDatabase = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_NAME); + this.sourceInventoryTable = "mpdb_inventory"; + this.sourceEnderChestTable = "mpdb_enderchest"; + this.sourceExperienceTable = "mpdb_experience"; + } + + @Override + public CompletableFuture start() { + plugin.getLoggingAdapter().log(Level.INFO, "Starting migration from MySQLPlayerDataBridge to HuskSync..."); + final long startTime = System.currentTimeMillis(); + return CompletableFuture.supplyAsync(() -> { + // Create jdbc driver connection url + final String jdbcUrl = "jdbc:mysql://" + sourceHost + ":" + sourcePort + "/" + sourceDatabase; + + // Create a new data source for the mpdb converter + try (final HikariDataSource connectionPool = new HikariDataSource()) { + plugin.getLoggingAdapter().log(Level.INFO, "Establishing connection to MySQLPlayerDataBridge database..."); + connectionPool.setJdbcUrl(jdbcUrl); + connectionPool.setUsername(sourceUsername); + connectionPool.setPassword(sourcePassword); + connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase()); + + plugin.getLoggingAdapter().log(Level.INFO, "Downloading raw data from the MySQLPlayerDataBridge database..."); + final List dataToMigrate = new ArrayList<>(); + try (final Connection connection = connectionPool.getConnection()) { + try (final PreparedStatement statement = connection.prepareStatement(""" + SELECT `player_uuid`, `player_name`, `inventory`, `armor`, `enderchest`, `exp_lvl`, `exp`, `total_exp` + FROM `%source_inventory_table%` + INNER JOIN `%source_ender_chest_table%` + ON `%source_inventory_table%`.`player_uuid` = `%source_ender_chest_table%`.`player_uuid` + INNER JOIN `%source_experience_table%` + ON `%source_inventory_table%`.`player_uuid` = `%source_experience_table%`.`player_uuid`; + """.replaceAll(Pattern.quote("%source_inventory_table%"), sourceInventoryTable).replaceAll(Pattern.quote("%source_ender_chest_table%"), sourceEnderChestTable).replaceAll(Pattern.quote("%source_experience_table%"), sourceExperienceTable))) { + try (final ResultSet resultSet = statement.executeQuery()) { + int playersMigrated = 0; + while (resultSet.next()) { + dataToMigrate.add(new MpdbData( + new User(UUID.fromString(resultSet.getString("player_uuid")), + resultSet.getString("player_name")), + resultSet.getString("inventory"), + resultSet.getString("armor"), + resultSet.getString("enderchest"), + resultSet.getInt("exp_lvl"), + resultSet.getInt("exp"), + resultSet.getInt("total_exp") + )); + playersMigrated++; + if (playersMigrated % 25 == 0) { + plugin.getLoggingAdapter().log(Level.INFO, "Downloaded MySQLPlayerDataBridge data for " + playersMigrated + " players..."); + } + } + } + } + } + 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 -> + plugin.getDatabase().ensureUser(data.user()).thenRun(() -> + plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.MPDB_MIGRATION)))); + plugin.getLoggingAdapter().log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!"); + return true; + } catch (Exception e) { + plugin.getLoggingAdapter().log(Level.SEVERE, "Error while migrating data: " + e.getMessage()); + return false; + } + }); + } + + @Override + public void handleConfigurationCommand(@NotNull String[] args) { + if (args.length == 2) { + if (switch (args[0].toLowerCase()) { + case "host" -> { + this.sourceHost = args[1]; + yield true; + } + case "port" -> { + try { + this.sourcePort = Integer.parseInt(args[1]); + yield true; + } catch (NumberFormatException e) { + yield false; + } + } + case "username" -> { + this.sourceUsername = args[1]; + yield true; + } + case "password" -> { + this.sourcePassword = args[1]; + yield true; + } + case "database" -> { + this.sourceDatabase = args[1]; + yield true; + } + case "inventory_table" -> { + this.sourceInventoryTable = args[1]; + yield true; + } + case "ender_chest_table" -> { + this.sourceEnderChestTable = args[1]; + yield true; + } + case "experience_table" -> { + this.sourceExperienceTable = args[1]; + yield true; + } + default -> false; + }) { + plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu()); + plugin.getLoggingAdapter().log(Level.INFO, "Successfully set " + args[0] + " to " + args[1]); + } else { + plugin.getLoggingAdapter().log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " + args[1] + " (is it a valid option?)"); + } + } + } + + @NotNull + @Override + public String getIdentifier() { + return "mpdb"; + } + + @NotNull + @Override + public String getName() { + return "MySQLPlayerDataBridge"; + } + + @NotNull + @Override + public String getHelpMenu() { + return """ + === MySQLPlayerDataBridge Migration Wizard ========== + This will migrate inventories, ender chests and XP + from the MySQLPlayerDataBridge plugin to HuskSync. + + To prevent excessive migration times, other non-vital + data will not be transferred. + + STEP 1] Please ensure no players are on the server. + + STEP 2] HuskSync will need to connect to the database + used to hold the source MySQLPlayerDataBridge data. + Please check these database parameters are OK: + - host: %source_host% + - port: %source_port% + - username: %source_username% + - password: %source_password% + - database: %source_database% + - inventory_table: %source_inventory_table% + - ender_chest_table: %source_ender_chest_table% + - experience_table: %source_xp_table% + If any of these are not correct, please correct them + using the command: + "husksync migrate mpdb set " + (e.g.: "husksync migrate mpdb set host 123.456.789") + + STEP 3] HuskSync will migrate data into the database + tables configures in the config.yml file of this + server. Please make sure you're happy with this + before proceeding. + + STEP 4] To start the migration, please run: + "husksync migrate mpdb start" + """; + } + + private record MpdbData(@NotNull User user, @NotNull String serializedInventory, + @NotNull String serializedArmor, @NotNull String serializedEnderChest, + int expLevel, float expProgress, int totalExp) { + @NotNull + public CompletableFuture toUserData(@NotNull MPDBConverter converter) { + return CompletableFuture.supplyAsync(() -> { + // Combine inventory and armour + final Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER); + inventory.setContents(converter.getItemStackFromSerializedData(serializedInventory)); + final ItemStack[] armor = converter.getItemStackFromSerializedData(serializedArmor).clone(); + for (int i = 36; i < 36 + armor.length; i++) { + inventory.setItem(i, armor[i - 36]); + } + + // Create user data record + return new UserData( + new StatusData(20, 20, 0, 20, 10, + 1, 0, totalExp, expLevel, expProgress, "SURVIVAL", + false), + new ItemData(BukkitSerializer.serializeItemStackArray(inventory.getContents()).join()), + new ItemData(BukkitSerializer.serializeItemStackArray(converter + .getItemStackFromSerializedData(serializedEnderChest)).join()), + new PotionEffectData(""), new ArrayList<>(), + 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<>())); + }); + } + } + +} diff --git a/bukkit/src/main/resources/plugin.yml b/bukkit/src/main/resources/plugin.yml index 7dedb160..b4b684e2 100644 --- a/bukkit/src/main/resources/plugin.yml +++ b/bukkit/src/main/resources/plugin.yml @@ -8,6 +8,8 @@ website: 'https://william278.net' softdepend: [ MysqlPlayerDataBridge ] libraries: - 'mysql:mysql-connector-java:8.0.29' + - 'org.xerial.snappy:snappy-java:1.1.8.4' + - 'dev.dejvokep:boosted-yaml:1.2' commands: husksync: usage: '/husksync ' diff --git a/common/build.gradle b/common/build.gradle index 9e39baf9..acd05469 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -1,24 +1,26 @@ dependencies { implementation 'commons-io:commons-io:2.11.0' - implementation 'dev.dejvokep:boosted-yaml:1.2' implementation 'de.themoep:minedown:1.7.1-SNAPSHOT' - implementation 'com.zaxxer:HikariCP:5.0.1' implementation 'com.google.code.gson:gson:2.9.0' - implementation 'org.xerial.snappy:snappy-java:1.1.8.4' - implementation 'redis.clients:jedis:4.2.3' + implementation('redis.clients:jedis:4.2.3') { + exclude module: 'slf4j-api' + } + implementation ('com.zaxxer:HikariCP:5.0.1') { + exclude module: 'slf4j-api' + } + compileOnly 'dev.dejvokep:boosted-yaml:1.2' + compileOnly 'org.xerial.snappy:snappy-java:1.1.8.4' compileOnly 'org.jetbrains:annotations:23.0.0' } shadowJar { relocate 'org.apache', 'net.william278.husksync.libraries' - relocate 'dev.dejvokep', 'net.william278.husksync.libraries' relocate 'de.themoep', 'net.william278.husksync.libraries' relocate 'org.jetbrains', 'net.william278.husksync.libraries' relocate 'org.intellij', 'net.william278.husksync.libraries' relocate 'com.zaxxer', 'net.william278.husksync.libraries' - relocate 'org.slf4j', 'net.william278.husksync.libraries.slf4j' relocate 'com.google', 'net.william278.husksync.libraries' - //relocate 'org.xerial', 'net.william278.husksync.libraries' relocate 'redis.clients', 'net.william278.husksync.libraries' + relocate 'org.json', 'net.william278.husksync.libraries.json' } \ No newline at end of file diff --git a/common/src/main/java/net/william278/husksync/HuskSync.java b/common/src/main/java/net/william278/husksync/HuskSync.java index 777be7aa..f51b77d5 100644 --- a/common/src/main/java/net/william278/husksync/HuskSync.java +++ b/common/src/main/java/net/william278/husksync/HuskSync.java @@ -6,11 +6,13 @@ import net.william278.husksync.data.DataAdapter; import net.william278.husksync.editor.DataEditor; import net.william278.husksync.database.Database; import net.william278.husksync.event.EventCannon; +import net.william278.husksync.migrator.Migrator; import net.william278.husksync.player.OnlineUser; import net.william278.husksync.redis.RedisManager; import net.william278.husksync.util.Logger; import org.jetbrains.annotations.NotNull; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -32,6 +34,8 @@ public interface HuskSync { @NotNull EventCannon getEventCannon(); + @NotNull List getAvailableMigrators(); + @NotNull Settings getSettings(); @NotNull Locales getLocales(); 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 5422aa82..d4614a9a 100644 --- a/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java +++ b/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java @@ -82,7 +82,7 @@ public class EnderChestCommand extends CommandBase implements TabCompletable { @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] : "")) + .filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : "")) .sorted().collect(Collectors.toList()); } 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 416f309b..29ab8da0 100644 --- a/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java +++ b/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java @@ -3,12 +3,14 @@ package net.william278.husksync.command; import de.themoep.minedown.MineDown; import net.william278.husksync.HuskSync; import net.william278.husksync.config.Locales; +import net.william278.husksync.migrator.Migrator; import net.william278.husksync.player.OnlineUser; import net.william278.husksync.util.UpdateChecker; import org.jetbrains.annotations.NotNull; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.logging.Level; import java.util.stream.Collectors; @@ -50,11 +52,13 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons return; } plugin.reload(); - player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| Reloaded config & message files.](#00fb9a)")); + plugin.getLocales().getLocale("reload_complete").ifPresent(player::sendMessage); } + case "migrate" -> + plugin.getLocales().getLocale("error_console_command_only").ifPresent(player::sendMessage); default -> plugin.getLocales().getLocale("error_invalid_syntax", "/husksync ") - .ifPresent(player::sendMessage); + .ifPresent(player::sendMessage); } } @@ -74,13 +78,46 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons plugin.getLoggingAdapter().log(Level.INFO, "Reloaded config & message files."); } case "migrate" -> { - //todo - MPDB migrator + if (args.length < 2) { + plugin.getLoggingAdapter().log(Level.INFO, + "Please choose a migrator, then run \"husksync migrate \""); + logMigratorsList(); + return; + } + final Optional selectedMigrator = plugin.getAvailableMigrators().stream().filter(availableMigrator -> + availableMigrator.getIdentifier().equalsIgnoreCase(args[1])).findFirst(); + selectedMigrator.ifPresentOrElse(migrator -> { + if (args.length < 3) { + plugin.getLoggingAdapter().log(Level.INFO, + "Invalid syntax. Console usage: \"husksync migrate " + args[1] + ""); + return; + } + switch (args[2]) { + case "start" -> migrator.start(); + case "set" -> migrator.handleConfigurationCommand(Arrays.copyOfRange(args, 3, args.length)); + default -> plugin.getLoggingAdapter().log(Level.INFO, + "Invalid syntax. Console usage: \"husksync migrate " + args[1] + ""); + } + }, () -> { + plugin.getLoggingAdapter().log(Level.INFO, + "Please specify a valid migrator.\n" + + "If a migrator is not available, please verify that you meet the prerequisites to use it."); + logMigratorsList(); + }); } default -> plugin.getLoggingAdapter().log(Level.INFO, "Invalid syntax. Console usage: \"husksync \""); } } + private void logMigratorsList() { + plugin.getLoggingAdapter().log(Level.INFO, + "List of available migrators:\nMigrator ID / Migrator Name:\n" + + plugin.getAvailableMigrators().stream() + .map(migrator -> migrator.getIdentifier() + " - " + migrator.getName()) + .collect(Collectors.joining("\n"))); + } + @Override public List onTabComplete(@NotNull OnlineUser player, @NotNull String[] args) { return Arrays.stream(COMMAND_ARGUMENTS) 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 23b0af90..9237b9e3 100644 --- a/common/src/main/java/net/william278/husksync/command/InventoryCommand.java +++ b/common/src/main/java/net/william278/husksync/command/InventoryCommand.java @@ -81,7 +81,7 @@ public class InventoryCommand extends CommandBase implements TabCompletable { @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] : "")) + .filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : "")) .sorted().collect(Collectors.toList()); } } diff --git a/common/src/main/java/net/william278/husksync/data/DataSaveCause.java b/common/src/main/java/net/william278/husksync/data/DataSaveCause.java index 1a760246..ac2ce0b4 100644 --- a/common/src/main/java/net/william278/husksync/data/DataSaveCause.java +++ b/common/src/main/java/net/william278/husksync/data/DataSaveCause.java @@ -58,10 +58,18 @@ public enum DataSaveCause { * @since 2.0 */ API, - - MPDB_IMPORT, - LEGACY_IMPORT, - MANUAL_IMPORT, + /** + * Indicates data was saved from being imported from MySQLPlayerDataBridge + * + * @since 2.0 + */ + MPDB_MIGRATION, + /** + * Indicates data was saved from being imported from a legacy version (v1.x) + * + * @since 2.0 + */ + LEGACY_MIGRATION, /** * Indicates data was saved by an unknown cause. *

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 4443083d..2fb51f91 100644 --- a/common/src/main/java/net/william278/husksync/editor/DataEditor.java +++ b/common/src/main/java/net/william278/husksync/editor/DataEditor.java @@ -13,6 +13,7 @@ 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 @@ -121,16 +122,18 @@ public class DataEditor { private @NotNull String generateAdvancementPreview(@NotNull List advancementData) { final StringJoiner joiner = new StringJoiner("\n"); + final List advancementsToPreview = advancementData.stream().filter(dataItem -> + !dataItem.key.startsWith("minecraft:recipes/")).toList(); final int PREVIEW_SIZE = 8; - for (int i = 0; i < advancementData.size(); i++) { - joiner.add(advancementData.get(i).key); + for (int i = 0; i < advancementsToPreview.size(); i++) { + joiner.add(advancementsToPreview.get(i).key); if (i >= PREVIEW_SIZE) { break; } } - final int remainingAdvancements = advancementData.size() - PREVIEW_SIZE; + final int remainingAdvancements = advancementsToPreview.size() - PREVIEW_SIZE; if (remainingAdvancements > 0) { - joiner.add(locales.getRawLocale("data_manager_advancement_preview_remaining", + joiner.add(locales.getRawLocale("data_manager_advancements_preview_remaining", Integer.toString(remainingAdvancements)).orElse("+" + remainingAdvancements + "…")); } return joiner.toString(); diff --git a/common/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java b/common/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java new file mode 100644 index 00000000..9aaaa6d0 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java @@ -0,0 +1,43 @@ +package net.william278.husksync.migrator; + +import net.william278.husksync.HuskSync; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +//todo: implement this +public class LegacyMigrator extends Migrator { + + public LegacyMigrator(@NotNull HuskSync plugin) { + super(plugin); + } + + @Override + public CompletableFuture start() { + return null; + } + + @Override + public void handleConfigurationCommand(@NotNull String[] args) { + + } + + @NotNull + @Override + public String getIdentifier() { + return "legacy"; + } + + @NotNull + @Override + public String getName() { + return "HuskSync v1.x --> v2.x"; + } + + @NotNull + @Override + public String getHelpMenu() { + return null; + } + +} diff --git a/common/src/main/java/net/william278/husksync/migrator/Migrator.java b/common/src/main/java/net/william278/husksync/migrator/Migrator.java new file mode 100644 index 00000000..65d24233 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/migrator/Migrator.java @@ -0,0 +1,38 @@ +package net.william278.husksync.migrator; + +import net.william278.husksync.HuskSync; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +public abstract class Migrator { + + protected final HuskSync plugin; + + protected Migrator(@NotNull HuskSync plugin) { + this.plugin = plugin; + } + + /** + * Start the migrator + * + * @return A future that will be completed when the migrator is done + */ + public abstract CompletableFuture start(); + + /** + * Handle a command that sets migrator configuration parameters + * @param args The command arguments + */ + public abstract void handleConfigurationCommand(@NotNull String[] args); + + @NotNull + public abstract String getIdentifier(); + + @NotNull + public abstract String getName(); + + @NotNull + public abstract String getHelpMenu(); + +} diff --git a/common/src/main/resources/locales/en-gb.yml b/common/src/main/resources/locales/en-gb.yml index 83bcfa95..6ca00306 100644 --- a/common/src/main/resources/locales/en-gb.yml +++ b/common/src/main/resources/locales/en-gb.yml @@ -1,15 +1,10 @@ synchronisation_complete: '[⏵ Data synchronised!](#00fb9a)' reload_complete: '[HuskSync](#00fb9a bold) [| Reloaded config and message files.](#00fb9a)' error_invalid_syntax: '[Error:](#ff3300) [Incorrect syntax. Usage: %1%](#ff7e5e)' -error_invalid_player: '[Error:](#ff3300) [Could not find that player.](#ff7e5e)' +error_invalid_player: '[Error:](#ff3300) [Could not find a player by that name.](#ff7e5e)' error_no_permission: '[Error:](#ff3300) [You do not have permission to execute this command](#ff7e5e)' -error_cannot_view_inventory_online: '[Error:](#ff3300) [You can''t access the inventory of an online player through HuskSync](#ff7e5e)' -error_cannot_view_ender_chest_online: '[Error:](#ff3300) [You can''t access the ender chest of an online player through HuskSync](#ff7e5e)' -error_cannot_view_own_inventory: '[Error:](#ff3300) [You can''t access your own inventory!](#ff7e5e)' -error_cannot_view_own_ender_chest: '[Error:](#ff3300) [You can''t access your own ender chest!](#ff7e5e)' -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_console_command_only: '[Error:](#ff3300) [That command can only be run through console](#ff7e5e)' +error_in_game_command_only: 'Error: That command can only be used in-game.' 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' @@ -23,9 +18,9 @@ data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\\n&7What 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%)\\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_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_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 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_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