diff --git a/.gitignore b/.gitignore index 0926c10..c9448a0 100644 --- a/.gitignore +++ b/.gitignore @@ -186,3 +186,4 @@ docs/dev/ # Extensions /src/main/java/net/coreprotect/extensions/*.java !/src/main/java/net/coreprotect/extensions/Extensions.java +!/src/main/java/net/coreprotect/extensions/PlayerBountiesHeadCompatibility.java diff --git a/src/main/java/net/coreprotect/consumer/process/SkullUpdateProcess.java b/src/main/java/net/coreprotect/consumer/process/SkullUpdateProcess.java index 31b582f..777d924 100644 --- a/src/main/java/net/coreprotect/consumer/process/SkullUpdateProcess.java +++ b/src/main/java/net/coreprotect/consumer/process/SkullUpdateProcess.java @@ -2,6 +2,7 @@ package net.coreprotect.consumer.process; import java.sql.Statement; import net.coreprotect.config.ConfigHandler; +import net.coreprotect.database.Database; import net.coreprotect.database.statement.SkullStatement; import net.coreprotect.utility.Util; import org.bukkit.block.BlockState; @@ -17,9 +18,22 @@ class SkullUpdateProcess { if (object instanceof BlockState) { BlockState block = (BlockState) object; - String query = "SELECT owner, skin, metadata FROM " + ConfigHandler.prefix + "skull WHERE rowid='" + rowId - + "' LIMIT 0, 1"; - SkullStatement.getData(statement, block, query); + StringBuilder query = new StringBuilder("SELECT owner"); + if (Database.hasSkullSkinColumn()) { + + query.append(", skin"); + + } + + if (Database.hasSkullMetadataColumn()) { + + query.append(", metadata"); + + } + + query.append(" FROM ").append(ConfigHandler.prefix).append("skull WHERE rowid='").append(rowId) + .append("' LIMIT 0, 1"); + SkullStatement.getData(statement, block, query.toString()); Util.updateBlock(block); } diff --git a/src/main/java/net/coreprotect/database/Database.java b/src/main/java/net/coreprotect/database/Database.java index a50d4c4..b8f7a8b 100755 --- a/src/main/java/net/coreprotect/database/Database.java +++ b/src/main/java/net/coreprotect/database/Database.java @@ -1,6 +1,7 @@ package net.coreprotect.database; import java.sql.Connection; +import java.sql.DatabaseMetaData; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -40,6 +41,8 @@ public class Database extends Queue { public static final int ENTITY_MAP = 11; public static final int BLOCKDATA = 12; public static final int ITEM = 13; + private static volatile Boolean skullHasSkinColumn = null; + private static volatile Boolean skullHasMetadataColumn = null; public static void beginTransaction(Statement statement, boolean isMySQL) { @@ -142,6 +145,48 @@ public class Database extends Queue { } + public static boolean hasSkullSkinColumn() { + + return Boolean.TRUE.equals(skullHasSkinColumn); + + } + + public static boolean hasSkullMetadataColumn() { + + return Boolean.TRUE.equals(skullHasMetadataColumn); + + } + + public static void initializeSkullColumnCache(Connection connection) { + + if (connection == null) { + + return; + + } + + try { + + if (skullHasSkinColumn == null) { + + skullHasSkinColumn = hasColumn(connection, ConfigHandler.prefix + "skull", "skin"); + + } + + if (skullHasMetadataColumn == null) { + + skullHasMetadataColumn = hasColumn(connection, ConfigHandler.prefix + "skull", "metadata"); + + } + + } catch (Exception e) { + + e.printStackTrace(); + + } + + } + public static void containerBreakCheck(String user, Material type, Object container, ItemStack[] contents, Location location) { @@ -319,8 +364,25 @@ public class Database extends Queue { + "sign (time, user, wid, x, y, z, action, color, color_secondary, data, waxed, face, line_1, line_2, line_3, line_4, line_5, line_6, line_7, line_8) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; String blockInsert = "INSERT INTO " + ConfigHandler.prefix + "block (time, user, wid, x, y, z, type, data, meta, blockdata, action, rolled_back) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - String skullInsert = "INSERT INTO " + ConfigHandler.prefix - + "skull (time, owner, skin, metadata) VALUES (?, ?, ?, ?)"; + initializeSkullColumnCache(connection); + String skullColumns = "time, owner"; + String skullValues = "?, ?"; + if (hasSkullSkinColumn()) { + + skullColumns = skullColumns + ", skin"; + skullValues = skullValues + ", ?"; + + } + + if (hasSkullMetadataColumn()) { + + skullColumns = skullColumns + ", metadata"; + skullValues = skullValues + ", ?"; + + } + + String skullInsert = "INSERT INTO " + ConfigHandler.prefix + "skull (" + skullColumns + ") VALUES (" + + skullValues + ")"; String containerInsert = "INSERT INTO " + ConfigHandler.prefix + "container (time, user, wid, x, y, z, type, data, amount, metadata, action, rolled_back) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; String itemInsert = "INSERT INTO " + ConfigHandler.prefix @@ -476,6 +538,9 @@ public class Database extends Queue { public static void createDatabaseTables(String prefix, Connection forceConnection, boolean mySQL, boolean purge) { + skullHasSkinColumn = null; + skullHasMetadataColumn = null; + ConfigHandler.databaseTables.clear(); ConfigHandler.databaseTables.addAll(Arrays.asList("art_map", "block", "chat", "command", "container", "item", "database_lock", "entity", "entity_map", "material_map", "blockdata_map", "session", "sign", "skull", @@ -542,6 +607,7 @@ public class Database extends Queue { + index + ") ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4"); statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + prefix + "skull(rowid int NOT NULL AUTO_INCREMENT,PRIMARY KEY(rowid), time int, owner varchar(255), skin varchar(255), metadata MEDIUMTEXT) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4"); + ensureSkullColumns(connection, statement, prefix); index = ", INDEX(user), INDEX(uuid)"; statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + prefix + "user(rowid int NOT NULL AUTO_INCREMENT,PRIMARY KEY(rowid),time int,user varchar(100),uuid varchar(64)" @@ -719,6 +785,8 @@ public class Database extends Queue { } + ensureSkullColumns(connection, statement, prefix); + if (!tableData.contains(prefix + "user")) { statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + prefix @@ -987,4 +1055,62 @@ public class Database extends Queue { } + private static boolean hasColumn(Connection connection, String tableName, String columnName) throws SQLException { + + DatabaseMetaData metaData = connection.getMetaData(); + try (ResultSet resultSet = metaData.getColumns(connection.getCatalog(), null, tableName, columnName)) { + + if (resultSet.next()) { + + return true; + + } + + } + + try (ResultSet resultSet = metaData.getColumns(connection.getCatalog(), null, + tableName.toUpperCase(Locale.ROOT), columnName.toUpperCase(Locale.ROOT))) + { + + return resultSet.next(); + + } + + } + + private static void ensureSkullColumns(Connection connection, Statement statement, String prefix) { + + try { + + String tableName = prefix + "skull"; + boolean hasMetadata = hasColumn(connection, tableName, "metadata"); + boolean hasSkin = hasColumn(connection, tableName, "skin"); + + if (!hasMetadata) { + + statement.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN metadata " + + (Config.getGlobal().MYSQL ? "MEDIUMTEXT" : "TEXT") + ";"); + hasMetadata = true; + + } + + if (!hasSkin) { + + statement.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN skin " + + (Config.getGlobal().MYSQL ? "VARCHAR(255)" : "TEXT") + ";"); + hasSkin = true; + + } + + skullHasMetadataColumn = hasMetadata; + skullHasSkinColumn = hasSkin; + + } catch (Exception e) { + + initializeSkullColumnCache(connection); + + } + + } + } diff --git a/src/main/java/net/coreprotect/database/statement/SkullStatement.java b/src/main/java/net/coreprotect/database/statement/SkullStatement.java index 73053e5..b74a811 100644 --- a/src/main/java/net/coreprotect/database/statement/SkullStatement.java +++ b/src/main/java/net/coreprotect/database/statement/SkullStatement.java @@ -25,8 +25,19 @@ public class SkullStatement { preparedStmt.setInt(1, time); preparedStmt.setString(2, owner); - preparedStmt.setString(3, skin); - preparedStmt.setString(4, metadata); + int parameterIndex = 3; + if (Database.hasSkullSkinColumn()) { + + preparedStmt.setString(parameterIndex++, skin); + + } + + if (Database.hasSkullMetadataColumn()) { + + preparedStmt.setString(parameterIndex, metadata); + + } + if (Database.hasReturningKeys()) { return preparedStmt.executeQuery(); @@ -69,14 +80,22 @@ public class SkullStatement { } - String skin = resultSet.getString("skin"); - if (owner != null && skin != null && skin.length() > 0) { + if (Database.hasSkullSkinColumn()) { - PaperAdapter.ADAPTER.setSkullSkin(skull, skin); + String skin = resultSet.getString("skin"); + if (owner != null && skin != null && skin.length() > 0) { + + PaperAdapter.ADAPTER.setSkullSkin(skull, skin); + + } } - PlayerBountiesHeadCompatibility.applySkullMetadata(skull, resultSet.getString("metadata")); + if (Database.hasSkullMetadataColumn()) { + + PlayerBountiesHeadCompatibility.applySkullMetadata(skull, resultSet.getString("metadata")); + + } } diff --git a/src/main/java/net/coreprotect/extensions/PlayerBountiesHeadCompatibility.java b/src/main/java/net/coreprotect/extensions/PlayerBountiesHeadCompatibility.java new file mode 100644 index 0000000..5219f54 --- /dev/null +++ b/src/main/java/net/coreprotect/extensions/PlayerBountiesHeadCompatibility.java @@ -0,0 +1,271 @@ +package net.coreprotect.extensions; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.block.Skull; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; + +public final class PlayerBountiesHeadCompatibility { + + private static final String TARGET_KEY = "bounty_head_target"; + private static final String UUID_KEY = "bounty_head_target_uuid"; + private static final String AMOUNT_KEY = "bounty_head_diamonds"; + private static final String ITEM_MARKER = "playerbounties_head"; + private static final String SKULL_MARKER = "pbh1"; + + private PlayerBountiesHeadCompatibility() { + + throw new IllegalStateException("Utility class"); + + } + + public static String serializeSkullMetadata(Skull skull) { + + if (skull == null) { + + return null; + + } + + SkullData skullData = readSkullData(skull.getPersistentDataContainer()); + if (skullData == null) { + + return null; + + } + + return String.join("|", SKULL_MARKER, encode(skullData.namespace), encode(skullData.targetName), + encode(skullData.targetUuid), Double.toString(skullData.bountyAmount)); + + } + + public static void applySkullMetadata(Skull skull, String metadata) { + + if (skull == null || metadata == null || metadata.isBlank()) { + + return; + + } + + SkullData skullData = parseMetadata(metadata); + if (skullData == null) { + + return; + + } + + applyPersistentData(skull.getPersistentDataContainer(), skullData); + + } + + public static void appendItemMetadata(ItemStack item, ItemMeta itemMeta, List>> metadata) { + + if (item == null || itemMeta == null || metadata == null || item.getType() != Material.PLAYER_HEAD) { + + return; + + } + + SkullData skullData = readSkullData(itemMeta.getPersistentDataContainer()); + if (skullData == null) { + + return; + + } + + Map data = new HashMap<>(); + data.put("plugin", ITEM_MARKER); + data.put("namespace", skullData.namespace); + data.put("target", skullData.targetName); + data.put("uuid", skullData.targetUuid); + data.put("amount", skullData.bountyAmount); + + List> list = new ArrayList<>(); + list.add(data); + metadata.add(list); + + } + + public static boolean applyItemMetadata(ItemStack itemstack, Map mapData) { + + if (itemstack == null || mapData == null || itemstack.getType() != Material.PLAYER_HEAD) { + + return false; + + } + + Object marker = mapData.get("plugin"); + if (!ITEM_MARKER.equals(marker)) { + + return false; + + } + + String namespace = asString(mapData.get("namespace")); + String target = asString(mapData.get("target")); + String uuid = asString(mapData.get("uuid")); + Double amount = asDouble(mapData.get("amount")); + if (namespace == null || target == null || uuid == null || amount == null) { + + return true; + + } + + ItemMeta itemMeta = itemstack.getItemMeta(); + if (itemMeta == null) { + + return true; + + } + + applyPersistentData(itemMeta.getPersistentDataContainer(), new SkullData(namespace, target, uuid, amount)); + itemstack.setItemMeta(itemMeta); + return true; + + } + + private static SkullData readSkullData(PersistentDataContainer container) { + + if (container == null) { + + return null; + + } + + NamespacedKey targetKey = null; + NamespacedKey uuidKey = null; + NamespacedKey amountKey = null; + + for (NamespacedKey key : container.getKeys()) { + + if (TARGET_KEY.equals(key.getKey())) { + + targetKey = key; + + } else if (UUID_KEY.equals(key.getKey())) { + + uuidKey = key; + + } else if (AMOUNT_KEY.equals(key.getKey())) { + + amountKey = key; + + } + + } + + if (targetKey == null || uuidKey == null || amountKey == null) { + + return null; + + } + + String target = container.get(targetKey, PersistentDataType.STRING); + String uuid = container.get(uuidKey, PersistentDataType.STRING); + Double amount = container.get(amountKey, PersistentDataType.DOUBLE); + if (target == null || uuid == null || amount == null) { + + return null; + + } + + return new SkullData(targetKey.getNamespace(), target, uuid, amount); + + } + + private static void applyPersistentData(PersistentDataContainer container, SkullData skullData) { + + NamespacedKey targetKey = new NamespacedKey(skullData.namespace, TARGET_KEY); + NamespacedKey uuidKey = new NamespacedKey(skullData.namespace, UUID_KEY); + NamespacedKey amountKey = new NamespacedKey(skullData.namespace, AMOUNT_KEY); + + container.set(targetKey, PersistentDataType.STRING, skullData.targetName); + container.set(uuidKey, PersistentDataType.STRING, skullData.targetUuid); + container.set(amountKey, PersistentDataType.DOUBLE, skullData.bountyAmount); + + } + + private static SkullData parseMetadata(String metadata) { + + String[] parts = metadata.split("\\|", -1); + if (parts.length != 5 || !SKULL_MARKER.equals(parts[0])) { + + return null; + + } + + try { + + return new SkullData(decode(parts[1]), decode(parts[2]), decode(parts[3]), Double.parseDouble(parts[4])); + + } catch (Exception e) { + + return null; + + } + + } + + private static String encode(String value) { + + return Base64.getEncoder().encodeToString(value.getBytes(StandardCharsets.UTF_8)); + + } + + private static String decode(String value) { + + return new String(Base64.getDecoder().decode(value), StandardCharsets.UTF_8); + + } + + private static String asString(Object value) { + + if (value == null) { + + return null; + + } + + return String.valueOf(value); + + } + + private static Double asDouble(Object value) { + + if (value instanceof Number) { + + return ((Number) value).doubleValue(); + + } + + if (value == null) { + + return null; + + } + + try { + + return Double.parseDouble(String.valueOf(value)); + + } catch (NumberFormatException e) { + + return null; + + } + + } + + private record SkullData(String namespace, String targetName, String targetUuid, double bountyAmount) { + } + +}