aboutsummaryrefslogtreecommitdiffstats
path: root/bukkit/src/main/java
diff options
context:
space:
mode:
authorKrakenied <krakenied1@gmail.com>2024-08-18 09:03:51 +0200
committerKrakenied <46192742+Krakenied@users.noreply.github.com>2025-05-13 20:34:15 +0200
commit4b34f05454f1349e1747fbf3928c898dd56ae8cd (patch)
tree861d228e1bed0c90b2426e41e7b646a677676209 /bukkit/src/main/java
parentd4b394bc06905510f4557178a148df71524bedc4 (diff)
Storage rework
Diffstat (limited to 'bukkit/src/main/java')
-rw-r--r--bukkit/src/main/java/com/leonardobishop/quests/bukkit/BukkitQuestsPlugin.java8
-rw-r--r--bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminMigrateCommandHandler.java15
-rw-r--r--bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataCompleteCommandHandler.java2
-rw-r--r--bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataFullresetCommandHandler.java2
-rw-r--r--bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataRandomCommandHandler.java2
-rw-r--r--bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataResetCommandHandler.java2
-rw-r--r--bukkit/src/main/java/com/leonardobishop/quests/bukkit/command/AdminModdataStartCommandHandler.java2
-rw-r--r--bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/ModernMySQLStorageProvider.java597
-rw-r--r--bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/ModernYAMLStorageProvider.java284
-rw-r--r--bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/MySqlStorageProvider.java471
-rw-r--r--bukkit/src/main/java/com/leonardobishop/quests/bukkit/storage/YamlStorageProvider.java212
-rw-r--r--bukkit/src/main/java/com/leonardobishop/quests/bukkit/util/CommandUtils.java27
12 files changed, 914 insertions, 710 deletions
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<QuestProgressFile> files = fromProvider.loadAllProgressFiles();
+ List<QPlayerData> 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<String, Object> 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<String, String> 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<String, Object> 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<String> 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<String, QuestProgress> 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<QuestProgress> 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<QPlayerData> loadAllPlayerData() {
+ if (this.fault) {
+ return Collections.emptyList();
+ }
+
+ final List<UUID> 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<QPlayerData> 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<String, String> 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<Integer, String> 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<String, String> 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<UUID, ReentrantLock> 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<String> 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<String> 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<QPlayerData> loadAllPlayerData() {
+ final List<QPlayerData> 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<Path> {
+
+ private static final String FILE_EXTENSION = ".yml";
+
+ private final ModernYAMLStorageProvider provider;
+ private final List<QPlayerData> allPlayerData;
+
+ public PlayerDataVisitor(final @NotNull ModernYAMLStorageProvider provider, final @NotNull List<QPlayerData> 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<String, String> 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<String, Quest> 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<String, QuestProgress> 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<QuestProgress> 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<QuestProgressFile> loadAllProgressFiles() {
- if (fault) return Collections.emptyList();
-
- Set<UUID> 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<QuestProgressFile> files = new ArrayList<>();
- for (UUID uuid : uuids) {
- QuestProgressFile file = loadProgressFile(uuid);
- if (file != null) {
- files.add(file);
- }
- }
-
- return files;
- }
-
- @Override
- public void saveAllProgressFiles(List<QuestProgressFile> 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<Integer, String> 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<UUID, ReentrantLock> 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<String, Quest> 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<QuestProgress> 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<QuestProgressFile> loadAllProgressFiles() {
- List<QuestProgressFile> files = new ArrayList<>();
-
- File directory = new File(plugin.getDataFolder() + File.separator + "playerdata");
- FileVisitor<Path> fileVisitor = new SimpleFileVisitor<Path>() {
- @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<QuestProgressFile> 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());
}
- });
- });
- }
+ }));
}
-
}