From 4b34f05454f1349e1747fbf3928c898dd56ae8cd Mon Sep 17 00:00:00 2001 From: Krakenied Date: Sun, 18 Aug 2024 09:03:51 +0200 Subject: Storage rework --- .../quests/bukkit/BukkitQuestsPlugin.java | 8 +- .../bukkit/command/AdminMigrateCommandHandler.java | 15 +- .../AdminModdataCompleteCommandHandler.java | 2 +- .../AdminModdataFullresetCommandHandler.java | 2 +- .../command/AdminModdataRandomCommandHandler.java | 2 +- .../command/AdminModdataResetCommandHandler.java | 2 +- .../command/AdminModdataStartCommandHandler.java | 2 +- .../bukkit/storage/ModernMySQLStorageProvider.java | 597 +++++++++++++++++++++ .../bukkit/storage/ModernYAMLStorageProvider.java | 284 ++++++++++ .../bukkit/storage/MySqlStorageProvider.java | 471 ---------------- .../quests/bukkit/storage/YamlStorageProvider.java | 212 -------- .../quests/bukkit/util/CommandUtils.java | 27 +- .../src/main/resources/resources/bukkit/config.yml | 64 ++- 13 files changed, 971 insertions(+), 717 deletions(-) create mode 100644 bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/ModernMySQLStorageProvider.java create mode 100644 bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/ModernYAMLStorageProvider.java delete mode 100644 bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/MySqlStorageProvider.java delete mode 100644 bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/YamlStorageProvider.java (limited to 'bukkit/src/main') diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/BukkitQuestsPlugin.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/BukkitQuestsPlugin.java index 1e6c2360..8d6d412b 100644 --- a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/BukkitQuestsPlugin.java +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/BukkitQuestsPlugin.java @@ -59,8 +59,8 @@ import com.leonardobishop.quests.bukkit.scheduler.ServerScheduler; import com.leonardobishop.quests.bukkit.scheduler.WrappedTask; import com.leonardobishop.quests.bukkit.scheduler.bukkit.BukkitServerSchedulerAdapter; import com.leonardobishop.quests.bukkit.scheduler.folia.FoliaServerScheduler; -import com.leonardobishop.quests.bukkit.storage.MySqlStorageProvider; -import com.leonardobishop.quests.bukkit.storage.YamlStorageProvider; +import com.leonardobishop.quests.bukkit.storage.ModernMySQLStorageProvider; +import com.leonardobishop.quests.bukkit.storage.ModernYAMLStorageProvider; import com.leonardobishop.quests.bukkit.tasktype.BukkitTaskTypeManager; import com.leonardobishop.quests.bukkit.tasktype.type.BarteringTaskType; import com.leonardobishop.quests.bukkit.tasktype.type.BlockItemdroppingTaskType; @@ -295,14 +295,14 @@ public class BukkitQuestsPlugin extends JavaPlugin implements Quests { default: questsLogger.warning("No valid storage provider is configured - Quests will use YAML storage as a default"); case "yaml": - this.storageProvider = new YamlStorageProvider(this); + this.storageProvider = new ModernYAMLStorageProvider(this); break; case "mysql": ConfigurationSection section = this.getConfig().getConfigurationSection("options.storage.database-settings"); if (section == null) { questsLogger.warning("No database settings are configured - default values will be used"); } - this.storageProvider = new MySqlStorageProvider(this, section); + this.storageProvider = new ModernMySQLStorageProvider(this, section); } try { diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminMigrateCommandHandler.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminMigrateCommandHandler.java index 2f3ad0af..7385bad1 100644 --- a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminMigrateCommandHandler.java +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminMigrateCommandHandler.java @@ -1,8 +1,9 @@ package com.leonardobishop.quests.bukkit.command; import com.leonardobishop.quests.bukkit.BukkitQuestsPlugin; -import com.leonardobishop.quests.bukkit.storage.MySqlStorageProvider; -import com.leonardobishop.quests.bukkit.storage.YamlStorageProvider; +import com.leonardobishop.quests.bukkit.storage.ModernMySQLStorageProvider; +import com.leonardobishop.quests.bukkit.storage.ModernYAMLStorageProvider; +import com.leonardobishop.quests.common.player.QPlayerData; import com.leonardobishop.quests.common.player.questprogressfile.QuestProgressFile; import com.leonardobishop.quests.common.storage.StorageProvider; import org.bukkit.ChatColor; @@ -87,15 +88,15 @@ public class AdminMigrateCommandHandler implements CommandHandler { } sender.sendMessage(ChatColor.GRAY + "Loading quest progress files from '" + fromProvider.getName() + "'..."); - List files = fromProvider.loadAllProgressFiles(); + List files = fromProvider.loadAllPlayerData(); sender.sendMessage(ChatColor.GRAY.toString() + files.size() + " files loaded."); - for (QuestProgressFile file : files) { + for (QPlayerData file : files) { file.setModified(true); } sender.sendMessage(ChatColor.GRAY + "Writing quest progress files to '" + toProvider.getName() + "'..."); - toProvider.saveAllProgressFiles(files); + toProvider.saveAllPlayerData(files); sender.sendMessage(ChatColor.GRAY + "Done."); shutdownProvider(sender, fromProvider); @@ -151,11 +152,11 @@ public class AdminMigrateCommandHandler implements CommandHandler { switch (configuredProvider.toLowerCase()) { default: case "yaml": - storageProvider = new YamlStorageProvider(plugin); + storageProvider = new ModernYAMLStorageProvider(plugin); break; case "mysql": ConfigurationSection section = configurationSection.getConfigurationSection("database-settings"); - storageProvider = new MySqlStorageProvider(plugin, section); + storageProvider = new ModernMySQLStorageProvider(plugin, section); } return storageProvider; } diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataCompleteCommandHandler.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataCompleteCommandHandler.java index 8f85f818..2f8aa8bc 100644 --- a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataCompleteCommandHandler.java +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataCompleteCommandHandler.java @@ -34,7 +34,7 @@ public class AdminModdataCompleteCommandHandler implements CommandHandler { qPlayer.completeQuest(quest); Messages.COMMAND_QUEST_ADMIN_COMPLETE_SUCCESS.send(sender, "{player}", args[3], "{quest}", quest.getId()); - CommandUtils.doSafeSave(qPlayer, questProgressFile, plugin); + CommandUtils.doSafeSave(this.plugin, qPlayer); }); return; } diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataFullresetCommandHandler.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataFullresetCommandHandler.java index 9703971d..885b155b 100644 --- a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataFullresetCommandHandler.java +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataFullresetCommandHandler.java @@ -27,7 +27,7 @@ public class AdminModdataFullresetCommandHandler implements CommandHandler { questProgressFile.reset(); Messages.COMMAND_QUEST_ADMIN_FULLRESET.send(sender, "{player}", args[3]); - CommandUtils.doSafeSave(qPlayer, questProgressFile, plugin); + CommandUtils.doSafeSave(this.plugin, qPlayer); }); return; diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataRandomCommandHandler.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataRandomCommandHandler.java index 81557cc5..81a5759a 100644 --- a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataRandomCommandHandler.java +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataRandomCommandHandler.java @@ -78,7 +78,7 @@ public class AdminModdataRandomCommandHandler implements CommandHandler { "{quest}", quest.getId()); } - CommandUtils.doSafeSave(qPlayer, questProgressFile, plugin); + CommandUtils.doSafeSave(this.plugin, qPlayer); }); } diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataResetCommandHandler.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataResetCommandHandler.java index c57d06ed..6355045e 100644 --- a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataResetCommandHandler.java +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataResetCommandHandler.java @@ -34,7 +34,7 @@ public class AdminModdataResetCommandHandler implements CommandHandler { questProgressFile.generateBlankQuestProgress(quest, true); Messages.COMMAND_QUEST_ADMIN_RESET_SUCCESS.send(sender, "{player}", args[3], "{quest}", quest.getId()); - CommandUtils.doSafeSave(qPlayer, questProgressFile, plugin); + CommandUtils.doSafeSave(this.plugin, qPlayer); }); return; } diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataStartCommandHandler.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataStartCommandHandler.java index 5b54cd53..15626726 100644 --- a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataStartCommandHandler.java +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataStartCommandHandler.java @@ -59,7 +59,7 @@ public class AdminModdataStartCommandHandler implements CommandHandler { Messages.COMMAND_QUEST_ADMIN_START_SUCCESS.send(sender, "{player}", args[3], "{quest}", quest.getId()); - CommandUtils.doSafeSave(qPlayer, questProgressFile, plugin); + CommandUtils.doSafeSave(this.plugin, qPlayer); }); return; } diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/ModernMySQLStorageProvider.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/ModernMySQLStorageProvider.java new file mode 100644 index 00000000..4eb8cba2 --- /dev/null +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/ModernMySQLStorageProvider.java @@ -0,0 +1,597 @@ +package com.leonardobishop.quests.bukkit.storage; + +import com.leonardobishop.quests.bukkit.BukkitQuestsPlugin; +import com.leonardobishop.quests.common.player.QPlayerData; +import com.leonardobishop.quests.common.player.questprogressfile.QuestProgress; +import com.leonardobishop.quests.common.player.questprogressfile.QuestProgressFile; +import com.leonardobishop.quests.common.player.questprogressfile.TaskProgress; +import com.leonardobishop.quests.common.quest.Quest; +import com.leonardobishop.quests.common.quest.Task; +import com.leonardobishop.quests.common.storage.StorageProvider; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.function.Function; +import java.util.logging.Level; + +public final class ModernMySQLStorageProvider implements StorageProvider { + + // Table creation SQL + private static final String CREATE_TABLE_QUEST_PROGRESS = + "CREATE TABLE IF NOT EXISTS `{prefix}quest_progress` (" + + " `uuid` VARCHAR(36) NOT NULL," + + " `quest_id` VARCHAR(50) NOT NULL," + + " `started` BOOL NOT NULL," + + " `started_date` BIGINT NOT NULL," + + " `completed` BOOL NOT NULL," + + " `completed_before` BOOL NOT NULL," + + " `completion_date` BIGINT NOT NULL," + + " PRIMARY KEY (`uuid`, `quest_id`));"; + private static final String CREATE_TABLE_TASK_PROGRESS = + "CREATE TABLE IF NOT EXISTS `{prefix}task_progress` (" + + " `uuid` VARCHAR(36) NOT NULL," + + " `quest_id` VARCHAR(50) NOT NULL," + + " `task_id` VARCHAR(50) NOT NULL," + + " `completed` BOOL NOT NULL," + + " `progress` VARCHAR(64) NULL," + + " `data_type` VARCHAR(10) NULL," + + " PRIMARY KEY (`uuid`, `quest_id`, `task_id`));"; + private static final String CREATE_TABLE_PLAYER_PREFERENCES = + "CREATE TABLE IF NOT EXISTS `{prefix}player_preferences` (" + + " `uuid` CHAR(36) NOT NULL," + + " `preference_id` VARCHAR(255) NOT NULL," + + " `value` VARCHAR(64) NULL," + + " `data_type` VARCHAR(10) NULL," + + " PRIMARY KEY (`uuid`, `preference_id`));"; + private static final String CREATE_TABLE_DATABASE_INFORMATION = + "CREATE TABLE IF NOT EXISTS `{prefix}database_information` (" + + " `key` VARCHAR(255) NOT NULL," + + " `value` VARCHAR(255) NOT NULL," + + " PRIMARY KEY (`key`));"; + + // Selection SQL + private static final String SELECT_PLAYER_QUEST_PROGRESS = + "SELECT quest_id, started, started_date, completed, completed_before, completion_date FROM `{prefix}quest_progress` WHERE uuid = ?;"; + private static final String SELECT_PLAYER_TASK_PROGRESS = + "SELECT quest_id, task_id, completed, progress, data_type FROM `{prefix}task_progress` WHERE uuid = ?;"; + private static final String SELECT_UUID_LIST = + "SELECT DISTINCT uuid FROM `{prefix}quest_progress`;"; + + // Insertion SQL + private static final String INSERT_PLAYER_QUEST_PROGRESS = + "INSERT INTO `{prefix}quest_progress` (uuid, quest_id, started, started_date, completed, completed_before, completion_date) VALUES (?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE started = ?, started_date = ?, completed = ?, completed_before = ?, completion_date = ?;"; + private static final String INSERT_PLAYER_TASK_PROGRESS = + "INSERT INTO `{prefix}task_progress` (uuid, quest_id, task_id, completed, progress, data_type) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE completed = ?, progress = ?, data_type = ?"; + + private static final Map ADDITIONAL_PROPERTIES = new HashMap<>() {{ + this.put("cachePrepStmts", true); + this.put("prepStmtCacheSize", 250); + this.put("prepStmtCacheSqlLimit", 2048); + this.put("useServerPrepStmts", true); + this.put("useLocalSessionState", true); + this.put("rewriteBatchedStatements", true); + this.put("cacheResultSetMetadata", true); + this.put("cacheServerConfiguration", true); + this.put("elideSetAutoCommits", true); + this.put("maintainTimeStats", false); + }}; + + private final BukkitQuestsPlugin plugin; + private final ConfigurationSection config; + + private HikariDataSource ds; + private Function prefixer; + private boolean validateQuests; + private boolean fault; + + public ModernMySQLStorageProvider(final @NotNull BukkitQuestsPlugin plugin, final @Nullable ConfigurationSection config) { + this.plugin = Objects.requireNonNull(plugin, "plugin cannot be null"); + this.config = Objects.requireNonNullElseGet(config, YamlConfiguration::new); + this.fault = true; + } + + @Override + public @NotNull String getName() { + return "mysql"; + } + + @Override + public void init() throws IOException { + // initialize hikari config and set pool name + final HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setPoolName("quests-hikari"); + + // set jdbc url + final String address = this.config.getString("network.address", "localhost:3306"); + final String database = this.config.getString("network.database", "minecraft"); + final String jdbcUrl = "jdbc:mysql://" + address + "/" + database; + hikariConfig.setJdbcUrl(jdbcUrl); + + // set username + final String username = this.config.getString("network.username", "root"); + hikariConfig.setUsername(username); + + // set password + final String password = this.config.getString("network.password"); + hikariConfig.setPassword(password); + + // set pool size related properties + final int minIdle = this.config.getInt("connection-pool-settings.minimum-idle", 8); + final int maxPoolSize = this.config.getInt("connection-pool-settings.maximum-pool-size", 8); + hikariConfig.setMinimumIdle(minIdle); + hikariConfig.setMaximumPoolSize(maxPoolSize); + + // set pool timeouts related properties + final long connectionTimeoutMs = this.config.getLong("connection-pool-settings.connection-timeout", 5000L); + final long idleTimeoutMs = this.config.getLong("connection-pool-settings.idle-timeout", 600000L); + final long keepaliveTimeMs = this.config.getLong("connection-pool-settings.keepalive-time", 0L); + final long maxLifetimeMs = this.config.getLong("connection-pool-settings.maximum-lifetime", 1800000L); + hikariConfig.setConnectionTimeout(connectionTimeoutMs); + hikariConfig.setIdleTimeout(idleTimeoutMs); + hikariConfig.setKeepaliveTime(keepaliveTimeMs); + hikariConfig.setMaxLifetime(maxLifetimeMs); + + // set additional datasource properties + for (final Map.Entry property : ADDITIONAL_PROPERTIES.entrySet()) { + hikariConfig.addDataSourceProperty(property.getKey(), property.getValue()); + } + + // Add additional custom data source properties + final ConfigurationSection propertiesSection = this.config.getConfigurationSection("connection-pool-settings.data-source-properties"); + if (propertiesSection != null) { + final Set properties = propertiesSection.getKeys(false); + + for (final String propertyName : properties) { + final Object propertyValue = propertiesSection.get(propertyName); + hikariConfig.addDataSourceProperty(propertyName, propertyValue); + } + } + + // initialize data source + this.ds = new HikariDataSource(hikariConfig); + + // set table prefixer + final String prefix = this.config.getString("table-prefix", "quests_"); + this.prefixer = s -> s.replace("{prefix}", prefix); + + // set whether quests ids should be validated + this.validateQuests = this.plugin.getConfig().getBoolean("options.verify-quest-exists-on-load", true); + + // create and upgrade default tables + try (final Connection conn = this.ds.getConnection()) { + try (final Statement stmt = conn.createStatement()) { + this.plugin.getQuestsLogger().debug("Creating default tables."); + + stmt.addBatch(this.prefixer.apply(CREATE_TABLE_QUEST_PROGRESS)); + stmt.addBatch(this.prefixer.apply(CREATE_TABLE_TASK_PROGRESS)); + stmt.addBatch(this.prefixer.apply(CREATE_TABLE_PLAYER_PREFERENCES)); + stmt.addBatch(this.prefixer.apply(CREATE_TABLE_DATABASE_INFORMATION)); + + stmt.executeBatch(); + } + + final DatabaseMigrator migrator = new DatabaseMigrator(this.plugin, this.prefixer, conn); + final int currentSchemaVersion = migrator.getCurrentSchemaVersion(); + + // upgrade the table only if current schema version is lower than the latest + if (currentSchemaVersion < DatabaseMigrator.LATEST_SCHEMA_VERSION) { + this.plugin.getLogger().info("Automatically upgrading database schema from version " + currentSchemaVersion + " to " + DatabaseMigrator.LATEST_SCHEMA_VERSION + "."); + migrator.upgrade(currentSchemaVersion); + } + } catch (final SQLException e) { + throw new IOException("Failed to create or upgrade default tables", e); + } + + this.fault = false; + } + + @Override + public void shutdown() { + if (this.ds != null) { + this.ds.close(); + } + } + + @Override + public @Nullable QPlayerData loadPlayerData(final @NotNull UUID uuid) { + Objects.requireNonNull(uuid, "uuid cannot be null"); + + if (this.fault) { + return null; + } + + final String uuidString = uuid.toString(); + final QuestProgressFile questProgressFile = new QuestProgressFile(this.plugin, uuid); + + try (final Connection conn = this.ds.getConnection()) { + this.plugin.getQuestsLogger().debug("Querying player data for " + uuidString + "."); + + final Map questProgressMap = new HashMap<>(); + + try (final PreparedStatement stmt = conn.prepareStatement(this.prefixer.apply(SELECT_PLAYER_QUEST_PROGRESS))) { + stmt.setString(1, uuidString); + + try (final ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + final String questId = rs.getString(1); + + if (this.validateQuests) { + final Quest quest = this.plugin.getQuestManager().getQuestById(questId); + + if (quest == null) { + continue; + } + } + + final boolean started = rs.getBoolean(2); + final long startedDate = rs.getLong(3); + final boolean completed = rs.getBoolean(4); + final boolean completedBefore = rs.getBoolean(5); + final long completionDate = rs.getLong(6); + + final QuestProgress questProgress = new QuestProgress(this.plugin, questId, uuid, started, startedDate, completed, completedBefore, completionDate); + questProgressMap.put(questId, questProgress); + } + } + } + + try (final PreparedStatement stmt = conn.prepareStatement(this.prefixer.apply(SELECT_PLAYER_TASK_PROGRESS))) { + stmt.setString(1, uuidString); + + try (final ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + final String questId = rs.getString(1); + + final QuestProgress questProgress = questProgressMap.get(questId); + if (questProgress == null) { + continue; + } + + final String taskId = rs.getString(2); + + if (this.validateQuests) { + final Quest quest = this.plugin.getQuestManager().getQuestById(questId); + if (quest == null) { + continue; + } + + final Task task = quest.getTaskById(taskId); + if (task == null) { + continue; + } + } + + final boolean completed = rs.getBoolean(3); + final String progressString = rs.getString(4); + final String dataTypeString = rs.getString(5); + + // maybe make an enum and use Enum#valueOf & then make a switch for enum instead? + // not sure about performance impact, probably just a small gain - need to benchmark it + final Object progress; + try { + progress = switch (dataTypeString) { + case null -> null; + case "int" -> Integer.parseInt(progressString); + case "float" -> Float.parseFloat(progressString); + case "long" -> Long.parseLong(progressString); + case "double" -> Double.parseDouble(progressString); + case "BigInteger" -> new BigInteger(progressString); + case "BigDecimal" -> new BigDecimal(progressString); + default -> throw new IllegalArgumentException("Unexpected data type: '" + dataTypeString + "'"); + }; + } catch (final NumberFormatException e) { + this.plugin.getLogger().log(Level.WARNING, "Cannot retrieve progress for task '" + taskId + + "' in quest '" + questId + "' for player " + uuidString + " since progress string '" + + progressString + "' is malformed!", e); + continue; + } catch (final IllegalArgumentException e) { + this.plugin.getLogger().log(Level.WARNING, "Cannot retrieve progress for task '" + taskId + + "' in quest '" + questId + "' for player " + uuidString + " since data type string '" + + dataTypeString + "' is unknown!", e); + continue; + } + + final TaskProgress taskProgress = new TaskProgress(questProgress, taskId, uuid, progress, completed); + questProgress.addTaskProgress(taskProgress); + } + } + } + + final Collection allQuestProgress = questProgressMap.values(); + + for (final QuestProgress questProgress : allQuestProgress) { + questProgressFile.addQuestProgress(questProgress); + } + } catch (final SQLException e) { + this.plugin.getLogger().log(Level.SEVERE, "Failed to load player data for " + uuidString + ".", e); + return null; + } + + return new QPlayerData(uuid, null, questProgressFile); // TODO player preferences + } + + @Override + public boolean savePlayerData(final @NotNull QPlayerData playerData) { + Objects.requireNonNull(playerData, "playerData cannot be null"); + + if (this.fault) { + return false; + } + + final UUID uuid = playerData.playerUUID(); + final String uuidString = uuid.toString(); // call it only once + + try (final Connection connection = this.ds.getConnection(); + final PreparedStatement questStmt = connection.prepareStatement(this.prefixer.apply(INSERT_PLAYER_QUEST_PROGRESS)); + final PreparedStatement taskStmt = connection.prepareStatement(this.prefixer.apply(INSERT_PLAYER_TASK_PROGRESS))) { + + this.plugin.getQuestsLogger().debug("Saving player data for " + uuidString + "."); + + final QuestProgressFile questProgressFile = playerData.questProgressFile(); + + for (final QuestProgress questProgress : questProgressFile.getAllQuestProgress()) { + if (!questProgress.isModified()) { + continue; + } + + final String questId = questProgress.getQuestId(); + + questStmt.setString(1, uuidString); + questStmt.setString(2, questId); + questStmt.setBoolean(3, questProgress.isStarted()); + questStmt.setLong(4, questProgress.getStartedDate()); + questStmt.setBoolean(5, questProgress.isCompleted()); + questStmt.setBoolean(6, questProgress.isCompletedBefore()); + questStmt.setLong(7, questProgress.getCompletionDate()); + questStmt.setBoolean(8, questProgress.isStarted()); + questStmt.setLong(9, questProgress.getStartedDate()); + questStmt.setBoolean(10, questProgress.isCompleted()); + questStmt.setBoolean(11, questProgress.isCompletedBefore()); + questStmt.setLong(12, questProgress.getCompletionDate()); + questStmt.addBatch(); + + for (final TaskProgress taskProgress : questProgress.getAllTaskProgress()) { + final String taskId = taskProgress.getTaskId(); + + final Object progress = taskProgress.getProgress(); + final String progressString; + final String dataTypeString; + + switch (progress) { + case null -> { + progressString = null; + dataTypeString = null; + } + case Integer i -> { + progressString = Integer.toString(i); + dataTypeString = "int"; + } + case Float f -> { + progressString = Float.toString(f); + dataTypeString = "float"; + } + case Long l -> { + progressString = Long.toString(l); + dataTypeString = "long"; + } + case Double d -> { + progressString = Double.toString(d); + dataTypeString = "double"; + } + case BigInteger bi -> { + progressString = bi.toString(); + dataTypeString = "BigInteger"; + } + case BigDecimal bd -> { + progressString = bd.toString(); + dataTypeString = "BigDecimal"; + } + default -> { + this.plugin.getLogger().warning("Cannot retrieve progress for task '" + taskId + + "' in quest '" + questId + "' for player " + uuidString + " since a valid encoder for '" + + progress.getClass().getName() + "' class has not been found!"); + continue; + } + } + + taskStmt.setString(1, uuidString); + taskStmt.setString(2, questId); + taskStmt.setString(3, taskId); + taskStmt.setBoolean(4, taskProgress.isCompleted()); + taskStmt.setString(5, progressString); + taskStmt.setString(6, dataTypeString); + taskStmt.setBoolean(7, taskProgress.isCompleted()); + taskStmt.setString(8, progressString); + taskStmt.setString(9, dataTypeString); + taskStmt.addBatch(); + } + } + + questStmt.executeBatch(); + taskStmt.executeBatch(); + + return true; + } catch (final SQLException e) { + this.plugin.getLogger().log(Level.SEVERE, "Failed to save player data for " + uuidString + ".", e); + return false; + } + } + + @Override + public @NotNull List loadAllPlayerData() { + if (this.fault) { + return Collections.emptyList(); + } + + final List uuids = new ArrayList<>(); + + try (final Connection conn = this.ds.getConnection(); + final PreparedStatement stmt = conn.prepareStatement(this.prefixer.apply(SELECT_UUID_LIST)); + final ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + // Get it by index to speed up it a little bit + final String uuidString = rs.getString(1); + + final UUID uuid; + try { + uuid = UUID.fromString(uuidString); + } catch (final IllegalArgumentException e) { + this.plugin.getLogger().log(Level.SEVERE, "Failed to parse player UUID: '" + uuidString + "'.", e); + continue; + } + + uuids.add(uuid); + } + } catch (final SQLException e) { + this.plugin.getLogger().log(Level.SEVERE, "Failed to load player UUIDs.", e); + return Collections.emptyList(); + } + + final List allPlayerData = new ArrayList<>(); + + for (final UUID uuid : uuids) { + final QPlayerData playerData = this.loadPlayerData(uuid); + + if (playerData != null) { + allPlayerData.add(playerData); + } + } + + return allPlayerData; + } + + @SuppressWarnings("RedundantIfStatement") // I hate it, but keep it just for readability + @Override + public boolean isSimilar(final @NotNull StorageProvider otherProvider) { + Objects.requireNonNull(otherProvider, "otherProvider cannot be null"); + + if (!(otherProvider instanceof final ModernMySQLStorageProvider mySQLProvider)) { + return false; + } + + final String address = this.config.getString("network.address", "localhost:3306"); + final String otherAddress = mySQLProvider.config.getString("network.address", "localhost:3306"); + + if (!address.equals(otherAddress)) { + return false; + } + + final String database = this.config.getString("network.database", "minecraft"); + final String otherDatabase = mySQLProvider.config.getString("network.database", "minecraft"); + + if (!database.equals(otherDatabase)) { + return false; + } + + return true; + } + + private record DatabaseMigrator(@NotNull BukkitQuestsPlugin plugin, @NotNull Function prefixer, @NotNull Connection conn) { + + private static final String GET_STARTED_DATE_COLUMN = + "SHOW COLUMNS from `{prefix}quest_progress` LIKE 'started_date';"; + private static final String SELECT_SCHEMA_VERSION = + "SELECT value FROM `{prefix}database_information` WHERE `key` LIKE 'schema_version';"; + private static final String UPDATE_DATABASE_INFORMATION = + "INSERT INTO `{prefix}database_information` (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = ?;"; + + private static final int LATEST_SCHEMA_VERSION = 2; + private static final Map MIGRATION_STATEMENTS = new HashMap<>() {{ + this.put(1, "ALTER TABLE `{prefix}quest_progress` ADD COLUMN `started_date` BIGINT NOT NULL AFTER `started`;"); + }}; + + private DatabaseMigrator(final @NotNull BukkitQuestsPlugin plugin, final @NotNull Function prefixer, final @NotNull Connection conn) { + this.plugin = Objects.requireNonNull(plugin, "plugin cannot be null"); + this.prefixer = Objects.requireNonNull(prefixer, "prefixer cannot be null"); + this.conn = Objects.requireNonNull(conn, "conn cannot be null"); + } + + public int getInitialSchemaVersion() throws SQLException { + this.plugin.getQuestsLogger().debug("Getting initial schema version for new database."); + + try (final Statement stmt = this.conn.createStatement(); + final ResultSet rs = stmt.executeQuery(this.prefixer.apply(GET_STARTED_DATE_COLUMN))) { + + if (rs.first()) { + return LATEST_SCHEMA_VERSION; + } else { + return 1; + } + } + } + + public int getCurrentSchemaVersion() throws SQLException { + this.plugin.getQuestsLogger().debug("Getting current schema version."); + + try (final Statement stmt = this.conn.createStatement(); + final ResultSet rs = stmt.executeQuery(this.prefixer.apply(SELECT_SCHEMA_VERSION))) { + + if (rs.first()) { + final int version = Integer.parseUnsignedInt(rs.getString(1)); + this.plugin.getQuestsLogger().debug("Current schema version: " + version + "."); + return version; + } + + final int version = this.getInitialSchemaVersion(); + this.updateSchemaVersion(version); + + return version; + } + } + + public void updateSchemaVersion(final int updatedSchemaVersion) throws SQLException { + this.plugin.getQuestsLogger().debug("Updating schema version to " + updatedSchemaVersion + "."); + + try (final PreparedStatement stmt = this.conn.prepareStatement(this.prefixer.apply(UPDATE_DATABASE_INFORMATION))) { + stmt.setString(1, "schema_version"); + stmt.setString(2, Integer.toString(updatedSchemaVersion)); + stmt.setString(3, Integer.toString(updatedSchemaVersion)); + + stmt.executeUpdate(); + } + } + + public void upgrade(final int initialSchemaVersion) throws SQLException { + this.plugin.getQuestsLogger().debug("Starting upgrade from version " + initialSchemaVersion + " to " + LATEST_SCHEMA_VERSION + "."); + + for (int i = initialSchemaVersion; i < LATEST_SCHEMA_VERSION; i++) { + final String statementString = this.prefixer.apply(MIGRATION_STATEMENTS.get(i)); + this.plugin.getQuestsLogger().debug("Running migration statement: " + statementString + "."); + + try (final Statement stmt = this.conn.createStatement()) { + stmt.execute(statementString); + } catch (final SQLException e) { + this.plugin.getLogger().severe("Failed to run migration statement (" + i + " -> " + (i + 1) + "): " + statementString + "."); + this.plugin.getLogger().severe("Quests will attempt to save current migration progress to prevent database corruption, but may not be able to do so."); + this.updateSchemaVersion(i); + + // we still want it to throw and prevent further plugin loading + throw e; + } + } + + this.updateSchemaVersion(LATEST_SCHEMA_VERSION); + } + } +} diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/ModernYAMLStorageProvider.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/ModernYAMLStorageProvider.java new file mode 100644 index 00000000..668b4b89 --- /dev/null +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/ModernYAMLStorageProvider.java @@ -0,0 +1,284 @@ +package com.leonardobishop.quests.bukkit.storage; + +import com.leonardobishop.quests.bukkit.BukkitQuestsPlugin; +import com.leonardobishop.quests.common.player.QPlayerData; +import com.leonardobishop.quests.common.player.questprogressfile.QuestProgress; +import com.leonardobishop.quests.common.player.questprogressfile.QuestProgressFile; +import com.leonardobishop.quests.common.player.questprogressfile.TaskProgress; +import com.leonardobishop.quests.common.quest.Quest; +import com.leonardobishop.quests.common.quest.Task; +import com.leonardobishop.quests.common.storage.StorageProvider; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; + +public final class ModernYAMLStorageProvider implements StorageProvider { + + private final BukkitQuestsPlugin plugin; + private final File dataDirectory; + private final Map lockMap; + + private boolean validateQuests; + + public ModernYAMLStorageProvider(final @NotNull BukkitQuestsPlugin plugin) { + this.plugin = Objects.requireNonNull(plugin, "plugin cannot be null"); + this.dataDirectory = new File(plugin.getDataFolder(), "playerdata"); + this.lockMap = new ConcurrentHashMap<>(); + } + + @Override + public @NotNull String getName() { + return "yaml"; + } + + @Override + public void init() { + //noinspection ResultOfMethodCallIgnored + this.dataDirectory.mkdirs(); + + // not really useful now, but maybe in the future it will be reloadable + this.validateQuests = this.plugin.getConfig().getBoolean("options.verify-quest-exists-on-load", true); + } + + @Override + public void shutdown() { + // no implementation needed + } + + @Override + public @Nullable QPlayerData loadPlayerData(final @NotNull UUID uuid) { + Objects.requireNonNull(uuid, "uuid cannot be null"); + + final String uuidString = uuid.toString(); + final QuestProgressFile questProgressFile = new QuestProgressFile(this.plugin, uuid); + final File dataFile = new File(this.dataDirectory, uuidString + ".yml"); + + final ReentrantLock lock = this.lock(uuid); + + try { + if (dataFile.isFile()) { + final YamlConfiguration data = new YamlConfiguration(); + data.load(dataFile); + + this.plugin.getQuestsLogger().debug("Player " + uuidString + " has a valid quest progress file."); + + final ConfigurationSection questProgressSection = data.getConfigurationSection("quest-progress"); + + if (questProgressSection != null) { + final Set questIds = questProgressSection.getKeys(false); + + for (final String questId : questIds) { + final Quest quest; + + if (this.validateQuests) { + quest = this.plugin.getQuestManager().getQuestById(questId); + + if (quest == null) { + continue; + } + } else { + quest = null; + } + + final ConfigurationSection questSection = questProgressSection.getConfigurationSection(questId); + + //noinspection DataFlowIssue + final boolean qStarted = questSection.getBoolean("started", false); + final long qStartedDate = questSection.getLong("started-date", 0L); + final boolean qCompleted = questSection.getBoolean("completed", false); + final boolean qCompletedBefore = questSection.getBoolean("completed-before", false); + final long qCompletionDate = questSection.getLong("completion-date", 0L); + + final QuestProgress questProgress = new QuestProgress(this.plugin, questId, uuid, qStarted, qStartedDate, qCompleted, qCompletedBefore, qCompletionDate); + + final ConfigurationSection taskProgressSection = questSection.getConfigurationSection("task-progress"); + + if (taskProgressSection != null) { + final Set taskIds = taskProgressSection.getKeys(false); + + for (final String taskId : taskIds) { + // quest is not null only if this.validateQuests is true + if (quest != null) { + final Task task = quest.getTaskById(taskId); + + if (task == null) { + continue; + } + } + + final ConfigurationSection taskSection = taskProgressSection.getConfigurationSection(taskId); + + //noinspection DataFlowIssue + final boolean tCompleted = taskSection.getBoolean("completed", false); + final Object tProgress = taskSection.get("progress", null); + + final TaskProgress taskProgress = new TaskProgress(questProgress, taskId, uuid, tProgress, tCompleted); + questProgress.addTaskProgress(taskProgress); + } + } + + questProgressFile.addQuestProgress(questProgress); + } + } + } else { + this.plugin.getQuestsLogger().debug("Player " + uuidString + " does not have a quest progress file."); + } + + return new QPlayerData(uuid, null, questProgressFile); // TODO player preferences + } catch (final FileNotFoundException e) { + this.plugin.getLogger().log(Level.SEVERE, "Failed to find player data file for " + uuidString + ".", e); + } catch (final IOException e) { + this.plugin.getLogger().log(Level.SEVERE, "Failed to read player data file for " + uuidString + ".", e); + } catch (final InvalidConfigurationException e) { + this.plugin.getLogger().log(Level.SEVERE, "Failed to parse player data file for " + uuidString + ".", e); + } finally { + lock.unlock(); + } + + return null; + } + + @Override + public boolean savePlayerData(final @NotNull QPlayerData playerData) { + Objects.requireNonNull(playerData, "playerData cannot be null"); + + final UUID uuid = playerData.playerUUID(); + final String uuidString = uuid.toString(); + final QuestProgressFile questProgressFile = playerData.questProgressFile(); + + final ReentrantLock lock = this.lock(uuid); + + try { + final File dataFile = new File(this.dataDirectory, uuidString + ".yml"); + final YamlConfiguration data = new YamlConfiguration(); + + if (dataFile.isFile()) { + data.load(dataFile); + this.plugin.getQuestsLogger().debug("Player " + uuidString + " has a valid quest progress file."); + } else { + this.plugin.getQuestsLogger().debug("Player " + uuidString + " does not have a quest progress file."); + } + + for (final QuestProgress questProgress : questProgressFile.getAllQuestProgress()) { + if (!questProgress.isModified()) { + continue; + } + + final String questId = questProgress.getQuestId(); + + data.set("quest-progress." + questId + ".started", questProgress.isStarted()); + data.set("quest-progress." + questId + ".started-date", questProgress.getStartedDate()); + data.set("quest-progress." + questId + ".completed", questProgress.isCompleted()); + data.set("quest-progress." + questId + ".completed-before", questProgress.isCompletedBefore()); + data.set("quest-progress." + questId + ".completion-date", questProgress.getCompletionDate()); + + for (final TaskProgress taskProgress : questProgress.getAllTaskProgress()) { + final String taskId = taskProgress.getTaskId(); + + data.set("quest-progress." + questId + ".task-progress." + taskId + ".completed", taskProgress.isCompleted()); + data.set("quest-progress." + questId + ".task-progress." + taskId + ".progress", taskProgress.getProgress()); + } + } + + this.plugin.getQuestsLogger().debug("Saving player data file for " + uuidString + " to disk."); + + try { + data.save(dataFile); + return true; + } catch (final IOException e) { + this.plugin.getLogger().log(Level.SEVERE, "Failed to write player data file for " + uuidString + ".", e); + } + } catch (final FileNotFoundException e) { + this.plugin.getLogger().log(Level.SEVERE, "Failed to find player data file for " + uuidString + ".", e); + } catch (final IOException e) { + this.plugin.getLogger().log(Level.SEVERE, "Failed to read player data file for " + uuidString + ".", e); + } catch (final InvalidConfigurationException e) { + this.plugin.getLogger().log(Level.SEVERE, "Failed to parse player data file for " + uuidString + ".", e); + } finally { + lock.unlock(); + } + + return false; + } + + @Override + public @NotNull List loadAllPlayerData() { + final List allPlayerData = new ArrayList<>(); + final PlayerDataVisitor playerDataVisitor = new PlayerDataVisitor(this, allPlayerData); + + try { + Files.walkFileTree(this.dataDirectory.toPath(), playerDataVisitor); + } catch (final IOException e) { + this.plugin.getLogger().log(Level.SEVERE, "Failed to walk the player data file tree", e); + } + + return allPlayerData; + } + + @Override + public boolean isSimilar(final @NotNull StorageProvider otherProvider) { + return otherProvider instanceof ModernYAMLStorageProvider; + } + + private @NotNull ReentrantLock lock(final @NotNull UUID uuid) { + final ReentrantLock lock = this.lockMap.computeIfAbsent(uuid, k -> new ReentrantLock()); + lock.lock(); + return lock; + } + + private static class PlayerDataVisitor extends SimpleFileVisitor { + + private static final String FILE_EXTENSION = ".yml"; + + private final ModernYAMLStorageProvider provider; + private final List allPlayerData; + + public PlayerDataVisitor(final @NotNull ModernYAMLStorageProvider provider, final @NotNull List allPlayerData) { + this.provider = provider; + this.allPlayerData = allPlayerData; + } + + @Override + public @NotNull FileVisitResult visitFile(final @NotNull Path path, final @NotNull BasicFileAttributes attributes) { + final String fileName = path.toFile().getName(); + final String uuidString = fileName.substring(0, fileName.length() - FILE_EXTENSION.length()); + + if (fileName.endsWith(FILE_EXTENSION)) { + final UUID uuid; + try { + uuid = UUID.fromString(uuidString); + } catch (final IllegalArgumentException e) { + this.provider.plugin.getLogger().log(Level.SEVERE, "Failed to parse player UUID: '" + uuidString + "'.", e); + return FileVisitResult.CONTINUE; + } + + final QPlayerData playerData = this.provider.loadPlayerData(uuid); + if (playerData != null) { + this.allPlayerData.add(playerData); + } + } + + return FileVisitResult.CONTINUE; + } + } +} diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/MySqlStorageProvider.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/MySqlStorageProvider.java deleted file mode 100644 index f8eaefba..00000000 --- a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/MySqlStorageProvider.java +++ /dev/null @@ -1,471 +0,0 @@ -package com.leonardobishop.quests.bukkit.storage; - -import com.leonardobishop.quests.bukkit.BukkitQuestsPlugin; -import com.leonardobishop.quests.common.player.questprogressfile.QuestProgress; -import com.leonardobishop.quests.common.player.questprogressfile.QuestProgressFile; -import com.leonardobishop.quests.common.player.questprogressfile.TaskProgress; -import com.leonardobishop.quests.common.quest.Quest; -import com.leonardobishop.quests.common.storage.StorageProvider; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.YamlConfiguration; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.math.BigDecimal; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import java.util.function.Function; - -public class MySqlStorageProvider implements StorageProvider { - - private static final String CREATE_TABLE_QUEST_PROGRESS = - "CREATE TABLE IF NOT EXISTS `{prefix}quest_progress` (" + - " `uuid` VARCHAR(36) NOT NULL," + - " `quest_id` VARCHAR(50) NOT NULL," + - " `started` BOOL NOT NULL," + - " `started_date` BIGINT NOT NULL," + - " `completed` BOOL NOT NULL," + - " `completed_before` BOOL NOT NULL," + - " `completion_date` BIGINT NOT NULL," + - " PRIMARY KEY (`uuid`, `quest_id`));"; - private static final String CREATE_TABLE_TASK_PROGRESS = - "CREATE TABLE IF NOT EXISTS `{prefix}task_progress` (" + - " `uuid` VARCHAR(36) NOT NULL," + - " `quest_id` VARCHAR(50) NOT NULL," + - " `task_id` VARCHAR(50) NOT NULL," + - " `completed` BOOL NOT NULL," + - " `progress` VARCHAR(64) NULL," + - " `data_type` VARCHAR(10) NULL," + - " PRIMARY KEY (`uuid`, `quest_id`, `task_id`));"; - private static final String CREATE_TABLE_DATABASE_INFORMATION = - "CREATE TABLE IF NOT EXISTS `{prefix}database_information` (" + - " `key` VARCHAR(255) NOT NULL," + - " `value` VARCHAR(255) NOT NULL," + - " PRIMARY KEY (`key`));"; - private static final String SELECT_PLAYER_QUEST_PROGRESS = - "SELECT quest_id, started, started_date, completed, completed_before, completion_date FROM `{prefix}quest_progress` WHERE uuid=?;"; - private static final String SELECT_PLAYER_TASK_PROGRESS = - "SELECT quest_id, task_id, completed, progress, data_type FROM `{prefix}task_progress` WHERE uuid=?;"; - private static final String SELECT_UUID_LIST = - "SELECT DISTINCT uuid FROM `{prefix}quest_progress`;"; - private static final String SELECT_KNOWN_PLAYER_QUEST_PROGRESS = - "SELECT quest_id FROM `{prefix}quest_progress` WHERE uuid=?;"; - private static final String SELECT_KNOWN_PLAYER_TASK_PROGRESS = - "SELECT quest_id, task_id FROM `{prefix}task_progress` WHERE uuid=?;"; - private static final String WRITE_PLAYER_QUEST_PROGRESS = - "INSERT INTO `{prefix}quest_progress` (uuid, quest_id, started, started_date, completed, completed_before, completion_date) VALUES (?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE started=?, started_date=?, completed=?, completed_before=?, completion_date=?"; - private static final String WRITE_PLAYER_TASK_PROGRESS = - "INSERT INTO `{prefix}task_progress` (uuid, quest_id, task_id, completed, progress, data_type) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE completed=?, progress=?, data_type=?"; - - private final ConfigurationSection configuration; - private final BukkitQuestsPlugin plugin; - private HikariDataSource hikari; - private String prefix; - private Function statementProcessor; - private boolean fault; - - public MySqlStorageProvider(BukkitQuestsPlugin plugin, ConfigurationSection configuration) { - this.plugin = plugin; - if (configuration == null) { - configuration = new YamlConfiguration(); - } - this.configuration = configuration; - this.fault = true; - } - - @Override - public String getName() { - return "mysql"; - } - - @Override - public void init() { - String address = configuration.getString("network.address", "localhost:3306"); - String database = configuration.getString("network.database", "minecraft"); - String url = "jdbc:mysql://" + address + "/" + database; - - HikariConfig config = new HikariConfig(); - config.setPoolName("quests-hikari"); - - config.setUsername(configuration.getString("network.username", "root")); - config.setPassword(configuration.getString("network.password", "")); - config.setJdbcUrl(url); - config.setMaximumPoolSize(configuration.getInt("connection-pool-settings.maximum-pool-size", 8)); - config.setMinimumIdle(configuration.getInt("connection-pool-settings.minimum-idle", 8)); - config.setMaxLifetime(configuration.getInt("connection-pool-settings.maximum-lifetime", 1800000)); - config.setConnectionTimeout(configuration.getInt("connection-pool-settings.connection-timeout", 5000)); - - config.addDataSourceProperty("cachePrepStmts", true); - config.addDataSourceProperty("prepStmtCacheSize", 250); - config.addDataSourceProperty("prepStmtCacheSqlLimit", 2048); - config.addDataSourceProperty("useServerPrepStmts", true); - config.addDataSourceProperty("useLocalSessionState", true); - config.addDataSourceProperty("rewriteBatchedStatements", true); - config.addDataSourceProperty("cacheResultSetMetadata", true); - config.addDataSourceProperty("cacheServerConfiguration", true); - config.addDataSourceProperty("elideSetAutoCommits", true); - config.addDataSourceProperty("maintainTimeStats", false); - - if (configuration.isConfigurationSection("connection-pool-settings.data-source-properties")) { - for (String property : configuration.getConfigurationSection("connection-pool-settings.data-source-properties").getKeys(false)) { - config.addDataSourceProperty(property, configuration.get("connection-pool-settings.data-source-properties." + property)); - } - } - - this.hikari = new HikariDataSource(config); - this.prefix = configuration.getString("database-settings.table-prefix", "quests_"); - this.statementProcessor = s -> s.replace("{prefix}", prefix); - try (Connection connection = hikari.getConnection()) { - try (Statement s = connection.createStatement()) { - plugin.getQuestsLogger().debug("Creating default tables"); - s.addBatch(this.statementProcessor.apply(CREATE_TABLE_QUEST_PROGRESS)); - s.addBatch(this.statementProcessor.apply(CREATE_TABLE_TASK_PROGRESS)); - s.addBatch(this.statementProcessor.apply(CREATE_TABLE_DATABASE_INFORMATION)); - - s.executeBatch(); - } - DatabaseMigrator migrator = new DatabaseMigrator(connection); - - int currentVersion = migrator.getCurrentSchemaVersion(); - if (currentVersion < DatabaseMigrator.CURRENT_SCHEMA_VERSION) { - plugin.getQuestsLogger().info("Automatically upgrading database schema from version " + currentVersion + " to " + DatabaseMigrator.CURRENT_SCHEMA_VERSION); - migrator.upgrade(currentVersion); - } - } catch (SQLException e) { - throw new RuntimeException(e); - } - this.fault = false; - } - - @Override - public void shutdown() { - if (hikari != null) hikari.close(); - } - - @Override - @Nullable - public QuestProgressFile loadProgressFile(@NotNull UUID uuid) { - Objects.requireNonNull(uuid, "uuid cannot be null"); - - if (fault) return null; - Map presentQuests = new HashMap<>(plugin.getQuestManager().getQuests()); - boolean validateQuests = plugin.getQuestsConfig().getBoolean("options.verify-quest-exists-on-load", true); - - QuestProgressFile questProgressFile = new QuestProgressFile(uuid, plugin); - try (Connection connection = hikari.getConnection()) { - plugin.getQuestsLogger().debug("Querying player " + uuid); - Map questProgressMap = new HashMap<>(); - try (PreparedStatement ps = connection.prepareStatement(this.statementProcessor.apply(SELECT_PLAYER_QUEST_PROGRESS))) { - ps.setString(1, uuid.toString()); - - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - String questId = rs.getString(1); - boolean started = rs.getBoolean(2); - long startedDate = rs.getLong(3); - boolean completed = rs.getBoolean(4); - boolean completedBefore = rs.getBoolean(5); - long completionDate = rs.getLong(6); - - if (validateQuests && !presentQuests.containsKey(questId)) continue; - QuestProgress questProgress = new QuestProgress(plugin, questId, completed, completedBefore, completionDate, uuid, started, startedDate); - questProgressMap.put(questId, questProgress); - } - } - } - try (PreparedStatement ps = connection.prepareStatement(this.statementProcessor.apply(SELECT_PLAYER_TASK_PROGRESS))) { - ps.setString(1, uuid.toString()); - - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - String questId = rs.getString(1); - String taskId = rs.getString(2); - boolean completed = rs.getBoolean(3); - String encodedProgress = rs.getString(4); - String type = rs.getString(5); - Object progress; - try { - if (type == null) { - progress = null; - } else if (type.equals("double")) { - progress = Double.valueOf(encodedProgress); - } else if (type.equals("float")) { - progress = Float.valueOf(encodedProgress); - } else if (type.equals("int")) { - progress = Integer.valueOf(encodedProgress); - } else if (type.equals("BigDecimal")) { - progress = new BigDecimal(encodedProgress); - } else { - throw new RuntimeException("unknown data type '" + type + "'"); - } - } catch (NumberFormatException ex) { - plugin.getQuestsLogger().warning("Cannot retrieve progress for task '" - + taskId + "' in quest '" + questId + "' for player " + uuid - + " since data is malformed!"); - continue; - } catch (RuntimeException ex) { - if (ex.getMessage().startsWith("unknown data type ")) { - plugin.getQuestsLogger().warning("Cannot retrieve progress for task '" - + taskId + "' in quest '" + questId + "' for player " + uuid - + ": " + ex.getMessage()); - continue; - } else { - throw ex; - } - } - - QuestProgress linkedQuestProgress = questProgressMap.get(questId); - if (linkedQuestProgress == null) continue; - if (validateQuests) { - if (!presentQuests.containsKey(questId)) continue; - if (presentQuests.get(questId).getTaskById(taskId) == null) continue; - } - TaskProgress questProgress = new TaskProgress(linkedQuestProgress, taskId, progress, uuid, completed); - linkedQuestProgress.addTaskProgress(questProgress); - } - } - } - for (QuestProgress questProgress : questProgressMap.values()) { - questProgressFile.addQuestProgress(questProgress); - } - } catch (SQLException e) { - e.printStackTrace(); - return null; - } - return questProgressFile; - } - - @Override - public boolean saveProgressFile(@NotNull UUID uuid, @NotNull QuestProgressFile questProgressFile) { - Objects.requireNonNull(uuid, "uuid cannot be null"); - Objects.requireNonNull(questProgressFile, "questProgressFile cannot be null"); - - if (fault) return false; - try (Connection connection = hikari.getConnection()) { - try (PreparedStatement writeQuestProgress = connection.prepareStatement(this.statementProcessor.apply(WRITE_PLAYER_QUEST_PROGRESS)); - PreparedStatement writeTaskProgress = connection.prepareStatement(this.statementProcessor.apply(WRITE_PLAYER_TASK_PROGRESS))) { - - List questProgressValues = new ArrayList<>(questProgressFile.getAllQuestProgress()); - for (QuestProgress questProgress : questProgressValues) { - if (!questProgress.isModified()) continue; - - String questId = questProgress.getQuestId(); - writeQuestProgress.setString(1, uuid.toString()); - writeQuestProgress.setString(2, questProgress.getQuestId()); - writeQuestProgress.setBoolean(3, questProgress.isStarted()); - writeQuestProgress.setLong(4, questProgress.getStartedDate()); - writeQuestProgress.setBoolean(5, questProgress.isCompleted()); - writeQuestProgress.setBoolean(6, questProgress.isCompletedBefore()); - writeQuestProgress.setLong(7, questProgress.getCompletionDate()); - writeQuestProgress.setBoolean(8, questProgress.isStarted()); - writeQuestProgress.setLong(9, questProgress.getStartedDate()); - writeQuestProgress.setBoolean(10, questProgress.isCompleted()); - writeQuestProgress.setBoolean(11, questProgress.isCompletedBefore()); - writeQuestProgress.setLong(12, questProgress.getCompletionDate()); - writeQuestProgress.addBatch(); - - for (TaskProgress taskProgress : questProgress.getTaskProgress()) { - String taskId = taskProgress.getTaskId(); - - String encodedProgress; - Object progress = taskProgress.getProgress(); - String type; - if (progress == null) { - type = null; - encodedProgress = null; - } else if (progress instanceof Double) { - type = "double"; - encodedProgress = String.valueOf(progress); - } else if (progress instanceof Integer) { - type = "int"; - encodedProgress = String.valueOf(progress); - } else if (progress instanceof Float) { - type = "float"; - encodedProgress = String.valueOf(progress); - } else if (progress instanceof BigDecimal) { - type = "BigDecimal"; - encodedProgress = String.valueOf(progress); - } else { - plugin.getQuestsLogger().warning("Cannot store progress for task '" - + taskId + "' in quest '" + questId + "' for player " + uuid - + " since type " + progress.getClass().getName() + " cannot be encoded!"); - continue; - } - writeTaskProgress.setString(1, uuid.toString()); - writeTaskProgress.setString(2, questId); - writeTaskProgress.setString(3, taskProgress.getTaskId()); - writeTaskProgress.setBoolean(4, taskProgress.isCompleted()); - writeTaskProgress.setString(5, encodedProgress); - writeTaskProgress.setString(6, type); - writeTaskProgress.setBoolean(7, taskProgress.isCompleted()); - writeTaskProgress.setString(8, encodedProgress); - writeTaskProgress.setString(9, type); - writeTaskProgress.addBatch(); - } - } - - writeQuestProgress.executeBatch(); - writeTaskProgress.executeBatch(); - } - return true; - } catch (SQLException e) { - e.printStackTrace(); - return false; - } - } - - @Override - public @NotNull List loadAllProgressFiles() { - if (fault) return Collections.emptyList(); - - Set uuids = new HashSet<>(); - - try (Connection connection = hikari.getConnection()) { - try (PreparedStatement ps = connection.prepareStatement(this.statementProcessor.apply(SELECT_UUID_LIST))) { - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - String uuidString = rs.getString(1); - try { - UUID uuid = UUID.fromString(uuidString); - uuids.add(uuid); - } catch (IllegalArgumentException ignored) { } - } - } - } - } catch (SQLException e) { - e.printStackTrace(); - return Collections.emptyList(); - } - - List files = new ArrayList<>(); - for (UUID uuid : uuids) { - QuestProgressFile file = loadProgressFile(uuid); - if (file != null) { - files.add(file); - } - } - - return files; - } - - @Override - public void saveAllProgressFiles(List files) { - if (fault) return; - - for (QuestProgressFile file : files) { - saveProgressFile(file.getPlayerUUID(), file); - } - } - - @Override - public boolean isSimilar(StorageProvider provider) { - if (!(provider instanceof MySqlStorageProvider)) { - return false; - } - - MySqlStorageProvider other = (MySqlStorageProvider) provider; - - String address = configuration.getString("network.address", "localhost:3306"); - String database = configuration.getString("network.database", "minecraft"); - - String otherAddress = other.configuration.getString("network.address", "localhost:3306"); - String otherDatabase = other.configuration.getString("network.database", "minecraft"); - - return address.equalsIgnoreCase(otherAddress) && database.equalsIgnoreCase(otherDatabase); - } - - private class DatabaseMigrator { - private static final String GET_STARTED_DATE_COLUMN = - "SHOW COLUMNS from `{prefix}quest_progress` LIKE 'started_date';"; - private static final String SELECT_SCHEMA_VERSION = - "SELECT value FROM `{prefix}database_information` WHERE `key`='schema_version';"; - private static final String UPDATE_DATABASE_INFORMATION = - "INSERT INTO `{prefix}database_information` (`key`, `value`) VALUES (?,?) ON DUPLICATE KEY UPDATE `value`=?;"; - private static final int CURRENT_SCHEMA_VERSION = 2; - - private final Map migrationStatements = new HashMap<>(); - - private final Connection connection; - - public DatabaseMigrator(Connection connection) { - this.connection = connection; - - this.migrationStatements.put(1, - "ALTER TABLE `{prefix}quest_progress` ADD COLUMN `started_date` BIGINT NOT NULL AFTER `started`;"); - } - - public int getInitialSchemaVersion() { - try (Statement statement = connection.createStatement()) { - plugin.getQuestsLogger().debug("Getting initial schema version for new database"); - ResultSet rs = statement.executeQuery(statementProcessor.apply(GET_STARTED_DATE_COLUMN)); - boolean hasStartedDateColumn = rs.next(); - - return hasStartedDateColumn ? CURRENT_SCHEMA_VERSION : 1; - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - public int getCurrentSchemaVersion() { - try (Statement statement = connection.createStatement()) { - plugin.getQuestsLogger().debug("Getting current schema version"); - ResultSet rs = statement.executeQuery(statementProcessor.apply(SELECT_SCHEMA_VERSION)); - if (rs.next()) { - int version = Integer.parseInt(rs.getString(1)); - plugin.getQuestsLogger().debug("Current schema version: " + version); - return version; - } else { - int initialVersion = getInitialSchemaVersion(); - updateSchemaVersion(initialVersion); - return initialVersion; - } - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - public void upgrade(int initialSchemaVersion) { - plugin.getQuestsLogger().debug("Starting upgrade from version " + initialSchemaVersion + " to " + CURRENT_SCHEMA_VERSION); - for (int i = initialSchemaVersion; i < CURRENT_SCHEMA_VERSION; i++) { - String statement = statementProcessor.apply(migrationStatements.get(i)); - plugin.getQuestsLogger().debug("Running migration statement: " + statement); - try (Statement stmt = connection.createStatement()) { - stmt.execute(statementProcessor.apply(statement)); - } catch (SQLException e) { - plugin.getQuestsLogger().severe("Failed to run migration statement (" + i + " -> " + (i + 1) + "): " + statement); - plugin.getQuestsLogger().severe("Quests will attempt to save current migration progress to prevent database corruption, but may not be able to do so"); - updateSchemaVersion(i); - throw new RuntimeException(e); - } - } - updateSchemaVersion(CURRENT_SCHEMA_VERSION); - } - - public void updateSchemaVersion(int version) { - plugin.getQuestsLogger().debug("Updating schema version to " + version); - try (PreparedStatement stmt = connection.prepareStatement(statementProcessor.apply(UPDATE_DATABASE_INFORMATION))) { - stmt.setString(1, "schema_version"); - stmt.setString(2, String.valueOf(version)); - stmt.setString(3, String.valueOf(version)); - - stmt.execute(); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - } -} diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/YamlStorageProvider.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/YamlStorageProvider.java deleted file mode 100644 index e74212af..00000000 --- a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/YamlStorageProvider.java +++ /dev/null @@ -1,212 +0,0 @@ -package com.leonardobishop.quests.bukkit.storage; - -import com.leonardobishop.quests.bukkit.BukkitQuestsPlugin; -import com.leonardobishop.quests.common.player.questprogressfile.QuestProgress; -import com.leonardobishop.quests.common.player.questprogressfile.QuestProgressFile; -import com.leonardobishop.quests.common.player.questprogressfile.TaskProgress; -import com.leonardobishop.quests.common.quest.Quest; -import com.leonardobishop.quests.common.storage.StorageProvider; -import org.bukkit.configuration.file.YamlConfiguration; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.IOException; -import java.nio.file.FileVisitResult; -import java.nio.file.FileVisitor; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReentrantLock; - -public class YamlStorageProvider implements StorageProvider { - - private final Map locks = new ConcurrentHashMap<>(); - private final BukkitQuestsPlugin plugin; - - public YamlStorageProvider(BukkitQuestsPlugin plugin) { - this.plugin = plugin; - } - - private ReentrantLock lock(UUID uuid) { - locks.putIfAbsent(uuid, new ReentrantLock()); - ReentrantLock lock = locks.get(uuid); - lock.lock(); - return lock; - } - - @Override - public String getName() { - return "yaml"; - } - - @Override - public void init() { - File directory = new File(plugin.getDataFolder() + File.separator + "playerdata"); - directory.mkdirs(); - } - - @Override - public void shutdown() { - // no impl - } - - public @Nullable QuestProgressFile loadProgressFile(@NotNull UUID uuid) { - Objects.requireNonNull(uuid, "uuid cannot be null"); - - ReentrantLock lock = lock(uuid); - Map presentQuests = new HashMap<>(plugin.getQuestManager().getQuests()); - boolean validateQuests = plugin.getQuestsConfig().getBoolean("options.verify-quest-exists-on-load", true); - - QuestProgressFile questProgressFile = new QuestProgressFile(uuid, plugin); - try { - File directory = new File(plugin.getDataFolder() + File.separator + "playerdata"); - if (directory.exists() && directory.isDirectory()) { - File file = new File(plugin.getDataFolder() + File.separator + "playerdata" + File.separator + uuid.toString() + ".yml"); - if (file.exists()) { - YamlConfiguration data = YamlConfiguration.loadConfiguration(file); - plugin.getQuestsLogger().debug("Player " + uuid + " has a valid quest progress file."); - if (data.isConfigurationSection("quest-progress")) { //Same job as "isSet" + it checks if is CfgSection - for (String id : data.getConfigurationSection("quest-progress").getKeys(false)) { - boolean started = data.getBoolean("quest-progress." + id + ".started"); - long startedDate = data.getLong("quest-progress." + id + ".started-date"); - boolean completed = data.getBoolean("quest-progress." + id + ".completed"); - boolean completedBefore = data.getBoolean("quest-progress." + id + ".completed-before"); - long completionDate = data.getLong("quest-progress." + id + ".completion-date"); - - if (validateQuests && !presentQuests.containsKey(id)) continue; - - QuestProgress questProgress = new QuestProgress(plugin, id, completed, completedBefore, completionDate, uuid, started, startedDate, true); - - if (data.isConfigurationSection("quest-progress." + id + ".task-progress")) { - for (String taskid : data.getConfigurationSection("quest-progress." + id + ".task-progress").getKeys(false)) { - boolean taskCompleted = data.getBoolean("quest-progress." + id + ".task-progress." + taskid + ".completed"); - Object taskProgression = data.get("quest-progress." + id + ".task-progress." + taskid + ".progress"); - - if (validateQuests && presentQuests.get(id).getTaskById(taskid) == null) continue; - - TaskProgress taskProgress = new TaskProgress(questProgress, taskid, taskProgression, uuid, taskCompleted, false); - questProgress.addTaskProgress(taskProgress); - } - } - - questProgressFile.addQuestProgress(questProgress); - } - } - } else { - plugin.getQuestsLogger().debug("Player " + uuid + " does not have a quest progress file."); - } - } - } catch (Exception ex) { - ex.printStackTrace(); - return null; - } finally { - lock.unlock(); - } - - return questProgressFile; - } - - public boolean saveProgressFile(@NotNull UUID uuid, @NotNull QuestProgressFile questProgressFile) { - Objects.requireNonNull(uuid, "uuid cannot be null"); - Objects.requireNonNull(questProgressFile, "questProgressFile cannot be null"); - - ReentrantLock lock = lock(uuid); - try { - List questProgressValues = new ArrayList<>(questProgressFile.getAllQuestProgress()); - File directory = new File(plugin.getDataFolder() + File.separator + "playerdata"); - if (!directory.exists() && !directory.isDirectory()) { - directory.mkdirs(); - } - - File file = new File(plugin.getDataFolder() + File.separator + "playerdata" + File.separator + uuid.toString() + ".yml"); - if (!file.exists()) { - try { - file.createNewFile(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - YamlConfiguration data = YamlConfiguration.loadConfiguration(file); - for (QuestProgress questProgress : questProgressValues) { - if (!questProgress.isModified()) continue; - data.set("quest-progress." + questProgress.getQuestId() + ".started", questProgress.isStarted()); - data.set("quest-progress." + questProgress.getQuestId() + ".started-date", questProgress.getStartedDate()); - data.set("quest-progress." + questProgress.getQuestId() + ".completed", questProgress.isCompleted()); - data.set("quest-progress." + questProgress.getQuestId() + ".completed-before", questProgress.isCompletedBefore()); - data.set("quest-progress." + questProgress.getQuestId() + ".completion-date", questProgress.getCompletionDate()); - for (TaskProgress taskProgress : questProgress.getTaskProgress()) { - data.set("quest-progress." + questProgress.getQuestId() + ".task-progress." + taskProgress.getTaskId() + ".completed", taskProgress - .isCompleted()); - data.set("quest-progress." + questProgress.getQuestId() + ".task-progress." + taskProgress.getTaskId() + ".progress", taskProgress - .getProgress()); - } - } - - plugin.getQuestsLogger().debug("Writing player " + uuid + " to disk."); - try { - data.save(file); - return true; - } catch (IOException e) { - e.printStackTrace(); - return false; - } - } finally { - lock.unlock(); - } - } - - public @NotNull List loadAllProgressFiles() { - List files = new ArrayList<>(); - - File directory = new File(plugin.getDataFolder() + File.separator + "playerdata"); - FileVisitor fileVisitor = new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path path, BasicFileAttributes attributes) { - if (path.toString().endsWith(".yml")) { - UUID uuid; - try { - uuid = UUID.fromString(path.getFileName().toString().replace(".yml", "")); - } catch (IllegalArgumentException e) { - return FileVisitResult.CONTINUE; - } - - QuestProgressFile file = loadProgressFile(uuid); - if (file != null) { - files.add(file); - } - } - return FileVisitResult.CONTINUE; - } - }; - - try { - Files.walkFileTree(directory.toPath(), fileVisitor); - } catch (IOException e) { - e.printStackTrace(); - } - - return files; - } - - @Override - public void saveAllProgressFiles(List files) { - for (QuestProgressFile file : files) { - saveProgressFile(file.getPlayerUUID(), file); - } - } - - @Override - public boolean isSimilar(StorageProvider provider) { - return provider instanceof YamlStorageProvider; - } -} diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/util/CommandUtils.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/util/CommandUtils.java index b17c6325..56463bf0 100644 --- a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/util/CommandUtils.java +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/util/CommandUtils.java @@ -4,11 +4,12 @@ import com.leonardobishop.quests.bukkit.BukkitQuestsPlugin; import com.leonardobishop.quests.bukkit.util.chat.Chat; import com.leonardobishop.quests.common.config.ConfigProblem; import com.leonardobishop.quests.common.player.QPlayer; -import com.leonardobishop.quests.common.player.questprogressfile.QuestProgressFile; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.HashMap; @@ -170,17 +171,21 @@ public class CommandUtils { } } - public static void doSafeSave(QPlayer qPlayer, QuestProgressFile questProgressFile, BukkitQuestsPlugin plugin) { - if (Bukkit.getPlayer(qPlayer.getPlayerUUID()) == null) { - plugin.getScheduler().doAsync(() -> { - plugin.getPlayerManager().savePlayerSync(qPlayer.getPlayerUUID(), questProgressFile); - plugin.getScheduler().doSync(() -> { - if (Bukkit.getPlayer(qPlayer.getPlayerUUID()) == null) { + public static void doSafeSave(final @NotNull BukkitQuestsPlugin plugin, final @NotNull QPlayer qPlayer) { + final Player playerBeforeSave = Bukkit.getPlayer(qPlayer.getPlayerUUID()); + + if (playerBeforeSave != null) { + return; + } + + plugin.getPlayerManager() + .savePlayer(qPlayer.getPlayerData()) + .thenAccept(unused -> plugin.getScheduler().doSync(() -> { + final Player playerAfterSave = Bukkit.getPlayer(qPlayer.getPlayerUUID()); + + if (playerAfterSave == null) { plugin.getPlayerManager().dropPlayer(qPlayer.getPlayerUUID()); } - }); - }); - } + })); } - } diff --git a/bukkit/src/main/resources/resources/bukkit/config.yml b/bukkit/src/main/resources/resources/bukkit/config.yml index a550a41b..8f432f14 100644 --- a/bukkit/src/main/resources/resources/bukkit/config.yml +++ b/bukkit/src/main/resources/resources/bukkit/config.yml @@ -178,16 +178,66 @@ options: password: "" # Address should be in the format ip:port (just like the game itself) address: "localhost:3306" - # This plugin uses 'HikariCP' for connection management, the pooling configuration can be changed here + # This plugin uses HikariCP (https://github.com/brettwooldridge/HikariCP) + # for connection pooling, its configuration can be changed here: connection-pool-settings: - # The maximum number of connections to keep open with the database (def=8) - maximum-pool-size: 8 - # The minimum number of connections to keep open with the database (def=8) + # (*) From HikariCP docs: + # This property controls the minimum number of idle connections that HikariCP tries to maintain + # in the pool. If the idle connections dip below this value and total connections in the pool are + # less than maximum-pool-size, HikariCP will make the best effort to add additional connections + # quickly and efficiently. However, for maximum performance and responsiveness to spike demands, + # we recommend not setting this value and instead allowing HikariCP to act as a fixed size + # connection pool. Default: same as maximum-pool-size + # (*) Quests note: + # The default value has been decreased to 8. minimum-idle: 8 - # The maximum time (in milliseconds) to keep a single connection open (def=1800000 - 30 min) - maximum-lifetime: 1800000 - # The time (in milliseconds) the plugin will wait for a response by the database (def=500) + # (*) From HikariCP docs: + # This property controls the maximum size that the pool is allowed to reach, including both idle and + # in-use connections. Basically this value will determine the maximum number of actual connections to + # the database backend. A reasonable value for this is best determined by your execution environment. + # When the pool reaches this size, and no idle connections are available, calls to getConnection() will + # block for up to connection-timeout milliseconds before timing out. Please read about pool sizing + # (https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing). Default: 10 + # (*) Quests note: + # The default value has been decreased to 8. + maximum-pool-size: 8 + # (*) From HikariCP docs: + # This property controls the maximum number of milliseconds that a client (that's you) will wait for + # a connection from the pool. If this time is exceeded without a connection becoming available, a SQLException + # will be thrown. Lowest acceptable connection timeout is 250 ms. Default: 30000 (30 seconds) + # (*) Quests note: + # The default value has been decreased to 5000 as setting it to 30000 seems a bit too excessive. connection-timeout: 5000 + # (*) From HikariCP docs: + # This property controls the maximum amount of time that a connection is allowed to sit idle in the pool. This + # setting only applies when minimum-idle is defined to be less than maximum-pool-size. Idle connections will not + # be retired once the pool reaches minimum-idle connections. Whether a connection is retired as idle or not is + # subject to a maximum variation of +30 seconds, and average variation of +15 seconds. A connection will never + # be retired as idle before this timeout. A value of 0 means that idle connections are never removed from the + # pool. The minimum allowed value is 10000ms (10 seconds). Default: 600000 (10 minutes) + idle-timeout: 600000 + # (*) From HikariCP docs: + # This property controls how frequently HikariCP will attempt to keep a connection alive, in order to + # prevent it from being timed out by the database or network infrastructure. This value must be less + # than the maximum-lifetime value. A "keepalive" will only occur on an idle connection. When the time + # arrives for a "keepalive" against a given connection, that connection will be removed from the pool, + # "pinged", and then returned to the pool. The 'ping' is one of either: invocation of the JDBC4 isValid() + # method, or execution of the connectionTestQuery. Typically, the duration out-of-the-pool should be + # measured in single digit milliseconds or even sub-millisecond, and therefore should have little or + # no noticeable performance impact. The minimum allowed value is 30000ms (30 seconds), but a value in + # the range of minutes is most desirable. Default: 0 (disabled) + keepalive-time: 0 + # (*) From HikariCP docs: + # This property controls the maximum lifetime of a connection in the pool. An in-use connection will never + # be retired, only when it is closed will it then be removed. On a connection-by-connection basis, minor + # negative attenuation is applied to avoid mass-extinction in the pool. We strongly recommend setting this + # value, and it should be several seconds shorter than any database or infrastructure imposed connection + # time limit. A value of 0 indicates no maximum lifetime (infinite lifetime), subject of course to the + # idle-timeout setting. The minimum allowed value is 30000ms (30 seconds). Default: 1800000 (30 minutes) + maximum-lifetime: 1800000 + # Additional data source properties to be used by the HikariCP connection pool. + # All available properties can be found in the HikariCP docs. + data-source-properties: {} # The prefix each table will use table-prefix: "quests_" -- cgit v1.2.3-70-g09d2