diff --git a/api/build.gradle b/api/build.gradle index c8d734a1..95e32229 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -1,16 +1,25 @@ dependencies { - compileOnly project(path: ':common') implementation project(path: ':bukkit') + compileOnly project(path: ':common') compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT' compileOnly 'org.jetbrains:annotations:23.0.0' } shadowJar { - relocate 'de.themoep', 'net.william278.husksync.libraries' - relocate 'org.bstats', 'net.william278.husksync.libraries.bstats' - relocate 'redis.clients', 'net.william278.husksync.libraries' 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 'net.byteflux.libby', 'net.william278.husksync.libraries.libby' + relocate 'org.bstats', 'net.william278.husksync.libraries.bstats' relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter' } diff --git a/api/src/main/java/net/william278/husksync/api/HuskSyncAPI.java b/api/src/main/java/net/william278/husksync/api/HuskSyncAPI.java new file mode 100644 index 00000000..ea441fe0 --- /dev/null +++ b/api/src/main/java/net/william278/husksync/api/HuskSyncAPI.java @@ -0,0 +1,4 @@ +package net.william278.husksync.api; + +public class HuskSyncAPI { +} diff --git a/build.gradle b/build.gradle index 8134007d..5cccac28 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { - id 'com.github.johnrengelman.shadow' version '7.1.0' - id 'org.ajoberstar.grgit' version '4.1.1' + id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'org.ajoberstar.grgit' version '5.0.0' id 'java' } @@ -9,8 +9,6 @@ version "$ext.plugin_version+${versionMetadata()}" ext { set 'version', version.toString() - set 'jedis_version', jedis_version.toString() - set 'sqlite_driver_version', sqlite_driver_version.toString() } import org.apache.tools.ant.filters.ReplaceTokens @@ -34,13 +32,6 @@ allprojects { maven { url 'https://jitpack.io' } } - dependencies { - implementation('redis.clients:jedis:4.2.3') { - //noinspection GroovyAssignabilityCheck - exclude module: 'slf4j-api' - } - } - processResources { filter ReplaceTokens as Class, beginToken: '${', endToken: '}', tokens: rootProject.ext.properties diff --git a/bukkit/build.gradle b/bukkit/build.gradle index e047f125..3e1356d0 100644 --- a/bukkit/build.gradle +++ b/bukkit/build.gradle @@ -3,19 +3,28 @@ dependencies { 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 '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.apache', 'net.william278.huskhomes.libraries' - relocate 'dev.dejvokep', 'net.william278.huskhomes.libraries' - relocate 'de.themoep', 'net.william278.huskhomes.libraries' - relocate 'org.jetbrains', 'net.william278.huskhomes.libraries' - relocate 'org.intellij', 'net.william278.huskhomes.libraries' - relocate 'com.zaxxer', 'net.william278.huskhomes.libraries' - relocate 'org.slf4j', 'net.william278.huskhomes.libraries.slf4j' - relocate 'com.google', 'net.william278.huskhomes.libraries' + + relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby' + relocate 'org.bstats', 'net.william278.husksync.libraries.bstats' + relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter' } \ No newline at end of file diff --git a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java new file mode 100644 index 00000000..4dd79800 --- /dev/null +++ b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java @@ -0,0 +1,244 @@ +package net.william278.husksync; + +import dev.dejvokep.boostedyaml.YamlDocument; +import dev.dejvokep.boostedyaml.dvs.versioning.BasicVersioning; +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.BukkitCommand; +import net.william278.husksync.command.CommandBase; +import net.william278.husksync.command.HuskSyncCommand; +import net.william278.husksync.command.Permission; +import net.william278.husksync.config.Locales; +import net.william278.husksync.config.Settings; +import net.william278.husksync.database.Database; +import net.william278.husksync.database.MySqlDatabase; +import net.william278.husksync.listener.BukkitEventListener; +import net.william278.husksync.listener.EventListener; +import net.william278.husksync.player.BukkitPlayer; +import net.william278.husksync.player.OnlineUser; +import net.william278.husksync.redis.RedisManager; +import net.william278.husksync.util.*; +import org.bukkit.Bukkit; +import org.bukkit.command.PluginCommand; +import org.bukkit.entity.Player; +import org.bukkit.permissions.PermissionDefault; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +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 Settings settings; + + private Locales locales; + + private static BukkitHuskSync instance; + + public static BukkitHuskSync getInstance() { + return instance; + } + + @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 + 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); + + // Load settings and locales + getLoggingAdapter().log(Level.INFO, "Loading plugin configuration settings & locales..."); + return reload().thenApply(loadedSettings -> { + if (loadedSettings) { + getLoggingAdapter().log(Level.INFO, "Successfully loaded plugin configuration settings & locales"); + } else { + getLoggingAdapter().log(Level.SEVERE, "Failed to load plugin configuration settings and/or locales"); + } + return loadedSettings; + }).join(); + }).thenApply(succeeded -> { + // Establish connection to the database + this.database = new MySqlDatabase(settings, resourceReader, logger); + if (succeeded) { + getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the database..."); + final CompletableFuture databaseConnectFuture = new CompletableFuture<>(); + Bukkit.getScheduler().runTask(this, () -> { + final boolean initialized = this.database.initialize(); + if (!initialized) { + getLoggingAdapter().log(Level.SEVERE, "Failed to establish a connection to the database. " + + "Please check the supplied database credentials in the config file"); + databaseConnectFuture.completeAsync(() -> false); + return; + } + getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the database"); + databaseConnectFuture.completeAsync(() -> true); + }); + return databaseConnectFuture.join(); + } + return false; + }).thenApply(succeeded -> { + // Establish connection to the Redis server + this.redisManager = new RedisManager(settings); + if (succeeded) { + getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the Redis server..."); + return this.redisManager.initialize().thenApply(initialized -> { + if (!initialized) { + getLoggingAdapter().log(Level.SEVERE, "Failed to establish a connection to the Redis server. " + + "Please check the supplied Redis credentials in the config file"); + return false; + } + getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the Redis server"); + return true; + }).join(); + } + return false; + }).thenApply(succeeded -> { + // Register events + if (succeeded) { + getLoggingAdapter().log(Level.INFO, "Registering events..."); + this.eventListener = new BukkitEventListener(this); + getLoggingAdapter().log(Level.INFO, "Successfully registered events listener"); + } + return succeeded; + }).thenApply(succeeded -> { + // Register permissions + if (succeeded) { + getLoggingAdapter().log(Level.INFO, "Registering permissions & commands..."); + Arrays.stream(Permission.values()).forEach(permission -> getServer().getPluginManager().addPermission(new org.bukkit.permissions.Permission(permission.node, switch (permission.defaultAccess) { + case EVERYONE -> PermissionDefault.TRUE; + case NOBODY -> PermissionDefault.FALSE; + case OPERATORS -> PermissionDefault.OP; + }))); + + // Register commands + final CommandBase[] commands = new CommandBase[]{new HuskSyncCommand(this)}; + for (CommandBase commandBase : commands) { + final PluginCommand pluginCommand = getCommand(commandBase.command); + if (pluginCommand != null) { + new BukkitCommand(commandBase, this).register(pluginCommand); + } + } + getLoggingAdapter().log(Level.INFO, "Successfully registered permissions & commands"); + } + return succeeded; + }).thenApply(succeeded -> { + // Check for updates + if (settings.getBooleanValue(Settings.ConfigOption.CHECK_FOR_UPDATES) && succeeded) { + getLoggingAdapter().log(Level.INFO, "Checking for updates..."); + new UpdateChecker(getVersion(), 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"); + getServer().getPluginManager().disablePlugin(this); + } else { + getLoggingAdapter().log(Level.INFO, "Successfully enabled HuskSync v" + getVersion()); + } + }); + } + + @Override + public void onDisable() { + if (this.eventListener != null) { + this.eventListener.handlePluginDisable(); + } + getLoggingAdapter().log(Level.INFO, "Successfully disabled HuskSync v" + getVersion()); + } + + @Override + public @NotNull Set getOnlineUsers() { + return Bukkit.getOnlinePlayers().stream().map(BukkitPlayer::adapt).collect(Collectors.toSet()); + } + + @Override + public @NotNull Optional getOnlineUser(@NotNull UUID uuid) { + final Player player = Bukkit.getPlayer(uuid); + if (player == null) { + return Optional.empty(); + } + return Optional.of(BukkitPlayer.adapt(player)); + } + + @Override + public @NotNull Database getDatabase() { + return database; + } + + @Override + public @NotNull RedisManager getRedisManager() { + return redisManager; + } + + @Override + public @NotNull Settings getSettings() { + return settings; + } + + @Override + public @NotNull Locales getLocales() { + return locales; + } + + @Override + public @NotNull Logger getLoggingAdapter() { + return logger; + } + + @Override + public @NotNull String getVersion() { + return getDescription().getVersion(); + } + + @Override + public CompletableFuture reload() { + return CompletableFuture.supplyAsync(() -> { + try { + this.settings = Settings.load(YamlDocument.create(new File(getDataFolder(), "config.yml"), Objects.requireNonNull(resourceReader.getResource("config.yml")), GeneralSettings.builder().setUseDefaults(false).build(), LoaderSettings.builder().setAutoUpdate(true).build(), DumperSettings.builder().setEncoding(DumperSettings.Encoding.UNICODE).build(), UpdaterSettings.builder().setVersioning(new BasicVersioning("config_version")).build())); + + this.locales = Locales.load(YamlDocument.create(new File(getDataFolder(), "messages-" + settings.getStringValue(Settings.ConfigOption.LANGUAGE) + ".yml"), Objects.requireNonNull(resourceReader.getResource("locales/" + settings.getStringValue(Settings.ConfigOption.LANGUAGE) + ".yml")))); + return true; + } catch (IOException | NullPointerException e) { + getLoggingAdapter().log(Level.SEVERE, "Failed to load data from the config", e); + return false; + } + }); + } +} diff --git a/bukkit/src/main/java/net/william278/husksync/command/BukkitCommand.java b/bukkit/src/main/java/net/william278/husksync/command/BukkitCommand.java new file mode 100644 index 00000000..2785f8a3 --- /dev/null +++ b/bukkit/src/main/java/net/william278/husksync/command/BukkitCommand.java @@ -0,0 +1,70 @@ +package net.william278.husksync.command; + +import net.william278.husksync.HuskSync; +import net.william278.husksync.player.BukkitPlayer; +import org.bukkit.command.*; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.List; + +/** + * Bukkit executor that implements and executes {@link CommandBase}s + */ +public class BukkitCommand implements CommandExecutor, TabExecutor { + + /** + * The {@link CommandBase} that will be executed + */ + private final CommandBase command; + + /** + * The implementing plugin + */ + private final HuskSync plugin; + + public BukkitCommand(@NotNull CommandBase command, @NotNull HuskSync implementor) { + this.command = command; + this.plugin = implementor; + } + + /** + * Registers a {@link PluginCommand} to this implementation + * + * @param pluginCommand {@link PluginCommand} to register + */ + public void register(@NotNull PluginCommand pluginCommand) { + pluginCommand.setExecutor(this); + pluginCommand.setTabCompleter(this); + pluginCommand.setPermission(command.permission); + pluginCommand.setDescription(command.getDescription()); + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, + @NotNull String label, @NotNull String[] args) { + if (sender instanceof Player player) { + this.command.onExecute(BukkitPlayer.adapt(player), args); + } else { + if (command instanceof ConsoleExecutable consoleExecutable) { + consoleExecutable.onConsoleExecute(args); + } else { + plugin.getLocales().getLocale("error_in_game_only"). + ifPresent(locale -> sender.spigot().sendMessage(locale.toComponent())); + } + } + return true; + } + + @Nullable + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, + @NotNull String alias, @NotNull String[] args) { + if (this.command instanceof TabCompletable tabCompletable) { + return tabCompletable.onTabComplete(BukkitPlayer.adapt((Player) sender), args); + } + return Collections.emptyList(); + } +} diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java new file mode 100644 index 00000000..f1603ac9 --- /dev/null +++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java @@ -0,0 +1,199 @@ +package net.william278.husksync.data; + +import org.bukkit.inventory.ItemStack; +import org.bukkit.potion.PotionEffect; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.bukkit.util.io.BukkitObjectOutputStream; +import org.jetbrains.annotations.Nullable; +import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class BukkitSerializer { + + /** + * Returns a serialized array of {@link ItemStack}s + * + * @param inventoryContents The contents of the inventory + * @return The serialized inventory contents + */ + public static CompletableFuture serializeInventory(ItemStack[] inventoryContents) { + return CompletableFuture.supplyAsync(() -> { + // Return an empty string if there is no inventory item data to serialize + if (inventoryContents.length == 0) { + return ""; + } + + // Create an output stream that will be encoded into base 64 + ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); + + try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) { + // Define the length of the inventory array to serialize + bukkitOutputStream.writeInt(inventoryContents.length); + + // Write each serialize each ItemStack to the output stream + for (ItemStack inventoryItem : inventoryContents) { + bukkitOutputStream.writeObject(serializeItemStack(inventoryItem)); + } + + // Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion + return Base64Coder.encodeLines(byteOutputStream.toByteArray()); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to serialize item stack data"); + } + }); + } + + /** + * Returns an array of ItemStacks from serialized inventory data. Note: empty slots will be represented by {@code null} + * + * @param inventoryData The serialized {@link ItemStack[]} array + * @return The inventory contents as an array of {@link ItemStack}s + */ + public static CompletableFuture deserializeInventory(String inventoryData) { + return CompletableFuture.supplyAsync(() -> { + // Return empty array if there is no inventory data (set the player as having an empty inventory) + if (inventoryData.isEmpty()) { + return new ItemStack[0]; + } + + // Create a byte input stream to read the serialized data + try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(inventoryData))) { + try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) { + // Read the length of the Bukkit input stream and set the length of the array to this value + ItemStack[] inventoryContents = new ItemStack[bukkitInputStream.readInt()]; + + // Set the ItemStacks in the array from deserialized ItemStack data + int slotIndex = 0; + for (ItemStack ignored : inventoryContents) { + inventoryContents[slotIndex] = deserializeItemStack(bukkitInputStream.readObject()); + slotIndex++; + } + + // Return the finished, serialized inventory contents + return inventoryContents; + } + } catch (IOException | ClassNotFoundException e) { + throw new RuntimeException("Failed to deserialize item stack data"); + } + }); + } + + /** + * Returns the serialized version of an {@link ItemStack} as a string to object Map + * + * @param item The {@link ItemStack} to serialize + * @return The serialized {@link ItemStack} + */ + private static Map serializeItemStack(ItemStack item) { + return item != null ? item.serialize() : null; + } + + /** + * Returns the deserialized {@link ItemStack} from the Object read from the {@link BukkitObjectInputStream} + * + * @param serializedItemStack The serialized item stack; a String-Object map + * @return The deserialized {@link ItemStack} + */ + @SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning + private static ItemStack deserializeItemStack(Object serializedItemStack) { + return serializedItemStack != null ? ItemStack.deserialize((Map) serializedItemStack) : null; + } + + /** + * Returns a serialized array of {@link PotionEffect}s + * + * @param potionEffects The potion effect array + * @return The serialized potion effects + */ + public static CompletableFuture serializePotionEffects(PotionEffect[] potionEffects) { + return CompletableFuture.supplyAsync(() -> { + // Return an empty string if there are no effects to serialize + if (potionEffects.length == 0) { + return ""; + } + + // Create an output stream that will be encoded into base 64 + ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); + + try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) { + // Define the length of the potion effect array to serialize + bukkitOutputStream.writeInt(potionEffects.length); + + // Write each serialize each PotionEffect to the output stream + for (PotionEffect potionEffect : potionEffects) { + bukkitOutputStream.writeObject(serializePotionEffect(potionEffect)); + } + + // Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion + return Base64Coder.encodeLines(byteOutputStream.toByteArray()); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to serialize potion effect data"); + } + }); + } + + /** + * Returns an array of ItemStacks from serialized potion effect data + * + * @param potionEffectData The serialized {@link PotionEffect[]} array + * @return The {@link PotionEffect}s + */ + public static CompletableFuture deserializePotionEffects(String potionEffectData) { + return CompletableFuture.supplyAsync(() -> { + // Return empty array if there is no potion effect data (don't apply any effects to the player) + if (potionEffectData.isEmpty()) { + return new PotionEffect[0]; + } + + // Create a byte input stream to read the serialized data + try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(potionEffectData))) { + try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) { + // Read the length of the Bukkit input stream and set the length of the array to this value + PotionEffect[] potionEffects = new PotionEffect[bukkitInputStream.readInt()]; + + // Set the potion effects in the array from deserialized PotionEffect data + int potionIndex = 0; + for (PotionEffect ignored : potionEffects) { + potionEffects[potionIndex] = deserializePotionEffect(bukkitInputStream.readObject()); + potionIndex++; + } + + // Return the finished, serialized potion effect array + return potionEffects; + } + } catch (IOException | ClassNotFoundException e) { + throw new RuntimeException("Failed to deserialize potion effects", e); + } + }); + } + + /** + * Returns the serialized version of an {@link ItemStack} as a string to object Map + * + * @param potionEffect The {@link ItemStack} to serialize + * @return The serialized {@link ItemStack} + */ + @Nullable + private static Map serializePotionEffect(PotionEffect potionEffect) { + return potionEffect != null ? potionEffect.serialize() : null; + } + + /** + * Returns the deserialized {@link PotionEffect} from the Object read from the {@link BukkitObjectInputStream} + * + * @param serializedPotionEffect The serialized potion effect; a String-Object map + * @return The deserialized {@link PotionEffect} + */ + @SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning + @Nullable + private static PotionEffect deserializePotionEffect(Object serializedPotionEffect) { + return serializedPotionEffect != null ? new PotionEffect((Map) serializedPotionEffect) : null; + } + + +} diff --git a/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java b/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java new file mode 100644 index 00000000..1a9e34e0 --- /dev/null +++ b/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java @@ -0,0 +1,48 @@ +package net.william278.husksync.listener; + +import net.william278.husksync.BukkitHuskSync; +import net.william278.husksync.player.BukkitPlayer; +import org.bukkit.Bukkit; +import org.bukkit.event.Cancellable; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.world.WorldSaveEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.stream.Collectors; + +public class BukkitEventListener extends EventListener implements Listener { + + public BukkitEventListener(@NotNull BukkitHuskSync huskSync) { + super(huskSync); + Bukkit.getServer().getPluginManager().registerEvents(this, huskSync); + } + + @EventHandler + public void onPlayerJoin(@NotNull PlayerJoinEvent event) { + super.handlePlayerJoin(BukkitPlayer.adapt(event.getPlayer())); + } + + @EventHandler + public void onPlayerQuit(@NotNull PlayerQuitEvent event) { + super.handlePlayerQuit(BukkitPlayer.adapt(event.getPlayer())); + BukkitPlayer.remove(event.getPlayer()); + } + + @EventHandler + public void onWorldSave(@NotNull WorldSaveEvent event) { + super.handleWorldSave(event.getWorld().getPlayers().stream().map(BukkitPlayer::adapt) + .collect(Collectors.toList())); + } + + /*@EventHandler(ignoreCancelled = true) + public void onGenericPlayerEvent(@NotNull PlayerEvent event) { + if (event instanceof Cancellable) { + ((Cancellable) event).setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer()))); + } + }*/ + +} diff --git a/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java b/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java new file mode 100644 index 00000000..c5f69d62 --- /dev/null +++ b/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java @@ -0,0 +1,434 @@ +package net.william278.husksync.player; + +import de.themoep.minedown.MineDown; +import net.md_5.bungee.api.ChatMessageType; +import net.william278.husksync.BukkitHuskSync; +import net.william278.husksync.data.*; +import org.apache.commons.lang.ArrayUtils; +import org.bukkit.*; +import org.bukkit.advancement.Advancement; +import org.bukkit.advancement.AdvancementProgress; +import org.bukkit.attribute.Attribute; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Bukkit implementation of an {@link OnlineUser} + */ +public class BukkitPlayer extends OnlineUser { + + private static final HashMap cachedPlayers = new HashMap<>(); + private final Player player; + + private BukkitPlayer(@NotNull Player player) { + super(player.getUniqueId(), player.getName()); + this.player = player; + } + + public static BukkitPlayer adapt(@NotNull Player player) { + if (cachedPlayers.containsKey(player.getUniqueId())) { + return cachedPlayers.get(player.getUniqueId()); + } + final BukkitPlayer bukkitPlayer = new BukkitPlayer(player); + cachedPlayers.put(player.getUniqueId(), bukkitPlayer); + return bukkitPlayer; + } + + public static void remove(@NotNull Player player) { + cachedPlayers.remove(player.getUniqueId()); + } + + @Override + public CompletableFuture getStatus() { + return CompletableFuture.supplyAsync(() -> { + final double maxHealth = getMaxHealth(player); + return new StatusData(Math.min(player.getHealth(), maxHealth), + maxHealth, + player.isHealthScaled() ? player.getHealthScale() : 0d, + player.getFoodLevel(), + player.getSaturation(), + player.getExhaustion(), + player.getInventory().getHeldItemSlot(), + player.getTotalExperience(), + player.getLevel(), + player.getExp(), + player.getGameMode().name(), + player.getAllowFlight() && player.isFlying()); + }); + } + + @Override + public CompletableFuture setStatus(@NotNull StatusData statusData, + boolean setHealth, boolean setMaxHealth, + boolean setHunger, boolean setExperience, + boolean setGameMode, boolean setFlying) { + return CompletableFuture.runAsync(() -> { + double currentMaxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)) + .getBaseValue(); + if (setMaxHealth) { + if (statusData.maxHealth != 0d) { + Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)) + .setBaseValue(statusData.maxHealth); + currentMaxHealth = statusData.maxHealth; + } + } + if (setHealth) { + final double currentHealth = player.getHealth(); + if (statusData.health != currentHealth) { + player.setHealth(currentHealth > currentMaxHealth ? currentMaxHealth : statusData.health); + } + + if (statusData.healthScale != 0d) { + player.setHealthScale(statusData.healthScale); + } else { + player.setHealthScale(statusData.maxHealth); + } + player.setHealthScaled(statusData.healthScale != 0D); + } + if (setHunger) { + player.setFoodLevel(statusData.hunger); + player.setSaturation(statusData.saturation); + player.setExhaustion(statusData.saturationExhaustion); + } + if (setExperience) { + player.setTotalExperience(statusData.totalExperience); + player.setLevel(statusData.expLevel); + player.setExp(statusData.expProgress); + } + if (setGameMode) { + player.setGameMode(GameMode.valueOf(statusData.gameMode)); + } + if (setFlying) { + if (statusData.isFlying) { + player.setAllowFlight(true); + player.setFlying(true); + } + player.setFlying(false); + } + }); + } + + @Override + public CompletableFuture getInventory() { + return BukkitSerializer.serializeInventory(player.getInventory().getContents()) + .thenApply(InventoryData::new); + } + + @Override + public CompletableFuture setInventory(@NotNull InventoryData inventoryData) { + return BukkitSerializer.deserializeInventory(inventoryData.serializedInventory).thenAccept(contents -> + Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), + () -> player.getInventory().setContents(contents))); + } + + @Override + public CompletableFuture getEnderChest() { + return BukkitSerializer.serializeInventory(player.getEnderChest().getContents()) + .thenApply(InventoryData::new); + } + + @Override + public CompletableFuture setEnderChest(@NotNull InventoryData enderChestData) { + return BukkitSerializer.deserializeInventory(enderChestData.serializedInventory).thenAccept(contents -> + Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), + () -> player.getEnderChest().setContents(contents))); + } + + @Override + public CompletableFuture getPotionEffects() { + return BukkitSerializer.serializePotionEffects(player.getActivePotionEffects() + .toArray(new PotionEffect[0])).thenApply(PotionEffectData::new); + } + + @Override + public CompletableFuture setPotionEffects(@NotNull PotionEffectData potionEffectData) { + return BukkitSerializer.deserializePotionEffects(potionEffectData.serializedPotionEffects).thenAccept( + effects -> Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> { + for (PotionEffect effect : player.getActivePotionEffects()) { + player.removePotionEffect(effect.getType()); + } + for (PotionEffect effect : effects) { + player.addPotionEffect(effect); + } + })); + } + + @Override + public CompletableFuture> getAdvancements() { + return CompletableFuture.supplyAsync(() -> { + final Iterator serverAdvancements = Bukkit.getServer().advancementIterator(); + final ArrayList advancementData = new ArrayList<>(); + + // Iterate through the server advancement set and add all advancements to the list + serverAdvancements.forEachRemaining(advancement -> { + final AdvancementProgress advancementProgress = player.getAdvancementProgress(advancement); + final Map awardedCriteria = new HashMap<>(); + + advancementProgress.getAwardedCriteria().forEach(criteriaKey -> awardedCriteria.put(criteriaKey, + advancementProgress.getDateAwarded(criteriaKey))); + + // Only save the advancement if criteria has been completed + if (!awardedCriteria.isEmpty()) { + advancementData.add(new AdvancementData(advancement.getKey().toString(), awardedCriteria)); + } + }); + return advancementData; + }); + } + + @Override + public CompletableFuture setAdvancements(@NotNull List advancementData) { + return CompletableFuture.runAsync(() -> { + // Temporarily disable advancement announcing if needed + boolean announceAdvancementUpdate = false; + if (Boolean.TRUE.equals(player.getWorld().getGameRuleValue(GameRule.ANNOUNCE_ADVANCEMENTS))) { + player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false); + announceAdvancementUpdate = true; + } + final boolean finalAnnounceAdvancementUpdate = announceAdvancementUpdate; + + // Save current experience and level + final int experienceLevel = player.getLevel(); + final float expProgress = player.getExp(); + + // Determines whether the experience might have changed warranting an update + final AtomicBoolean correctExperience = new AtomicBoolean(false); + + // Apply the advancements to the player + final Iterator serverAdvancements = Bukkit.getServer().advancementIterator(); + while (serverAdvancements.hasNext()) { + // Iterate through all advancements + final Advancement advancement = serverAdvancements.next(); + final AdvancementProgress playerProgress = player.getAdvancementProgress(advancement); + + advancementData.stream().filter(record -> record.key.equals(advancement.getKey().toString())).findFirst().ifPresentOrElse( + // Award all criteria that the player does not have that they do on the cache + record -> { + record.completedCriteria.keySet().stream() + .filter(criterion -> !playerProgress.getAwardedCriteria().contains(criterion)) + .forEach(criterion -> { + Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), + () -> player.getAdvancementProgress(advancement).awardCriteria(criterion)); + correctExperience.set(true); + }); + + // Revoke all criteria that the player does have but should not + new ArrayList<>(playerProgress.getAwardedCriteria()).stream().filter(criterion -> !record.completedCriteria.containsKey(criterion)) + .forEach(criterion -> Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), + () -> player.getAdvancementProgress(advancement).revokeCriteria(criterion))); + + }, + // Revoke the criteria as the player shouldn't have any + () -> new ArrayList<>(playerProgress.getAwardedCriteria()).forEach(criterion -> + Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), + () -> player.getAdvancementProgress(advancement).revokeCriteria(criterion)))); + + // Update the player's experience in case the advancement changed that + if (correctExperience.get()) { + player.setLevel(experienceLevel); + player.setExp(expProgress); + correctExperience.set(false); + } + } + + // Re-enable announcing advancements (back on main thread again) + Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> { + if (finalAnnounceAdvancementUpdate) { + player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, true); + } + }); + }); + } + + @Override + public CompletableFuture getStatistics() { + return CompletableFuture.supplyAsync(() -> { + final Map untypedStatisticValues = new HashMap<>(); + final Map> blockStatisticValues = new HashMap<>(); + final Map> itemStatisticValues = new HashMap<>(); + final Map> entityStatisticValues = new HashMap<>(); + + for (Statistic statistic : Statistic.values()) { + switch (statistic.getType()) { + case ITEM -> { + final Map itemValues = new HashMap<>(); + Arrays.stream(Material.values()).filter(Material::isItem) + .filter(itemMaterial -> (player.getStatistic(statistic, itemMaterial)) != 0) + .forEach(itemMaterial -> itemValues.put(itemMaterial.name(), + player.getStatistic(statistic, itemMaterial))); + if (!itemValues.isEmpty()) { + itemStatisticValues.put(statistic.name(), itemValues); + } + } + case BLOCK -> { + final Map blockValues = new HashMap<>(); + Arrays.stream(Material.values()).filter(Material::isBlock) + .filter(blockMaterial -> (player.getStatistic(statistic, blockMaterial)) != 0) + .forEach(blockMaterial -> blockValues.put(blockMaterial.name(), + player.getStatistic(statistic, blockMaterial))); + if (!blockValues.isEmpty()) { + blockStatisticValues.put(statistic.name(), blockValues); + } + } + case ENTITY -> { + final Map entityValues = new HashMap<>(); + Arrays.stream(EntityType.values()).filter(EntityType::isAlive) + .filter(entityType -> (player.getStatistic(statistic, entityType)) != 0) + .forEach(entityType -> entityValues.put(entityType.name(), + player.getStatistic(statistic, entityType))); + if (!entityValues.isEmpty()) { + entityStatisticValues.put(statistic.name(), entityValues); + } + } + case UNTYPED -> { + if (player.getStatistic(statistic) != 0) { + untypedStatisticValues.put(statistic.name(), player.getStatistic(statistic)); + } + } + } + } + + return new StatisticsData(untypedStatisticValues, blockStatisticValues, + itemStatisticValues, entityStatisticValues); + }); + } + + @Override + public CompletableFuture setStatistics(@NotNull StatisticsData statisticsData) { + return CompletableFuture.runAsync(() -> { + // Set untyped statistics + for (String statistic : statisticsData.untypedStatistic.keySet()) { + player.setStatistic(Statistic.valueOf(statistic), statisticsData.untypedStatistic.get(statistic)); + } + + // Set block statistics + for (String statistic : statisticsData.blockStatistics.keySet()) { + for (String blockMaterial : statisticsData.blockStatistics.get(statistic).keySet()) { + player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(blockMaterial), + statisticsData.blockStatistics.get(statistic).get(blockMaterial)); + } + } + + // Set item statistics + for (String statistic : statisticsData.itemStatistics.keySet()) { + for (String itemMaterial : statisticsData.itemStatistics.get(statistic).keySet()) { + player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(itemMaterial), + statisticsData.itemStatistics.get(statistic).get(itemMaterial)); + } + } + + // Set entity statistics + for (String statistic : statisticsData.entityStatistics.keySet()) { + for (String entityType : statisticsData.entityStatistics.get(statistic).keySet()) { + player.setStatistic(Statistic.valueOf(statistic), EntityType.valueOf(entityType), + statisticsData.entityStatistics.get(statistic).get(entityType)); + } + } + }); + } + + @Override + public CompletableFuture getLocation() { + return CompletableFuture.supplyAsync(() -> + new LocationData(player.getWorld().getName(), player.getWorld().getUID(), player.getWorld().getEnvironment().name(), + player.getLocation().getX(), player.getLocation().getY(), player.getLocation().getZ(), + player.getLocation().getYaw(), player.getLocation().getPitch())); + } + + @Override + public CompletableFuture setLocation(@NotNull LocationData locationData) { + final CompletableFuture completableFuture = new CompletableFuture<>(); + AtomicReference bukkitWorld = new AtomicReference<>(Bukkit.getWorld(locationData.worldName)); + if (bukkitWorld.get() == null) { + bukkitWorld.set(Bukkit.getWorld(locationData.worldUuid)); + } + if (bukkitWorld.get() == null) { + Bukkit.getWorlds().stream().filter(world -> world.getEnvironment() == World.Environment + .valueOf(locationData.worldEnvironment)).findFirst().ifPresent(bukkitWorld::set); + } + if (bukkitWorld.get() != null) { + player.teleport(new Location(bukkitWorld.get(), + locationData.x, locationData.y, locationData.z, + locationData.yaw, locationData.pitch), PlayerTeleportEvent.TeleportCause.PLUGIN); + } + CompletableFuture.runAsync(() -> completableFuture.completeAsync(() -> null)); + return completableFuture; + } + + @Override + public CompletableFuture getPersistentDataContainer() { + return CompletableFuture.supplyAsync(() -> { + final PersistentDataContainer container = player.getPersistentDataContainer(); + if (container.isEmpty()) { + return new PersistentDataContainerData(new HashMap<>()); + } + final HashMap persistentDataMap = new HashMap<>(); + for (NamespacedKey key : container.getKeys()) { + persistentDataMap.put(key.toString(), ArrayUtils.toObject(container.get(key, PersistentDataType.BYTE_ARRAY))); + } + return new PersistentDataContainerData(persistentDataMap); + }); + } + + @Override + public CompletableFuture setPersistentDataContainer(@NotNull PersistentDataContainerData persistentDataContainerData) { + return CompletableFuture.runAsync(() -> { + player.getPersistentDataContainer().getKeys().forEach(namespacedKey -> + player.getPersistentDataContainer().remove(namespacedKey)); + persistentDataContainerData.persistentDataMap.keySet().forEach(keyString -> { + final NamespacedKey key = NamespacedKey.fromString(keyString); + if (key != null) { + final byte[] data = ArrayUtils.toPrimitive(persistentDataContainerData + .persistentDataMap.get(keyString)); + player.getPersistentDataContainer().set(key, PersistentDataType.BYTE_ARRAY, data); + } + }); + }); + } + + @Override + public boolean hasPermission(@NotNull String node) { + return player.hasPermission(node); + } + + @Override + public void sendActionBar(@NotNull MineDown mineDown) { + player.spigot().sendMessage(ChatMessageType.ACTION_BAR, mineDown.toComponent()); + } + + @Override + public void sendMessage(@NotNull MineDown mineDown) { + player.spigot().sendMessage(mineDown.toComponent()); + } + + /** + * Returns a {@link Player}'s maximum health, minus any health boost effects + * + * @param player The {@link Player} to get the maximum health of + * @return The {@link Player}'s max health + */ + private static double getMaxHealth(@NotNull Player player) { + double maxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).getBaseValue(); + + // If the player has additional health bonuses from synchronised potion effects, subtract these from this number as they are synchronised separately + if (player.hasPotionEffect(PotionEffectType.HEALTH_BOOST) && maxHealth > 20D) { + PotionEffect healthBoostEffect = player.getPotionEffect(PotionEffectType.HEALTH_BOOST); + assert healthBoostEffect != null; + double healthBoostBonus = 4 * (healthBoostEffect.getAmplifier() + 1); + maxHealth -= healthBoostBonus; + } + return maxHealth; + } + +} diff --git a/bukkit/src/main/java/net/william278/husksync/util/BukkitLogger.java b/bukkit/src/main/java/net/william278/husksync/util/BukkitLogger.java new file mode 100644 index 00000000..5bcd0662 --- /dev/null +++ b/bukkit/src/main/java/net/william278/husksync/util/BukkitLogger.java @@ -0,0 +1,38 @@ +package net.william278.husksync.util; + +import java.util.logging.Level; + +public class BukkitLogger implements Logger { + + private final java.util.logging.Logger logger; + + public BukkitLogger(java.util.logging.Logger logger) { + this.logger = logger; + } + + @Override + public void log(Level level, String message, Exception e) { + logger.log(level, message, e); + } + + @Override + public void log(Level level, String message) { + logger.log(level, message); + } + + @Override + public void info(String message) { + logger.info(message); + } + + @Override + public void severe(String message) { + logger.severe(message); + } + + @Override + public void config(String message) { + logger.config(message); + } + +} diff --git a/bukkit/src/main/java/net/william278/husksync/util/BukkitResourceReader.java b/bukkit/src/main/java/net/william278/husksync/util/BukkitResourceReader.java new file mode 100644 index 00000000..38200c39 --- /dev/null +++ b/bukkit/src/main/java/net/william278/husksync/util/BukkitResourceReader.java @@ -0,0 +1,28 @@ +package net.william278.husksync.util; + +import net.william278.husksync.BukkitHuskSync; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.InputStream; +import java.util.Objects; + +public class BukkitResourceReader implements ResourceReader { + + private final BukkitHuskSync plugin; + + public BukkitResourceReader(BukkitHuskSync plugin) { + this.plugin = plugin; + } + + @Override + public @NotNull InputStream getResource(String fileName) { + return Objects.requireNonNull(plugin.getResource(fileName)); + } + + @Override + public @NotNull File getDataFolder() { + return plugin.getDataFolder(); + } + +} diff --git a/bukkit/src/main/resources/plugin.yml b/bukkit/src/main/resources/plugin.yml index 109cff7d..6c5a3c60 100644 --- a/bukkit/src/main/resources/plugin.yml +++ b/bukkit/src/main/resources/plugin.yml @@ -1,8 +1,13 @@ name: HuskSync version: ${version} -main: net.william278.husksync.HuskSyncBukkit +main: net.william278.husksync.BukkitHuskSync api-version: 1.16 author: William278 description: 'A modern, cross-server player data synchronization system' website: 'https://william278.net' -softdepend: [MysqlPlayerDataBridge] \ No newline at end of file +softdepend: [ MysqlPlayerDataBridge ] +libraries: + - 'mysql:mysql-connector-java:8.0.29' +commands: + husksync: + usage: '/husksync ' \ No newline at end of file diff --git a/bungeecord/build.gradle b/bungeecord/build.gradle deleted file mode 100644 index 3b83194a..00000000 --- a/bungeecord/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -dependencies { - implementation project(path: ':common') - - implementation 'com.zaxxer:HikariCP:5.0.1' - implementation 'org.bstats:bstats-bungeecord:3.0.0' - implementation 'de.themoep:minedown:1.7.1-SNAPSHOT' - implementation 'net.byteflux:libby-bungee:1.1.5' - - compileOnly 'net.md-5:bungeecord-api:1.16-R0.5-SNAPSHOT' -} - -shadowJar { - relocate 'de.themoep', 'net.william278.husksync.libraries' - relocate 'net.byteflux', 'net.william278.husksync.libraries' - relocate 'org.bstats', 'net.william278.husksync.libraries.bstats' - relocate 'redis.clients', 'net.william278.husksync.libraries' - relocate 'org.apache', 'net.william278.husksync.libraries' - - dependencies { - //noinspection GroovyAssignabilityCheck - exclude dependency(':slf4j-api') - } -} \ No newline at end of file diff --git a/bungeecord/src/main/resources/bungee.yml b/bungeecord/src/main/resources/bungee.yml deleted file mode 100644 index 844687bb..00000000 --- a/bungeecord/src/main/resources/bungee.yml +++ /dev/null @@ -1,5 +0,0 @@ -name: HuskSync -version: ${version} -main: net.william278.husksync.HuskSyncBungeeCord -author: William278 -description: 'A modern, cross-server player data synchronization system' \ No newline at end of file diff --git a/common/build.gradle b/common/build.gradle index b0a7207f..9e39baf9 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -4,20 +4,21 @@ dependencies { 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' compileOnly 'org.jetbrains:annotations:23.0.0' - compileOnly 'org.xerial:sqlite-jdbc:' + sqlite_driver_version - compileOnly 'redis.clients:jedis:' + jedis_version } 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.apache', 'net.william278.huskhomes.libraries' - relocate 'dev.dejvokep', 'net.william278.huskhomes.libraries' - relocate 'de.themoep', 'net.william278.huskhomes.libraries' - relocate 'org.jetbrains', 'net.william278.huskhomes.libraries' - relocate 'org.intellij', 'net.william278.huskhomes.libraries' - relocate 'com.zaxxer', 'net.william278.huskhomes.libraries' - relocate 'org.slf4j', 'net.william278.huskhomes.libraries.slf4j' - relocate 'com.google', 'net.william278.huskhomes.libraries' } \ 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 848fd23c..1c20308d 100644 --- a/common/src/main/java/net/william278/husksync/HuskSync.java +++ b/common/src/main/java/net/william278/husksync/HuskSync.java @@ -2,16 +2,16 @@ package net.william278.husksync; import net.william278.husksync.config.Locales; import net.william278.husksync.config.Settings; -import net.william278.husksync.listener.EventListener; +import net.william278.husksync.database.Database; import net.william278.husksync.player.OnlineUser; import net.william278.husksync.redis.RedisManager; -import net.william278.husksync.database.Database; import net.william278.husksync.util.Logger; import org.jetbrains.annotations.NotNull; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CompletableFuture; public interface HuskSync { @@ -19,8 +19,6 @@ public interface HuskSync { @NotNull Optional getOnlineUser(@NotNull UUID uuid); - @NotNull EventListener getEventListener(); - @NotNull Database getDatabase(); @NotNull RedisManager getRedisManager(); @@ -29,10 +27,10 @@ public interface HuskSync { @NotNull Locales getLocales(); - @NotNull Logger getLogger(); + @NotNull Logger getLoggingAdapter(); @NotNull String getVersion(); - void reload(); + CompletableFuture reload(); } 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 0b6d8aca..cc1cc9af 100644 --- a/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java +++ b/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java @@ -28,7 +28,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.getLogger()); + final UpdateChecker updateChecker = new UpdateChecker(plugin.getVersion(), 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)" + @@ -56,22 +56,22 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons @Override public void onConsoleExecute(@NotNull String[] args) { if (args.length < 1) { - plugin.getLogger().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.getLogger()).logToConsole(); - case "info", "about" -> plugin.getLogger().log(Level.INFO, plugin.getLocales().stripMineDown( + case "update", "version" -> new UpdateChecker(plugin.getVersion(), plugin.getLoggingAdapter()).logToConsole(); + case "info", "about" -> plugin.getLoggingAdapter().log(Level.INFO, plugin.getLocales().stripMineDown( Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getVersion()))); case "reload" -> { plugin.reload(); - plugin.getLogger().log(Level.INFO, "Reloaded config & message files."); + plugin.getLoggingAdapter().log(Level.INFO, "Reloaded config & message files."); } case "migrate" -> { //todo - MPDB migrator } default -> - plugin.getLogger().log(Level.INFO, "Invalid syntax. Console usage: /husksync "); + plugin.getLoggingAdapter().log(Level.INFO, "Invalid syntax. Console usage: /husksync "); } } 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 0ca41baf..0ad81a83 100644 --- a/common/src/main/java/net/william278/husksync/config/Settings.java +++ b/common/src/main/java/net/william278/husksync/config/Settings.java @@ -118,7 +118,7 @@ public class Settings { LANGUAGE("language", OptionType.STRING, "en-gb"), CHECK_FOR_UPDATES("check_for_updates", OptionType.BOOLEAN, true), - CLUSTER_ID("cluster_id", OptionType.STRING, ""), //todo implement this + CLUSTER_ID("cluster_id", OptionType.STRING, ""), 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/AdvancementData.java b/common/src/main/java/net/william278/husksync/data/AdvancementData.java index d7df3f39..67860a04 100644 --- a/common/src/main/java/net/william278/husksync/data/AdvancementData.java +++ b/common/src/main/java/net/william278/husksync/data/AdvancementData.java @@ -1,6 +1,7 @@ package net.william278.husksync.data; import com.google.gson.annotations.SerializedName; +import org.jetbrains.annotations.NotNull; import java.util.Date; import java.util.Map; @@ -25,4 +26,8 @@ public class AdvancementData { public AdvancementData() { } + public AdvancementData(@NotNull String key, @NotNull Map awardedCriteria) { + this.key = key; + this.completedCriteria = awardedCriteria; + } } diff --git a/common/src/main/java/net/william278/husksync/data/PersistentDataContainerData.java b/common/src/main/java/net/william278/husksync/data/PersistentDataContainerData.java index 994446ed..c1c9cc57 100644 --- a/common/src/main/java/net/william278/husksync/data/PersistentDataContainerData.java +++ b/common/src/main/java/net/william278/husksync/data/PersistentDataContainerData.java @@ -3,19 +3,21 @@ package net.william278.husksync.data; import com.google.gson.annotations.SerializedName; import org.jetbrains.annotations.NotNull; +import java.util.Map; + /** * Store's a user's persistent data container, holding a map of plugin-set persistent values */ public class PersistentDataContainerData { /** - * A base64 string of platform-serialized PersistentDataContainer data + * Map of namespaced key strings to a byte array representing the persistent data */ - @SerializedName("serialized_persistent_data_container") - public String serializedPersistentDataContainer; + @SerializedName("persistent_data_map") + public Map persistentDataMap; - public PersistentDataContainerData(@NotNull final String serializedPersistentDataContainer) { - this.serializedPersistentDataContainer = serializedPersistentDataContainer; + public PersistentDataContainerData(@NotNull final Map persistentDataMap) { + this.persistentDataMap = persistentDataMap; } public PersistentDataContainerData() { diff --git a/common/src/main/java/net/william278/husksync/data/StatisticsData.java b/common/src/main/java/net/william278/husksync/data/StatisticsData.java index 2320064a..62a2d409 100644 --- a/common/src/main/java/net/william278/husksync/data/StatisticsData.java +++ b/common/src/main/java/net/william278/husksync/data/StatisticsData.java @@ -4,6 +4,7 @@ import com.google.gson.annotations.SerializedName; import org.jetbrains.annotations.NotNull; import java.util.HashMap; +import java.util.Map; /** * Stores information about a player's statistics @@ -14,30 +15,30 @@ public class StatisticsData { * Map of untyped statistic names to their values */ @SerializedName("untyped_statistics") - public HashMap untypedStatistic; + public Map untypedStatistic; /** * Map of block type statistics to a map of material types to values */ @SerializedName("block_statistics") - public HashMap> blockStatistics; + public Map> blockStatistics; /** * Map of item type statistics to a map of material types to values */ @SerializedName("item_statistics") - public HashMap> itemStatistics; + public Map> itemStatistics; /** * Map of entity type statistics to a map of entity types to values */ @SerializedName("entity_statistics") - public HashMap> entityStatistics; + public Map> entityStatistics; - public StatisticsData(@NotNull HashMap untypedStatistic, - @NotNull HashMap> blockStatistics, - @NotNull HashMap> itemStatistics, - @NotNull HashMap> entityStatistics) { + public StatisticsData(@NotNull Map untypedStatistic, + @NotNull Map> blockStatistics, + @NotNull Map> itemStatistics, + @NotNull Map> entityStatistics) { this.untypedStatistic = untypedStatistic; this.blockStatistics = blockStatistics; this.itemStatistics = itemStatistics; diff --git a/common/src/main/java/net/william278/husksync/data/StatusData.java b/common/src/main/java/net/william278/husksync/data/StatusData.java index 4a8ba2a8..7606ca7e 100644 --- a/common/src/main/java/net/william278/husksync/data/StatusData.java +++ b/common/src/main/java/net/william278/husksync/data/StatusData.java @@ -69,7 +69,7 @@ public class StatusData { public float expProgress; /** - * The player's game mode string (one of "survival", "creative", "adventure", "spectator") + * The player's game mode string (one of "SURVIVAL", "CREATIVE", "ADVENTURE", "SPECTATOR") */ @SerializedName("game_mode") public String gameMode; 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 b602e7d7..93fc1007 100644 --- a/common/src/main/java/net/william278/husksync/data/UserData.java +++ b/common/src/main/java/net/william278/husksync/data/UserData.java @@ -5,24 +5,12 @@ import com.google.gson.JsonSyntaxException; import com.google.gson.annotations.SerializedName; import org.jetbrains.annotations.NotNull; -import java.time.Instant; -import java.util.HashSet; -import java.util.UUID; +import java.util.List; /*** * Stores data about a user */ -public class UserData implements Comparable { - - /** - * The unique identifier for this user data version - */ - protected UUID dataUuidVersion; - - /** - * An epoch milliseconds timestamp of when this data was created - */ - protected long creationTimestamp; +public class UserData { /** * Stores the user's status data, including health, food, etc. @@ -52,7 +40,7 @@ public class UserData implements Comparable { * Stores the set of this user's advancements */ @SerializedName("advancements") - protected HashSet advancementData; + protected List advancementData; /** * Stores the user's set of statistics @@ -74,10 +62,8 @@ public class UserData implements Comparable { public UserData(@NotNull StatusData statusData, @NotNull InventoryData inventoryData, @NotNull InventoryData enderChestData, @NotNull PotionEffectData potionEffectData, - @NotNull HashSet advancementData, @NotNull StatisticsData statisticData, + @NotNull List advancementData, @NotNull StatisticsData statisticData, @NotNull LocationData locationData, @NotNull PersistentDataContainerData persistentDataContainerData) { - this.dataUuidVersion = UUID.randomUUID(); - this.creationTimestamp = Instant.now().toEpochMilli(); this.statusData = statusData; this.inventoryData = inventoryData; this.enderChestData = enderChestData; @@ -91,40 +77,6 @@ public class UserData implements Comparable { protected UserData() { } - /** - * Compare UserData by creation timestamp - * - * @param other the other UserData to be compared - * @return the comparison result; the more recent UserData is greater than the less recent UserData - */ - @Override - public int compareTo(@NotNull UserData other) { - return Long.compare(this.creationTimestamp, other.creationTimestamp); - } - - @NotNull - public static UserData fromJson(String json) throws JsonSyntaxException { - return new GsonBuilder().create().fromJson(json, UserData.class); - } - - @NotNull - public String toJson() { - return new GsonBuilder().create().toJson(this); - } - - public void setMetadata(@NotNull UUID dataUuidVersion, long creationTimestamp) { - this.dataUuidVersion = dataUuidVersion; - this.creationTimestamp = creationTimestamp; - } - - public UUID getDataUuidVersion() { - return dataUuidVersion; - } - - public long getCreationTimestamp() { - return creationTimestamp; - } - public StatusData getStatusData() { return statusData; } @@ -141,7 +93,7 @@ public class UserData implements Comparable { return potionEffectData; } - public HashSet getAdvancementData() { + public List getAdvancementData() { return advancementData; } @@ -156,4 +108,15 @@ public class UserData implements Comparable { public PersistentDataContainerData getPersistentDataContainerData() { return persistentDataContainerData; } + + @NotNull + public static UserData fromJson(String json) throws JsonSyntaxException { + return new GsonBuilder().create().fromJson(json, UserData.class); + } + + @NotNull + public String toJson() { + return new GsonBuilder().create().toJson(this); + } + } diff --git a/common/src/main/java/net/william278/husksync/data/VersionedUserData.java b/common/src/main/java/net/william278/husksync/data/VersionedUserData.java new file mode 100644 index 00000000..2f03a564 --- /dev/null +++ b/common/src/main/java/net/william278/husksync/data/VersionedUserData.java @@ -0,0 +1,40 @@ +package net.william278.husksync.data; + +import org.jetbrains.annotations.NotNull; + +import java.util.Date; +import java.util.UUID; + +/** + * Represents a uniquely versioned and timestamped snapshot of a user's data + * + * @param versionUUID The unique identifier for this user data version + * @param versionTimestamp An epoch milliseconds timestamp of when this data was created + * @param userData The {@link UserData} that has been versioned + */ +public record VersionedUserData(@NotNull UUID versionUUID, @NotNull Date versionTimestamp, + @NotNull UserData userData) implements Comparable { + + public VersionedUserData(@NotNull final UUID versionUUID, @NotNull final Date versionTimestamp, + @NotNull UserData userData) { + this.versionUUID = versionUUID; + this.versionTimestamp = versionTimestamp; + this.userData = userData; + } + + public static VersionedUserData version(@NotNull UserData userData) { + return new VersionedUserData(UUID.randomUUID(), new Date(), userData); + } + + /** + * Compare UserData by creation timestamp + * + * @param other the other UserData to be compared + * @return the comparison result; the more recent UserData is greater than the less recent UserData + */ + @Override + public int compareTo(@NotNull VersionedUserData 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 707d9426..0fb0c374 100644 --- a/common/src/main/java/net/william278/husksync/database/Database.java +++ b/common/src/main/java/net/william278/husksync/database/Database.java @@ -1,6 +1,7 @@ package net.william278.husksync.database; import net.william278.husksync.data.UserData; +import net.william278.husksync.data.VersionedUserData; import net.william278.husksync.player.User; import net.william278.husksync.util.Logger; import net.william278.husksync.util.ResourceReader; @@ -71,10 +72,8 @@ public abstract class Database { * @throws IOException if the resource could not be read */ protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException { - return formatStatementTables( - new String(resourceReader.getResource(schemaFileName) - .readAllBytes(), StandardCharsets.UTF_8)) - .split(";"); + return formatStatementTables(new String(resourceReader.getResource(schemaFileName) + .readAllBytes(), StandardCharsets.UTF_8)).split(";"); } /** @@ -91,9 +90,9 @@ public abstract class Database { /** * Initialize the database and ensure tables are present; create tables if they do not exist. * - * @return A future returning void when complete + * @return A future returning boolean - if the connection could be established. */ - public abstract CompletableFuture initialize(); + public abstract boolean initialize(); /** * Ensure a {@link User} has an entry in the database and that their username is up-to-date @@ -120,20 +119,20 @@ public abstract class Database { public abstract CompletableFuture> getUserByName(@NotNull String username); /** - * Get the current user data for a given user, if it exists. + * 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 user data, if it exists, or an empty optional if it does not + * @return an optional containing the {@link VersionedUserData}, 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 UserData entries for a user from the database. + * Get all {@link VersionedUserData} 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 data + * @return A future returning a list of a user's {@link VersionedUserData} entries */ - public abstract CompletableFuture> getUserData(@NotNull User user); + public abstract CompletableFuture> getUserData(@NotNull User user); /** * Prune user data records for a given user to the maximum value as configured @@ -148,9 +147,14 @@ public abstract class Database { * This will remove the oldest data for the user if the amount of data exceeds the limit as configured * * @param user The user to add data for - * @param userData The data to add + * @param userData The uniquely versioned data to add as a {@link VersionedUserData} * @return A future returning void when complete */ - public abstract CompletableFuture setUserData(@NotNull User user, @NotNull UserData userData); + public abstract CompletableFuture setUserData(@NotNull User user, @NotNull VersionedUserData userData); + + /** + * Close the database connection + */ + public abstract void close(); } 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 ff3af9f7..5ae4901b 100644 --- a/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java +++ b/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java @@ -3,19 +3,24 @@ package net.william278.husksync.database; import com.zaxxer.hikari.HikariDataSource; import net.william278.husksync.config.Settings; import net.william278.husksync.data.UserData; +import net.william278.husksync.data.VersionedUserData; import net.william278.husksync.player.User; import net.william278.husksync.util.Logger; import net.william278.husksync.util.ResourceReader; import org.jetbrains.annotations.NotNull; +import org.xerial.snappy.Snappy; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.sql.*; -import java.time.Instant; import java.util.*; +import java.util.Date; import java.util.concurrent.CompletableFuture; import java.util.logging.Level; public class MySqlDatabase extends Database { + /** * MySQL server hostname */ @@ -40,9 +45,12 @@ public class MySqlDatabase extends Database { private final int hikariKeepAliveTime; private final int hikariConnectionTimeOut; - private static final String DATA_POOL_NAME = "HuskHomesHikariPool"; + private static final String DATA_POOL_NAME = "HuskSyncHikariPool"; - private HikariDataSource dataSource; + /** + * The Hikari data source - a pool of database connections that can be fetched on-demand + */ + private HikariDataSource connectionPool; public MySqlDatabase(@NotNull Settings settings, @NotNull ResourceReader resourceReader, @NotNull Logger logger) { super(settings.getStringValue(Settings.ConfigOption.DATABASE_PLAYERS_TABLE_NAME), @@ -69,31 +77,31 @@ public class MySqlDatabase extends Database { * @throws SQLException if the connection fails for some reason */ private Connection getConnection() throws SQLException { - return dataSource.getConnection(); + return connectionPool.getConnection(); } @Override - public CompletableFuture initialize() { - return CompletableFuture.runAsync(() -> { + public boolean initialize() { + try { // Create jdbc driver connection url final String jdbcUrl = "jdbc:mysql://" + mySqlHost + ":" + mySqlPort + "/" + mySqlDatabaseName + mySqlConnectionParameters; - dataSource = new HikariDataSource(); - dataSource.setJdbcUrl(jdbcUrl); + connectionPool = new HikariDataSource(); + connectionPool.setJdbcUrl(jdbcUrl); // Authenticate - dataSource.setUsername(mySqlUsername); - dataSource.setPassword(mySqlPassword); + connectionPool.setUsername(mySqlUsername); + connectionPool.setPassword(mySqlPassword); // Set various additional parameters - dataSource.setMaximumPoolSize(hikariMaximumPoolSize); - dataSource.setMinimumIdle(hikariMinimumIdle); - dataSource.setMaxLifetime(hikariMaximumLifetime); - dataSource.setKeepaliveTime(hikariKeepAliveTime); - dataSource.setConnectionTimeout(hikariConnectionTimeOut); - dataSource.setPoolName(DATA_POOL_NAME); + connectionPool.setMaximumPoolSize(hikariMaximumPoolSize); + connectionPool.setMinimumIdle(hikariMinimumIdle); + connectionPool.setMaxLifetime(hikariMaximumLifetime); + connectionPool.setKeepaliveTime(hikariKeepAliveTime); + connectionPool.setConnectionTimeout(hikariConnectionTimeOut); + connectionPool.setPoolName(DATA_POOL_NAME); // Prepare database schema; make tables if they don't exist - try (Connection connection = dataSource.getConnection()) { + try (Connection connection = connectionPool.getConnection()) { // Load database schema CREATE statements from schema file final String[] databaseSchema = getSchemaStatements("database/mysql_schema.sql"); try (Statement statement = connection.createStatement()) { @@ -101,10 +109,14 @@ public class MySqlDatabase extends Database { statement.execute(tableCreationStatement); } } + return true; } catch (SQLException | IOException e) { - getLogger().log(Level.SEVERE, "An error occurred creating tables on the MySQL database: ", e); + getLogger().log(Level.SEVERE, "Failed to perform database setup: " + e.getMessage()); } - }); + } catch (Exception e) { + e.printStackTrace(); + } + return false; } @Override @@ -194,7 +206,7 @@ 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(""" @@ -206,13 +218,17 @@ public class MySqlDatabase extends Database { statement.setString(1, user.uuid.toString()); final ResultSet resultSet = statement.executeQuery(); if (resultSet.next()) { - final UserData data = UserData.fromJson(resultSet.getString("data")); - data.setMetadata(UUID.fromString(resultSet.getString("version_uuid")), - resultSet.getTimestamp("timestamp").toInstant().toEpochMilli()); - return Optional.of(data); + final Blob blob = resultSet.getBlob("data"); + final byte[] compressedDataJson = blob.getBytes(1, (int) blob.length()); + blob.free(); + return Optional.of(new VersionedUserData( + UUID.fromString(resultSet.getString("version_uuid")), + Date.from(resultSet.getTimestamp("timestamp").toInstant()), + UserData.fromJson(new String(Snappy.uncompress(compressedDataJson), + StandardCharsets.UTF_8)))); } } - } catch (SQLException e) { + } catch (SQLException | IOException e) { getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e); } return Optional.empty(); @@ -220,9 +236,9 @@ public class MySqlDatabase extends Database { } @Override - public CompletableFuture> getUserData(@NotNull User user) { + public CompletableFuture> getUserData(@NotNull User user) { return CompletableFuture.supplyAsync(() -> { - final ArrayList retrievedData = new ArrayList<>(); + final List retrievedData = new ArrayList<>(); try (Connection connection = getConnection()) { try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" SELECT `version_uuid`, `timestamp`, `data` @@ -232,14 +248,19 @@ public class MySqlDatabase extends Database { statement.setString(1, user.uuid.toString()); final ResultSet resultSet = statement.executeQuery(); while (resultSet.next()) { - final UserData data = UserData.fromJson(resultSet.getString("data")); - data.setMetadata(UUID.fromString(resultSet.getString("version_uuid")), - resultSet.getTimestamp("timestamp").toInstant().toEpochMilli()); + final Blob blob = resultSet.getBlob("data"); + final byte[] compressedDataJson = blob.getBytes(1, (int) blob.length()); + blob.free(); + final VersionedUserData data = new VersionedUserData( + UUID.fromString(resultSet.getString("version_uuid")), + Date.from(resultSet.getTimestamp("timestamp").toInstant()), + UserData.fromJson(new String(Snappy.uncompress(compressedDataJson), + StandardCharsets.UTF_8))); retrievedData.add(data); } return retrievedData; } - } catch (SQLException e) { + } catch (SQLException | IOException e) { getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e); } return retrievedData; @@ -256,7 +277,7 @@ public class MySqlDatabase extends Database { try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" DELETE FROM `%data_table%` WHERE `version_uuid`=?"""))) { - statement.setString(1, dataToDelete.getDataUuidVersion().toString()); + statement.setString(1, dataToDelete.versionUUID().toString()); statement.executeUpdate(); } } catch (SQLException e) { @@ -268,7 +289,7 @@ public class MySqlDatabase extends Database { } @Override - public CompletableFuture setUserData(@NotNull User user, @NotNull UserData userData) { + public CompletableFuture setUserData(@NotNull User user, @NotNull VersionedUserData userData) { return CompletableFuture.runAsync(() -> { try (Connection connection = getConnection()) { try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" @@ -276,14 +297,25 @@ public class MySqlDatabase extends Database { (`player_uuid`,`version_uuid`,`timestamp`,`data`) VALUES (?,?,?,?);"""))) { statement.setString(1, user.uuid.toString()); - statement.setString(2, userData.getDataUuidVersion().toString()); - statement.setTimestamp(3, Timestamp.from(Instant.ofEpochMilli(userData.getCreationTimestamp()))); - statement.setString(4, userData.toJson()); + statement.setString(2, userData.versionUUID().toString()); + statement.setTimestamp(3, Timestamp.from(userData.versionTimestamp().toInstant())); + statement.setBlob(4, new ByteArrayInputStream(Snappy + .compress(userData.userData().toJson().getBytes(StandardCharsets.UTF_8)))); statement.executeUpdate(); } - } catch (SQLException e) { + } catch (SQLException | IOException e) { getLogger().log(Level.SEVERE, "Failed to set user data in the database", e); } - }).thenRunAsync(() -> pruneUserDataRecords(user).join()); + })/*.thenRunAsync(() -> pruneUserDataRecords(user).join())*/; } + + @Override + public void close() { + if (connectionPool != null) { + if (!connectionPool.isClosed()) { + connectionPool.close(); + } + } + } + } 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 9fba36f7..3c0f14a1 100644 --- a/common/src/main/java/net/william278/husksync/listener/EventListener.java +++ b/common/src/main/java/net/william278/husksync/listener/EventListener.java @@ -12,12 +12,25 @@ import java.util.concurrent.CompletableFuture; public class EventListener { + /** + * The plugin instance + */ private final HuskSync huskSync; + + /** + * Set of UUIDs current awaiting item synchronization. Events will be cancelled for these users + */ private final HashSet usersAwaitingSync; + /** + * Whether the plugin is currently being disabled + */ + private boolean disabling; + protected EventListener(@NotNull HuskSync huskSync) { this.huskSync = huskSync; this.usersAwaitingSync = new HashSet<>(); + this.disabling = false; } public final void handlePlayerJoin(@NotNull OnlineUser user) { @@ -27,7 +40,7 @@ public class EventListener { userData -> user.setData(userData, huskSync.getSettings()).join(), () -> huskSync.getDatabase().getCurrentUserData(user).thenAccept( databaseUserData -> databaseUserData.ifPresent( - data -> user.setData(data, huskSync.getSettings()).join())).join())).thenRunAsync( + data -> user.setData(data.userData(), huskSync.getSettings()).join())).join())).thenRunAsync( () -> { huskSync.getLocales().getLocale("synchronisation_complete").ifPresent(user::sendActionBar); usersAwaitingSync.remove(user.uuid); @@ -36,16 +49,35 @@ public class EventListener { } public final void handlePlayerQuit(@NotNull OnlineUser user) { - user.getUserData().thenAccept(userData -> huskSync.getRedisManager() - .setPlayerData(user, userData, RedisManager.RedisKeyType.SERVER_CHANGE).thenRun( - () -> huskSync.getDatabase().setUserData(user, userData).join())); + if (disabling) { + return; + } + user.getUserData().thenAccept(userData -> { + System.out.println(userData.userData().toJson()); + huskSync.getRedisManager() + .setUserData(user, userData.userData(), RedisManager.RedisKeyType.SERVER_CHANGE).thenRun( + () -> huskSync.getDatabase().setUserData(user, userData).join()); + }); } public final void handleWorldSave(@NotNull List usersInWorld) { + if (disabling) { + return; + } CompletableFuture.runAsync(() -> usersInWorld.forEach(user -> huskSync.getDatabase().setUserData(user, user.getUserData().join()).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()).join()); + + huskSync.getDatabase().close(); + huskSync.getRedisManager().close(); + } + public final boolean cancelPlayerEvent(@NotNull OnlineUser user) { return usersAwaitingSync.contains(user.uuid); } 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 c401cefd..3dcab98a 100644 --- a/common/src/main/java/net/william278/husksync/player/OnlineUser.java +++ b/common/src/main/java/net/william278/husksync/player/OnlineUser.java @@ -5,7 +5,7 @@ import net.william278.husksync.config.Settings; import net.william278.husksync.data.*; import org.jetbrains.annotations.NotNull; -import java.util.HashSet; +import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -25,6 +25,22 @@ public abstract class OnlineUser extends User { */ public abstract CompletableFuture getStatus(); + /** + * Set the player's {@link StatusData} + * + * @param statusData the player's {@link StatusData} + * @param setHealth whether to set the player's health + * @param setMaxHealth whether to set the player's max health + * @param setHunger whether to set the player's hunger + * @param setExperience whether to set the player's experience + * @param setGameMode whether to set the player's game mode + * @return a future returning void when complete + */ + public abstract CompletableFuture setStatus(@NotNull StatusData statusData, + final boolean setHealth, final boolean setMaxHealth, + final boolean setHunger, final boolean setExperience, + final boolean setGameMode, boolean setFlying); + /** * Get the player's inventory {@link InventoryData} contents * @@ -32,6 +48,14 @@ public abstract class OnlineUser extends User { */ public abstract CompletableFuture getInventory(); + /** + * Set the player's {@link InventoryData} + * + * @param inventoryData The player's {@link InventoryData} + * @return a future returning void when complete + */ + public abstract CompletableFuture setInventory(@NotNull InventoryData inventoryData); + /** * Get the player's ender chest {@link InventoryData} contents * @@ -39,6 +63,15 @@ public abstract class OnlineUser extends User { */ public abstract CompletableFuture getEnderChest(); + /** + * Set the player's {@link InventoryData} + * + * @param enderChestData The player's {@link InventoryData} + * @return a future returning void when complete + */ + public abstract CompletableFuture setEnderChest(@NotNull InventoryData enderChestData); + + /** * Get the player's {@link PotionEffectData} * @@ -46,12 +79,28 @@ public abstract class OnlineUser extends User { */ public abstract CompletableFuture getPotionEffects(); + /** + * Set the player's {@link PotionEffectData} + * + * @param potionEffectData The player's {@link PotionEffectData} + * @return a future returning void when complete + */ + public abstract CompletableFuture setPotionEffects(@NotNull PotionEffectData potionEffectData); + /** * Get the player's set of {@link AdvancementData} * * @return the player's set of {@link AdvancementData} */ - public abstract CompletableFuture> getAdvancements(); + public abstract CompletableFuture> getAdvancements(); + + /** + * Set the player's {@link AdvancementData} + * + * @param advancementData List of the player's {@link AdvancementData} + * @return a future returning void when complete + */ + public abstract CompletableFuture setAdvancements(@NotNull List advancementData); /** * Get the player's {@link StatisticsData} @@ -60,6 +109,14 @@ public abstract class OnlineUser extends User { */ public abstract CompletableFuture getStatistics(); + /** + * Set the player's {@link StatisticsData} + * + * @param statisticsData The player's {@link StatisticsData} + * @return a future returning void when complete + */ + public abstract CompletableFuture setStatistics(@NotNull StatisticsData statisticsData); + /** * Get the player's {@link LocationData} * @@ -67,6 +124,14 @@ public abstract class OnlineUser extends User { */ public abstract CompletableFuture getLocation(); + /** + * Set the player's {@link LocationData} + * + * @param locationData the player's {@link LocationData} + * @return a future returning void when complete + */ + public abstract CompletableFuture setLocation(@NotNull LocationData locationData); + /** * Get the player's {@link PersistentDataContainerData} * @@ -74,6 +139,14 @@ public abstract class OnlineUser extends User { */ public abstract CompletableFuture getPersistentDataContainer(); + /** + * Set the player's {@link PersistentDataContainerData} + * + * @param persistentDataContainerData The player's {@link PersistentDataContainerData} to set + * @return A future returning void when complete + */ + public abstract CompletableFuture setPersistentDataContainer(@NotNull PersistentDataContainerData persistentDataContainerData); + /** * Set {@link UserData} to a player * @@ -81,7 +154,37 @@ public abstract class OnlineUser extends User { * @param settings Plugin settings, for determining what needs setting * @return a future that will be completed when done */ - public abstract CompletableFuture setData(@NotNull UserData data, @NotNull Settings settings); + public final CompletableFuture setData(@NotNull UserData data, @NotNull Settings settings) { + return CompletableFuture.runAsync(() -> { + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)) { + setInventory(data.getInventoryData()).join(); + } + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ENDER_CHESTS)) { + setEnderChest(data.getEnderChestData()).join(); + } + setStatus(data.getStatusData(), settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HEALTH), + settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_MAX_HEALTH), + settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HUNGER), + settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_EXPERIENCE), + settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_GAME_MODE), + settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION)).join(); + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_POTION_EFFECTS)) { + setPotionEffects(data.getPotionEffectData()).join(); + } + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ADVANCEMENTS)) { + setAdvancements(data.getAdvancementData()).join(); + } + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_STATISTICS)) { + setStatistics(data.getStatisticData()).join(); + } + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER)) { + setPersistentDataContainer(data.getPersistentDataContainerData()).join(); + } + if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION)) { + setLocation(data.getLocationData()).join(); + } + }); + } /** * Dispatch a MineDown-formatted message to this player @@ -110,10 +213,11 @@ public abstract class OnlineUser extends User { * * @return the player's current {@link UserData} */ - public final CompletableFuture getUserData() { - return CompletableFuture.supplyAsync(() -> new UserData(getStatus().join(), getInventory().join(), - getEnderChest().join(), getPotionEffects().join(), getAdvancements().join(), - getStatistics().join(), getLocation().join(), getPersistentDataContainer().join())); + public final CompletableFuture getUserData() { + return CompletableFuture.supplyAsync( + () -> VersionedUserData.version(new UserData(getStatus().join(), getInventory().join(), + getEnderChest().join(), getPotionEffects().join(), getAdvancements().join(), + getStatistics().join(), getLocation().join(), getPersistentDataContainer().join()))); } } 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 9c054fb2..2e5b36f3 100644 --- a/common/src/main/java/net/william278/husksync/redis/RedisManager.java +++ b/common/src/main/java/net/william278/husksync/redis/RedisManager.java @@ -4,65 +4,130 @@ import net.william278.husksync.config.Settings; import net.william278.husksync.data.UserData; import net.william278.husksync.player.User; import org.jetbrains.annotations.NotNull; +import org.xerial.snappy.Snappy; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.exceptions.JedisException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.CompletableFuture; +/** + * Manages the connection to the Redis server, handling the caching of user data + */ public class RedisManager { private static final String KEY_NAMESPACE = "husksync:"; private static String clusterId = ""; - private final JedisPool jedisPool; - private RedisManager(@NotNull Settings settings) { + private final JedisPoolConfig jedisPoolConfig; + + private final String redisHost; + private final int redisPort; + private final String redisPassword; + private final boolean redisUseSsl; + + private JedisPool jedisPool; + + public RedisManager(@NotNull Settings settings) { clusterId = settings.getStringValue(Settings.ConfigOption.CLUSTER_ID); - JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); - jedisPoolConfig.setMaxIdle(0); - jedisPoolConfig.setTestOnBorrow(true); - jedisPoolConfig.setTestOnReturn(true); - if (settings.getStringValue(Settings.ConfigOption.REDIS_PASSWORD).isBlank()) { - jedisPool = new JedisPool(jedisPoolConfig, - settings.getStringValue(Settings.ConfigOption.REDIS_HOST), - settings.getIntegerValue(Settings.ConfigOption.REDIS_PORT), - 0, - settings.getBooleanValue(Settings.ConfigOption.REDIS_USE_SSL)); - } else { - jedisPool = new JedisPool(jedisPoolConfig, - settings.getStringValue(Settings.ConfigOption.REDIS_HOST), - settings.getIntegerValue(Settings.ConfigOption.REDIS_PORT), - 0, - settings.getStringValue(Settings.ConfigOption.REDIS_PASSWORD), - settings.getBooleanValue(Settings.ConfigOption.REDIS_USE_SSL)); + this.redisHost = settings.getStringValue(Settings.ConfigOption.REDIS_HOST); + this.redisPort = settings.getIntegerValue(Settings.ConfigOption.REDIS_PORT); + this.redisPassword = settings.getStringValue(Settings.ConfigOption.REDIS_PASSWORD); + this.redisUseSsl = settings.getBooleanValue(Settings.ConfigOption.REDIS_USE_SSL); + + // Configure the jedis pool + this.jedisPoolConfig = new JedisPoolConfig(); + this.jedisPoolConfig.setMaxIdle(0); + this.jedisPoolConfig.setTestOnBorrow(true); + this.jedisPoolConfig.setTestOnReturn(true); + } + + /** + * Initialize the redis connection pool + * + * @return a future returning void when complete + */ + public CompletableFuture initialize() { + return CompletableFuture.supplyAsync(() -> { + if (redisPassword.isBlank()) { + jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, 0, redisUseSsl); + } else { + jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, 0, redisPassword, redisUseSsl); + } + try { + jedisPool.getResource().ping(); + } catch (JedisException e) { + return false; + } + return true; + }); + } + + /** + * Set a user's data to the Redis server + * + * @param user the user to set data for + * @param userData the user's data to set + * @param redisKeyType the type of key to set the data with. This determines the time to live for the data. + * @return a future returning void when complete + */ + public CompletableFuture setUserData(@NotNull User user, @NotNull UserData userData, + @NotNull RedisKeyType redisKeyType) { + try { + return CompletableFuture.runAsync(() -> { + try (Jedis jedis = jedisPool.getResource()) { + // Set the user's data as a compressed byte array of the json using Snappy + jedis.setex(getKey(redisKeyType, user.uuid), redisKeyType.timeToLive, + Snappy.compress(userData.toJson().getBytes(StandardCharsets.UTF_8))); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * Fetch a user's data from the Redis server + * + * @param user The user to fetch data for + * @param redisKeyType The type of key to fetch + * @return The user's data, if it's present on the database. Otherwise, an empty optional. + */ + public CompletableFuture> getUserData(@NotNull User user, + @NotNull RedisKeyType redisKeyType) { + return CompletableFuture.supplyAsync(() -> { + try (Jedis jedis = jedisPool.getResource()) { + final byte[] compressedJson = jedis.get(getKey(redisKeyType, user.uuid)); + if (compressedJson == null) { + return Optional.empty(); + } + // Use Snappy to decompress the json + return Optional.of(UserData.fromJson(new String(Snappy.uncompress(compressedJson), + StandardCharsets.UTF_8))); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + public void close() { + if (jedisPool != null) { + if (!jedisPool.isClosed()) { + jedisPool.close(); + } } } - public CompletableFuture setPlayerData(@NotNull User user, @NotNull UserData userData, - @NotNull RedisKeyType redisKeyType) { - return CompletableFuture.runAsync(() -> { - try (Jedis jedis = jedisPool.getResource()) { - jedis.setex(redisKeyType.getKeyPrefix() + user.uuid.toString(), - redisKeyType.timeToLive, userData.toJson()); - } - }); - } - - public CompletableFuture> getUserData(@NotNull User user, @NotNull RedisKeyType redisKeyType) { - return CompletableFuture.supplyAsync(() -> { - try (Jedis jedis = jedisPool.getResource()) { - final String json = jedis.get(redisKeyType.getKeyPrefix() + user.uuid.toString()); - if (json == null) { - return Optional.empty(); - } - return Optional.of(UserData.fromJson(json)); - } - }); - } - - public static CompletableFuture initialize(@NotNull Settings settings) { - return CompletableFuture.supplyAsync(() -> new RedisManager(settings)); + private static byte[] getKey(@NotNull RedisKeyType keyType, @NotNull UUID uuid) { + return (keyType.getKeyPrefix() + ":" + uuid).getBytes(StandardCharsets.UTF_8); } public enum RedisKeyType { @@ -77,7 +142,7 @@ public class RedisManager { @NotNull public String getKeyPrefix() { - return KEY_NAMESPACE.toLowerCase() + ":" + clusterId.toLowerCase() + ":" + name().toLowerCase() + ":"; + return KEY_NAMESPACE.toLowerCase() + ":" + clusterId.toLowerCase() + ":" + name().toLowerCase(); } } diff --git a/common/src/main/resources/config.yml b/common/src/main/resources/config.yml index 70c75274..0a851709 100644 --- a/common/src/main/resources/config.yml +++ b/common/src/main/resources/config.yml @@ -48,4 +48,6 @@ synchronization: game_mode: true statistics: true persistent_data_container: true - location: false \ No newline at end of file + location: false + +config_version: 1 \ No newline at end of file diff --git a/common/src/main/resources/database/mysql_scehma.sql b/common/src/main/resources/database/mysql_schema.sql similarity index 72% rename from common/src/main/resources/database/mysql_scehma.sql rename to common/src/main/resources/database/mysql_schema.sql index f8936879..964c6f27 100644 --- a/common/src/main/resources/database/mysql_scehma.sql +++ b/common/src/main/resources/database/mysql_schema.sql @@ -10,10 +10,10 @@ CREATE TABLE IF NOT EXISTS `%players_table%` # Create the player data table if it does not exist CREATE TABLE IF NOT EXISTS `%data_table%` ( - `version_uuid` char(36) NOT NULL, - `player_uuid` char(36) NOT NULL, - `timestamp` datetime NOT NULL, - `data` json NOT NULL, + `version_uuid` char(36) NOT NULL, + `player_uuid` char(36) NOT NULL, + `timestamp` datetime NOT NULL, + `data` mediumblob NOT NULL, PRIMARY KEY (`version_uuid`), FOREIGN KEY (`player_uuid`) REFERENCES `%players_table%` (`uuid`) ON DELETE CASCADE diff --git a/common/src/main/resources/locales/de-de.yml b/common/src/main/resources/locales/de-de.yml new file mode 100644 index 00000000..1813606b --- /dev/null +++ b/common/src/main/resources/locales/de-de.yml @@ -0,0 +1,14 @@ +synchronisation_complete: '[Daten synchronisiert!](#00fb9a)' +viewing_inventory_of: '[Einsicht in das Inventar von](#00fb9a) [%1%](#00fb9a bold)' +viewing_ender_chest_of: '[Einsicht in die Endertruhe von](#00fb9a) [%1%](#00fb9a bold)' +reload_complete: '[HuskSync](#00fb9a bold) [| Die Konfigurations- und Meldungsdateien wurden aktualisiert.](#00fb9a)' +error_invalid_syntax: '[Fehler:](#ff3300) [Falsche Syntax. Nutze: %1%](#ff7e5e)' +error_invalid_player: '[Fehler:](#ff3300) [Dieser Spieler konnte nicht gefunden werden](#ff7e5e)' +error_no_permission: '[Fehler:](#ff3300) [Du hast nicht die benötigten Berechtigungen um diesen Befehl auszuführen](#ff7e5e)' +error_cannot_view_inventory_online: '[Fehler:](#ff3300) [Du kannst nicht über HuskSync auf das Inventar eines Online-Spielers zugreifen](#ff7e5e)' +error_cannot_view_ender_chest_online: '[Fehler:](#ff3300) [Du kannst nicht über HuskSync auf die Endertruhe eines Online-Spielers zugreifen](#ff7e5e)' +error_cannot_view_own_inventory: '[Fehler:](#ff3300) [Du kannst nicht auf dein eigenes Inventar zugreifen!](#ff7e5e)' +error_cannot_view_own_ender_chest: '[Fehler:](#ff3300) [Du kannst nicht auf deine eigene Endertruhe zugreifen!](#ff7e5e)' +error_console_command_only: '[Fehler:](#ff3300) [Dieser Befehl kann nur über die %1% Konsole ausgeführt werden](#ff7e5e)' +error_no_servers_proxied: '[Fehler:](#ff3300) [Vorgang konnte nicht verarbeitet werden; Es sind keine Server online, auf denen HuskSync installiert ist. Bitte stelle sicher, dass HuskSync sowohl auf dem Proxy-Server als auch auf allen Servern installiert ist, zwischen denen du Daten synchronisieren möchtest.](#ff7e5e)' +error_invalid_cluster: '[Fehler:](#ff3300) [Bitte gib die ID eines gültigen Clusters an.](#ff7e5e)' diff --git a/common/src/main/resources/locales/es-es.yml b/common/src/main/resources/locales/es-es.yml new file mode 100644 index 00000000..fd644330 --- /dev/null +++ b/common/src/main/resources/locales/es-es.yml @@ -0,0 +1,14 @@ +synchronisation_complete: '[Datos sincronizados!](#00fb9a)' +viewing_inventory_of: '[Viendo el inventario de](#00fb9a) [%1%](#00fb9a bold)' +viewing_ender_chest_of: '[Viendo el Ender Chest de](#00fb9a) [%1%](#00fb9a bold)' +reload_complete: '[HuskSync](#00fb9a bold) [| Se ha reiniciado la configuración y los archivos de los mensajes.](#00fb9a)' +error_invalid_syntax: '[Error:](#ff3300) [Sintaxis incorrecta. Uso: %1%](#ff7e5e)' +error_invalid_player: '[Error:](#ff3300) [No se ha podido encontrar a ese jugador](#ff7e5e)' +error_no_permission: '[Error:](#ff3300) [No tienes permiso para ejecutar este comando](#ff7e5e)' +error_cannot_view_inventory_online: '[Error:](#ff3300) [A traves de HuskSync no puedes acceder al inventario de un jugador conectado](#ff7e5e)' +error_cannot_view_ender_chest_online: '[Error:](#ff3300) [A traves de HuskSync no puedes acceder al Ender Chest de un jugador conectado.](#ff7e5e)' +error_cannot_view_own_inventory: '[Error:](#ff3300) [No puedes acceder a tu inventario!](#ff7e5e)' +error_cannot_view_own_ender_chest: '[Error:](#ff3300) [No puedes acceder a tu Ender Chest!](#ff7e5e)' +error_console_command_only: '[Error:](#ff3300) [Ese comando solo puede ser ejecutado desde la %1% consola](#ff7e5e)' +error_no_servers_proxied: '[Error:](#ff3300) [Ha ocurrido un error mientras se procesaba la acción; no hay servidores online con HusckSync instalado. Por favor, asegúrate que HuskSync está instalado tanto en el proxy como en todos los servidores entre los que quieres sincronizar datos.](#ff7e5e)' +error_invalid_cluster: '[Error:](#ff3300) [Por favor, especifica la ID de un cluster válido.](#ff7e5e)' diff --git a/common/src/main/resources/locales/ja-jp.yml b/common/src/main/resources/locales/ja-jp.yml new file mode 100644 index 00000000..370a1567 --- /dev/null +++ b/common/src/main/resources/locales/ja-jp.yml @@ -0,0 +1,14 @@ +synchronisation_complete: '[データが同期されました!](#00fb9a)' +viewing_inventory_of: '[%1%](#00fb9a bold) [のインベントリを表示します](#00fb9a) ' +viewing_ender_chest_of: '[%1%](#00fb9a bold) [のエンダーチェストを表示します](#00fb9a) ' +reload_complete: '[HuskSync](#00fb9a bold) [| 設定ファイルとメッセージファイルを再読み込みしました。](#00fb9a)' +error_invalid_syntax: '[Error:](#ff3300) [構文が正しくありません。使用法: %1%](#ff7e5e)' +error_invalid_player: '[Error:](#ff3300) [そのプレイヤーは見つかりませんでした](#ff7e5e)' +error_no_permission: '[Error:](#ff3300) [このコマンドを実行する権限がありません](#ff7e5e)' +error_cannot_view_inventory_online: '[Error:](#ff3300) [HuskSyncからオンラインプレイヤーのインベントリにはアクセスできません](#ff7e5e)' +error_cannot_view_ender_chest_online: '[Error:](#ff3300) [HuskSyncからオンラインプレイヤーのエンダーチェストにはアクセスできません](#ff7e5e)' +error_cannot_view_own_inventory: '[Error:](#ff3300) [自分のインベントリにはアクセスできません!](#ff7e5e)' +error_cannot_view_own_ender_chest: '[Error:](#ff3300) [自分のエンダーチェストにはアクセスできません!](#ff7e5e)' +error_console_command_only: '[Error:](#ff3300) [そのコマンドは%1%コンソールからのみ実行できます](#ff7e5e)' +error_no_servers_proxied: '[Error:](#ff3300) [操作の処理に失敗; HuskSyncがインストールされているサーバーがオンラインになっていません。プロキシサーバーとデータを同期させたいすべてのサーバーにHuskSyncがインストールされていることを確認してください。](#ff7e5e)' +error_invalid_cluster: '[Error:](#ff3300) [有効なクラスターのIDを指定してください。](#ff7e5e)' diff --git a/common/src/main/resources/locales/uk-ua.yml b/common/src/main/resources/locales/uk-ua.yml new file mode 100644 index 00000000..58c5e838 --- /dev/null +++ b/common/src/main/resources/locales/uk-ua.yml @@ -0,0 +1,14 @@ +synchronisation_complete: '[Дані синхронізовано!](#00fb9a)' +viewing_inventory_of: '[Переглядання інвентару](#00fb9a) [%1%](#00fb9a bold)' +viewing_ender_chest_of: '[Переглядання скрині енду](#00fb9a) [%1%](#00fb9a bold)' +reload_complete: '[HuskSync](#00fb9a bold) [| Перезавантажено конфіґ та файли повідомлень.](#00fb9a)' +error_invalid_syntax: '[Помилка:](#ff3300) [Неправильний синтакс. Використання: %1%](#ff7e5e)' +error_invalid_player: '[Помилка:](#ff3300) [Гравця не знайдено](#ff7e5e)' +error_no_permission: '[Помилка:](#ff3300) [Ввас немає дозволу на використання цієї команди](#ff7e5e)' +error_cannot_view_inventory_online: '[Помилка:](#ff3300) [Ви не можете переглядати інвентар гравців, що знаходяться онлайн, з допомогую HuskSync](#ff7e5e)' +error_cannot_view_ender_chest_online: '[Помилка:](#ff3300) [Ви не можете переглядати скриню енду гравців, що знаходяться онлайн, з допомогую HuskSync](#ff7e5e)' +error_cannot_view_own_inventory: '[Помилка:](#ff3300) [Ви не можете переглядати власний інвентар!](#ff7e5e)' +error_cannot_view_own_ender_chest: '[Помилка:](#ff3300) [Ви не можете переглядати власну скриню енду!](#ff7e5e)' +error_console_command_only: '[Помилка:](#ff3300) [Ця команда може бути використана лише з допомогою %1% консолі](#ff7e5e)' +error_no_servers_proxied: '[Помилка:](#ff3300) [Не вдалося опрацювати операцію; не знайдено жодного сервера із встановленим HuskSync. Запевніться, будьласка, що HuskSync встановлено на Проксі та усіх серверах між якими ви хочете синхроніхувати дані.](#ff7e5e)' +error_invalid_cluster: '[Помилка:](#ff3300) [Зазнчте будь ласка ID слушного кластеру.](#ff7e5e)' \ No newline at end of file diff --git a/common/src/main/resources/locales/zh-cn.yml b/common/src/main/resources/locales/zh-cn.yml new file mode 100644 index 00000000..529f91aa --- /dev/null +++ b/common/src/main/resources/locales/zh-cn.yml @@ -0,0 +1,14 @@ +synchronisation_complete: '[数据同步完成](#00fb9a)' +viewing_inventory_of: '[查看玩家背包:](#00fb9a) [%1%](#00fb9a bold)' +viewing_ender_chest_of: '[查看玩家末影箱:](#00fb9a) [%1%](#00fb9a bold)' +reload_complete: '[HuskSync](#00fb9a bold) [| 配置与语言文件重载完成.](#00fb9a)' +error_invalid_syntax: '[错误:](#ff3300) [格式错误. 使用方法: %1%](#ff7e5e)' +error_invalid_player: '[错误:](#ff3300) [未找到目标玩家](#ff7e5e)' +error_no_permission: '[错误:](#ff3300) [你没有权限执行此命令](#ff7e5e)' +error_cannot_view_inventory_online: '[错误:](#ff3300) [你不能在玩家在线时通过 HuskSync 查看与编辑玩家物品栏](#ff7e5e)' +error_cannot_view_ender_chest_online: '[错误:](#ff3300) [你不能在玩家在线时通过 HuskSync 查看与编辑玩家末影箱](#ff7e5e)' +error_cannot_view_own_inventory: '[错误:](#ff3300) [你不能查看和编辑自己的物品栏!](#ff7e5e)' +error_cannot_view_own_ender_chest: '[错误:](#ff3300) [你不能查看和编辑自己的末影箱!](#ff7e5e)' +error_console_command_only: '[错误:](#ff3300) [该命令只能由 %1% 控制台执行](#ff7e5e)' +error_no_servers_proxied: '[错误:](#ff3300) [操作处理失败; 没有任何安装了 HuskSync 的后端服务器在线. 请确认 HuskSync 已在 BungeeCord/Velocity 等代理服务器和所有你希望互相同步数据的后端服务器间安装.](#ff7e5e)' +error_invalid_cluster: '[错误:](#ff3300) [请指定一个有效的集群(cluster) ID.](#ff7e5e)' diff --git a/common/src/main/resources/locales/zh-tw.yml b/common/src/main/resources/locales/zh-tw.yml new file mode 100644 index 00000000..0aa3afaa --- /dev/null +++ b/common/src/main/resources/locales/zh-tw.yml @@ -0,0 +1,14 @@ +synchronisation_complete: '[資料已同步!!](#00fb9a)' +viewing_inventory_of: '[查看](#00fb9a) [%1%](#00fb9a bold) [的背包](#00fb9a)' +viewing_ender_chest_of: '[查看](#00fb9a) [%1%](#00fb9a bold) [的終界箱](#00fb9a)' +reload_complete: '[HuskSync](#00fb9a bold) [| 已重新載入配置和訊息文件](#00fb9a)' +error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法: %1%](#ff7e5e)' +error_invalid_player: '[錯誤:](#ff3300) [找不到這位玩家](#ff7e5e)' +error_no_permission: '[錯誤:](#ff3300) [您沒有權限執行這個指令](#ff7e5e)' +error_cannot_view_inventory_online: '[錯誤:](#ff3300) [您無法通過 HuskSync 查看在線玩家的背包](#ff7e5e)' +error_cannot_view_ender_chest_online: '[錯誤:](#ff3300) [您無法通過 HuskSync 查看在線玩家的終界箱](#ff7e5e)' +error_cannot_view_own_inventory: '[錯誤:](#ff3300) [您無法查看自己的背包!](#ff7e5e)' +error_cannot_view_own_ender_chest: '[錯誤:](#ff3300) [你無法查看自己的終界箱!](#ff7e5e)' +error_console_command_only: '[錯誤:](#ff3300) [該指令只能通過 %1% 控制台運行](#ff7e5e)' +error_no_servers_proxied: '[錯誤:](#ff3300) [處理操作失敗: 沒有安裝 HuskSync 的伺服器在線。 請確保在 Proxy 伺服器和您希望在其他同步數據的所有伺服器上都安裝了 HuskSync。](#ff7e5e)' +error_invalid_cluster: '[錯誤:](#ff3300) [請提供有效的 Cluster ID](#ff7e5e)' \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 5b3490b3..8d5cd6a9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,8 +3,5 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8' org.gradle.daemon=true javaVersion=16 -plugin_version=1.5 -plugin_archive=husksync - -jedis_version=4.2.3 -sqlite_driver_version=3.36.0.3 \ No newline at end of file +plugin_version=2.0 +plugin_archive=husksync \ No newline at end of file diff --git a/plugin/build.gradle b/plugin/build.gradle index 021a17b0..4fef6474 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -4,7 +4,7 @@ plugins { dependencies { implementation project(path: ':bukkit', configuration: 'shadow') - //implementation project(path: ':api', configuration: 'shadow') + implementation project(path: ':api', configuration: 'shadow') } shadowJar { diff --git a/velocity/build.gradle b/velocity/build.gradle deleted file mode 100644 index 05a9eb72..00000000 --- a/velocity/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -dependencies { - implementation project(path: ':common') - - implementation 'com.zaxxer:HikariCP:5.0.1' - implementation 'org.bstats:bstats-velocity:3.0.0' - implementation 'de.themoep:minedown-adventure:1.7.1-SNAPSHOT' - implementation 'net.byteflux:libby-velocity:1.1.5' - - compileOnly 'com.velocitypowered:velocity-api:3.1.0' -} - -shadowJar { - relocate 'de.themoep', 'net.william278.husksync.libraries' - relocate 'net.byteflux', 'net.william278.husksync.libraries' - relocate 'org.bstats', 'net.william278.husksync.libraries.bstats' - relocate 'redis.clients', 'net.william278.husksync.libraries' - relocate 'org.apache', 'net.william278.husksync.libraries' - - dependencies { - //noinspection GroovyAssignabilityCheck - exclude dependency(':slf4j-api') - } -} \ No newline at end of file