From 12e882fe2250fc2b44105313880fa7b50185c922 Mon Sep 17 00:00:00 2001 From: William Date: Fri, 28 Jul 2023 16:50:52 +0100 Subject: [PATCH] v2.2.6: Crafting inventory safety, Maria v11 support (#153) * Clear player inventory crafting slots on sync * Bundle Maria driver for v11 support --- build.gradle | 2 + .../william278/husksync/BukkitHuskSync.java | 6 +- .../listener/BukkitEventListener.java | 5 + .../husksync/player/BukkitPlayer.java | 18 ++- bukkit/src/main/resources/plugin.yml | 1 + .../HuskSyncInitializationException.java | 4 +- .../william278/husksync/config/Settings.java | 2 +- .../husksync/database/Database.java | 4 +- .../husksync/database/MySqlDatabase.java | 146 ++++++++---------- gradle.properties | 5 +- 10 files changed, 95 insertions(+), 98 deletions(-) diff --git a/build.gradle b/build.gradle index 1e08cafc..66d1c5f8 100644 --- a/build.gradle +++ b/build.gradle @@ -14,8 +14,10 @@ defaultTasks 'licenseFormat', 'build' ext { set 'version', version.toString() set 'description', description.toString() + set 'jedis_version', jedis_version.toString() set 'mysql_driver_version', mysql_driver_version.toString() + set 'mariadb_driver_version', mariadb_driver_version.toString() set 'snappy_version', snappy_version.toString() set 'commons_text_version', commons_text_version.toString() } diff --git a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java index 174041dc..9320a4ff 100644 --- a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java +++ b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java @@ -130,8 +130,8 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { // Prepare database connection this.database = new MySqlDatabase(this); - log(Level.INFO, "Attempting to establish connection to the " + settings.getSqlType().getDisplayName() + " database..."); - initialized.set(this.database.initialize()); + log(Level.INFO, "Attempting to establish connection to the " + settings.getDatabaseType().getDisplayName() + " database..."); + this.database.initialize(); if (initialized.get()) { log(Level.INFO, "Successfully established a connection to the database"); } else { @@ -195,7 +195,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync { "An update is available for HuskSync, v" + newVersion + " (Currently running v" + getPluginVersion() + ")"))); } - } catch (HuskSyncInitializationException exception) { + } catch (IllegalStateException exception) { log(Level.SEVERE, """ *************************************************** diff --git a/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java b/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java index fb613f38..516f6732 100644 --- a/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java +++ b/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java @@ -40,6 +40,7 @@ import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.entity.ProjectileLaunchEvent; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryOpenEvent; +import org.bukkit.event.inventory.PrepareItemCraftEvent; import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.player.PlayerInteractEntityEvent; @@ -172,6 +173,10 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven event.setCancelled(cancelPlayerEvent(event.getWhoClicked().getUniqueId())); } + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onCraftItem(@NotNull PrepareItemCraftEvent event) { + } + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) { if (event.getEntity() instanceof Player player) { diff --git a/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java b/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java index f70db974..a44beb13 100644 --- a/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java +++ b/bukkit/src/main/java/net/william278/husksync/player/BukkitPlayer.java @@ -36,6 +36,7 @@ import org.bukkit.advancement.AdvancementProgress; import org.bukkit.attribute.Attribute; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.player.PlayerTeleportEvent; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; @@ -55,7 +56,7 @@ import java.util.logging.Level; * Bukkit implementation of an {@link OnlineUser} */ public class BukkitPlayer extends OnlineUser { - + private final BukkitHuskSync plugin; private final Player player; @@ -178,6 +179,7 @@ public class BukkitPlayer extends OnlineUser { return BukkitSerializer.deserializeInventory(itemData.serializedItems).thenApplyAsync(contents -> { final CompletableFuture inventorySetFuture = new CompletableFuture<>(); Bukkit.getScheduler().runTask(plugin, () -> { + this.clearInventoryCraftingSlots(); player.setItemOnCursor(null); player.getInventory().setContents(contents.getContents()); player.updateInventory(); @@ -187,6 +189,16 @@ public class BukkitPlayer extends OnlineUser { }); } + // Clears any items the player may have in the crafting slots of their inventory + private void clearInventoryCraftingSlots() { + final Inventory inventory = player.getOpenInventory().getTopInventory(); + if (inventory.getType() == InventoryType.CRAFTING) { + for (int slot = 0; slot < 5; slot++) { + inventory.setItem(slot, null); + } + } + } + @Override public CompletableFuture getEnderChest() { final Inventory enderChest = player.getEnderChest(); @@ -500,7 +512,7 @@ public class BukkitPlayer extends OnlineUser { .ifPresentOrElse(mapping -> mapping.setContainerValue(container, player, key), () -> plugin.log(Level.WARNING, "Could not set " + player.getName() + "'s persistent data key " + keyString + - " as it has an invalid type. Skipping!")); + " as it has an invalid type. Skipping!")); } }); }).exceptionally(throwable -> { @@ -517,7 +529,7 @@ public class BukkitPlayer extends OnlineUser { public Audience getAudience() { return plugin.getAudiences().player(player); } - + @Override public boolean isOffline() { try { diff --git a/bukkit/src/main/resources/plugin.yml b/bukkit/src/main/resources/plugin.yml index 100061b7..b337a9eb 100644 --- a/bukkit/src/main/resources/plugin.yml +++ b/bukkit/src/main/resources/plugin.yml @@ -11,6 +11,7 @@ softdepend: libraries: - 'redis.clients:jedis:${jedis_version}' - 'com.mysql:mysql-connector-j:${mysql_driver_version}' + - 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}' - 'org.xerial.snappy:snappy-java:${snappy_version}' - 'org.apache.commons:commons-text:${commons_text_version}' diff --git a/common/src/main/java/net/william278/husksync/HuskSyncInitializationException.java b/common/src/main/java/net/william278/husksync/HuskSyncInitializationException.java index 2e6f3c69..de75a0e4 100644 --- a/common/src/main/java/net/william278/husksync/HuskSyncInitializationException.java +++ b/common/src/main/java/net/william278/husksync/HuskSyncInitializationException.java @@ -22,9 +22,9 @@ package net.william278.husksync; import org.jetbrains.annotations.NotNull; /** - * Indicates an exception occurred while initialising the HuskSync plugin + * Indicates an exception occurred while initializing the HuskSync plugin */ -public class HuskSyncInitializationException extends RuntimeException { +public class HuskSyncInitializationException extends IllegalStateException { public HuskSyncInitializationException(@NotNull String message) { super(message); } 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 c491a707..9ba4ec21 100644 --- a/common/src/main/java/net/william278/husksync/config/Settings.java +++ b/common/src/main/java/net/william278/husksync/config/Settings.java @@ -174,7 +174,7 @@ public class Settings { @NotNull - public Database.Type getSqlType() { + public Database.Type getDatabaseType() { return databaseType; } 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 e6522bd0..c6cd47d4 100644 --- a/common/src/main/java/net/william278/husksync/database/Database.java +++ b/common/src/main/java/net/william278/husksync/database/Database.java @@ -75,10 +75,8 @@ public abstract class Database { /** * Initialize the database and ensure tables are present; create tables if they do not exist. - * - * @return A future returning boolean - if the connection could be established. */ - public abstract boolean initialize(); + public abstract void initialize(); /** * Ensure a {@link User} has an entry in the database and that their username is up-to-date 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 12823846..979fda53 100644 --- a/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java +++ b/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java @@ -40,57 +40,13 @@ import java.util.logging.Level; public class MySqlDatabase extends Database { - /** - * MySQL protocol - */ - private final Database.Type type; - - /** - * MySQL server hostname - */ - private final String mySqlHost; - - /** - * MySQL server port - */ - private final int mySqlPort; - - /** - * Database to use on the MySQL server - */ - private final String mySqlDatabaseName; - private final String mySqlUsername; - private final String mySqlPassword; - private final String mySqlConnectionParameters; - - private final int hikariMaximumPoolSize; - private final int hikariMinimumIdle; - private final long hikariMaximumLifetime; - private final long hikariKeepAliveTime; - private final long hikariConnectionTimeOut; - private static final String DATA_POOL_NAME = "HuskSyncHikariPool"; - - /** - * The Hikari data source - a pool of database connections that can be fetched on-demand - */ - private HikariDataSource connectionPool; + private final String protocol; + private HikariDataSource dataSource; public MySqlDatabase(@NotNull HuskSync plugin) { super(plugin); - final Settings settings = plugin.getSettings(); - this.type = settings.getSqlType(); - this.mySqlHost = settings.getMySqlHost(); - this.mySqlPort = settings.getMySqlPort(); - this.mySqlDatabaseName = settings.getMySqlDatabase(); - this.mySqlUsername = settings.getMySqlUsername(); - this.mySqlPassword = settings.getMySqlPassword(); - this.mySqlConnectionParameters = settings.getMySqlConnectionParameters(); - this.hikariMaximumPoolSize = settings.getMySqlConnectionPoolSize(); - this.hikariMinimumIdle = settings.getMySqlConnectionPoolIdle(); - this.hikariMaximumLifetime = settings.getMySqlConnectionPoolLifetime(); - this.hikariKeepAliveTime = settings.getMySqlConnectionPoolKeepAlive(); - this.hikariConnectionTimeOut = settings.getMySqlConnectionPoolTimeout(); + this.protocol = plugin.getSettings().getDatabaseType().getProtocol(); } /** @@ -100,46 +56,68 @@ public class MySqlDatabase extends Database { * @throws SQLException if the connection fails for some reason */ private Connection getConnection() throws SQLException { - return connectionPool.getConnection(); + return dataSource.getConnection(); } @Override - public boolean initialize() { - try { - // Create jdbc driver connection url - final String jdbcUrl = "jdbc:" + type.getProtocol() + "://" + mySqlHost + ":" + mySqlPort + "/" + mySqlDatabaseName + mySqlConnectionParameters; - connectionPool = new HikariDataSource(); - connectionPool.setJdbcUrl(jdbcUrl); - - // Authenticate - connectionPool.setUsername(mySqlUsername); - connectionPool.setPassword(mySqlPassword); - - // Set various additional parameters - 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 = connectionPool.getConnection()) { - // Load database schema CREATE statements from schema file - final String[] databaseSchema = getSchemaStatements("database/mysql_schema.sql"); - try (Statement statement = connection.createStatement()) { - for (String tableCreationStatement : databaseSchema) { - statement.execute(tableCreationStatement); - } + public void initialize() throws IllegalStateException { + // Initialize the Hikari pooled connection + dataSource = new HikariDataSource(); + dataSource.setJdbcUrl(String.format("jdbc:%s://%s:%s/%s%s", + protocol, + plugin.getSettings().getMySqlHost(), + plugin.getSettings().getMySqlPort(), + plugin.getSettings().getMySqlDatabase(), + plugin.getSettings().getMySqlConnectionParameters() + )); + + // Authenticate with the database + dataSource.setUsername(plugin.getSettings().getMySqlUsername()); + dataSource.setPassword(plugin.getSettings().getMySqlPassword()); + + // Set connection pool options + dataSource.setMaximumPoolSize(plugin.getSettings().getMySqlConnectionPoolSize()); + dataSource.setMinimumIdle(plugin.getSettings().getMySqlConnectionPoolIdle()); + dataSource.setMaxLifetime(plugin.getSettings().getMySqlConnectionPoolLifetime()); + dataSource.setKeepaliveTime(plugin.getSettings().getMySqlConnectionPoolKeepAlive()); + dataSource.setConnectionTimeout(plugin.getSettings().getMySqlConnectionPoolTimeout()); + dataSource.setPoolName(DATA_POOL_NAME); + + // Set additional connection pool properties + final Properties properties = new Properties(); + properties.putAll( + Map.of("cachePrepStmts", "true", + "prepStmtCacheSize", "250", + "prepStmtCacheSqlLimit", "2048", + "useServerPrepStmts", "true", + "useLocalSessionState", "true", + "useLocalTransactionState", "true" + )); + properties.putAll( + Map.of( + "rewriteBatchedStatements", "true", + "cacheResultSetMetadata", "true", + "cacheServerConfiguration", "true", + "elideSetAutoCommits", "true", + "maintainTimeStats", "false") + ); + dataSource.setDataSourceProperties(properties); + + // Prepare database schema; make tables if they don't exist + try (Connection connection = dataSource.getConnection()) { + final String[] databaseSchema = getSchemaStatements(String.format("database/%s_schema.sql", protocol)); + try (Statement statement = connection.createStatement()) { + for (String tableCreationStatement : databaseSchema) { + statement.execute(tableCreationStatement); } - return true; - } catch (SQLException | IOException e) { - plugin.log(Level.SEVERE, "Failed to perform database setup: " + e.getMessage()); + } catch (SQLException e) { + throw new IllegalStateException("Failed to create database tables. Please ensure you are running MySQL v8.0+ " + + "and that your connecting user account has privileges to create tables.", e); } - } catch (Exception e) { - plugin.log(Level.SEVERE, "An unhandled exception occurred during database setup!", e); + } catch (SQLException | IOException e) { + throw new IllegalStateException("Failed to establish a connection to the MySQL database. " + + "Please check the supplied database credentials in the config file", e); } - return false; } @Override @@ -445,9 +423,9 @@ public class MySqlDatabase extends Database { @Override public void close() { - if (connectionPool != null) { - if (!connectionPool.isClosed()) { - connectionPool.close(); + if (dataSource != null) { + if (!dataSource.isClosed()) { + dataSource.close(); } } } diff --git a/gradle.properties b/gradle.properties index e488ac69..3b33a893 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,11 +3,12 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8' org.gradle.daemon=true javaVersion=16 -plugin_version=2.2.5 +plugin_version=2.2.6 plugin_archive=husksync plugin_description=A modern, cross-server player data synchronization system jedis_version=4.3.2 -mysql_driver_version=8.0.32 +mysql_driver_version=8.1.0 +mariadb_driver_version=3.1.4 snappy_version=1.1.9.1 commons_text_version=1.10.0 \ No newline at end of file