aboutsummaryrefslogtreecommitdiffstats
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
parentd4b394bc06905510f4557178a148df71524bedc4 (diff)
Storage rework
-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
-rw-r--r--bukkit/src/main/resources/resources/bukkit/config.yml64
-rw-r--r--common/src/main/java/com/leonardobishop/quests/common/player/QPlayer.java118
-rw-r--r--common/src/main/java/com/leonardobishop/quests/common/player/QPlayerData.java44
-rw-r--r--common/src/main/java/com/leonardobishop/quests/common/player/QPlayerManager.java143
-rw-r--r--common/src/main/java/com/leonardobishop/quests/common/player/questprogressfile/QuestProgress.java344
-rw-r--r--common/src/main/java/com/leonardobishop/quests/common/player/questprogressfile/QuestProgressFile.java414
-rw-r--r--common/src/main/java/com/leonardobishop/quests/common/player/questprogressfile/TaskProgress.java151
-rw-r--r--common/src/main/java/com/leonardobishop/quests/common/storage/StorageProvider.java82
20 files changed, 1856 insertions, 1128 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());
}
- });
- });
- }
+ }));
}
-
}
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_"
diff --git a/common/src/main/java/com/leonardobishop/quests/common/player/QPlayer.java b/common/src/main/java/com/leonardobishop/quests/common/player/QPlayer.java
index c3e38e67..bcfafdd0 100644
--- a/common/src/main/java/com/leonardobishop/quests/common/player/QPlayer.java
+++ b/common/src/main/java/com/leonardobishop/quests/common/player/QPlayer.java
@@ -18,29 +18,52 @@ import java.util.UUID;
/**
* Represents a player.
*/
-public class QPlayer {
+public final class QPlayer {
private final Quests plugin;
- private final UUID uuid;
- private final QPlayerPreferences playerPreferences;
- private final QuestProgressFile questProgressFile;
+ private final QPlayerData playerData;
private QuestController questController;
- public QPlayer(Quests plugin, UUID uuid, QPlayerPreferences playerPreferences, QuestProgressFile questProgressFile, QuestController questController) {
+ public QPlayer(final @NotNull Quests plugin, final @NotNull QPlayerData playerData, final @NotNull QuestController questController) {
this.plugin = plugin;
- this.uuid = uuid;
- this.playerPreferences = playerPreferences;
- this.questProgressFile = questProgressFile;
+ this.playerData = playerData;
this.questController = questController;
}
/**
+ * Get this players associated {@link QPlayerData}
+ *
+ * @return the players data
+ */
+ public @NotNull QPlayerData getPlayerData() {
+ return this.playerData;
+ }
+
+ /**
* Get the player UUID associated with this quest player. The player may not be online.
*
* @return uuid
*/
public @NotNull UUID getPlayerUUID() {
- return this.uuid;
+ return this.playerData.playerUUID();
+ }
+
+ /**
+ * Get this players associated {@link QPlayerPreferences}
+ *
+ * @return the players preferences
+ */
+ public @NotNull QPlayerPreferences getPlayerPreferences() {
+ return this.playerData.playerPreferences();
+ }
+
+ /**
+ * Get this players associated {@link QuestProgressFile}
+ *
+ * @return the quest progress file
+ */
+ public @NotNull QuestProgressFile getQuestProgressFile() {
+ return this.playerData.questProgressFile();
}
/**
@@ -50,31 +73,32 @@ public class QPlayer {
* @param quest the quest to complete
* @return true (always)
*/
- public boolean completeQuest(@NotNull Quest quest) {
+ @SuppressWarnings("UnusedReturnValue")
+ public boolean completeQuest(final @NotNull Quest quest) {
Objects.requireNonNull(quest, "quest cannot be null");
- return questController.completeQuestForPlayer(this, quest);
+ return this.questController.completeQuestForPlayer(this, quest);
}
/**
* Attempt to track a quest for the player. This will also play all effects (such as titles, messages etc.)
- **
+ *
* @param quest the quest to track
*/
- public void trackQuest(@Nullable Quest quest) {
- questController.trackQuestForPlayer(this, quest);
+ public void trackQuest(final @Nullable Quest quest) {
+ this.questController.trackQuestForPlayer(this, quest);
}
/**
- * Gets whether or not the player has started a specific quest.
+ * Gets whether the player has started a specific quest.
*
* @param quest the quest to test for
* @return true if the quest is started or quest autostart is enabled and the quest is ready to start, false otherwise
*/
- public boolean hasStartedQuest(@NotNull Quest quest) {
+ public boolean hasStartedQuest(final @NotNull Quest quest) {
Objects.requireNonNull(quest, "quest cannot be null");
- return questController.hasPlayerStartedQuest(this, quest);
+ return this.questController.hasPlayerStartedQuest(this, quest);
}
/**
@@ -129,17 +153,16 @@ public class QPlayer {
/**
* Attempt to start a quest for the player. This will also play all effects (such as titles, messages etc.)
- *
* Warning: will fail if the player is not online.
*
* @param quest the quest to start
* @return the quest start result -- {@code QuestStartResult.QUEST_SUCCESS} indicates success
*/
// TODO PlaceholderAPI support
- public @NotNull QuestStartResult startQuest(@NotNull Quest quest) {
+ public @NotNull QuestStartResult startQuest(final @NotNull Quest quest) {
Objects.requireNonNull(quest, "quest cannot be null");
- return questController.startQuestForPlayer(this, quest);
+ return this.questController.startQuestForPlayer(this, quest);
}
/**
@@ -148,10 +171,10 @@ public class QPlayer {
* @param quest the quest to start
* @return true if the quest was cancelled, false otherwise
*/
- public boolean cancelQuest(@NotNull Quest quest) {
+ public boolean cancelQuest(final @NotNull Quest quest) {
Objects.requireNonNull(quest, "quest cannot be null");
- return questController.cancelQuestForPlayer(this, quest);
+ return this.questController.cancelQuestForPlayer(this, quest);
}
/**
@@ -160,52 +183,34 @@ public class QPlayer {
* @param quest the quest to start
* @return true if the quest was expired, false otherwise
*/
- public boolean expireQuest(@NotNull Quest quest) {
+ @SuppressWarnings("UnusedReturnValue")
+ public boolean expireQuest(final @NotNull Quest quest) {
Objects.requireNonNull(quest, "quest cannot be null");
- return questController.expireQuestForPlayer(this, quest);
+ return this.questController.expireQuestForPlayer(this, quest);
}
/**
* Check if the player can start a quest.
- *
* Warning: will fail if the player is not online.
*
* @param quest the quest to check
* @return the quest start result
*/
- public @NotNull QuestStartResult canStartQuest(@NotNull Quest quest) {
+ public @NotNull QuestStartResult canStartQuest(final @NotNull Quest quest) {
Objects.requireNonNull(quest, "quest cannot be null");
- return questController.canPlayerStartQuest(this, quest);
+ return this.questController.canPlayerStartQuest(this, quest);
}
/**
- * Get this players associated {@link QuestProgressFile}
- *
- * @return the quest progress file
- */
- public @NotNull QuestProgressFile getQuestProgressFile() {
- return questProgressFile;
- }
-
- /**
- * Get this players associated {@link QPlayerPreferences}
- *
- * @return the players preferences
- */
- public @NotNull QPlayerPreferences getPlayerPreferences() {
- return playerPreferences;
- }
-
- /**
- * Get this players associated {@link QuestController}, usually the servers active quest controller
+ * Get player's associated {@link QuestController}. It's usually the server's active quest controller.
*
* @see QPlayerManager#getActiveQuestController()
* @return the quest controller for this player
*/
public @NotNull QuestController getQuestController() {
- return questController;
+ return this.questController;
}
/**
@@ -213,21 +218,24 @@ public class QPlayer {
*
* @param questController new quest controller
*/
- public void setQuestController(@NotNull QuestController questController) {
+ public void setQuestController(final @NotNull QuestController questController) {
Objects.requireNonNull(questController, "questController cannot be null");
this.questController = questController;
}
- @Override //Used by java GC
- public boolean equals(Object o) {
- if (!(o instanceof QPlayer)) return false;
- QPlayer qPlayer = (QPlayer) o;
- return this.uuid == qPlayer.getPlayerUUID();
+ @Override
+ public boolean equals(final @Nullable Object o) {
+ if (o instanceof final QPlayer qPlayer) {
+ return this.getPlayerUUID() == qPlayer.getPlayerUUID();
+ } else {
+ return false;
+ }
}
- @Override //Used by java GC
+ @Override
public int hashCode() {
- return uuid.hashCode() * 73; //uuid hash * prime number
+ // uuid hash * prime number
+ return this.getPlayerUUID().hashCode() * 73;
}
}
diff --git a/common/src/main/java/com/leonardobishop/quests/common/player/QPlayerData.java b/common/src/main/java/com/leonardobishop/quests/common/player/QPlayerData.java
new file mode 100644
index 00000000..cdabd385
--- /dev/null
+++ b/common/src/main/java/com/leonardobishop/quests/common/player/QPlayerData.java
@@ -0,0 +1,44 @@
+package com.leonardobishop.quests.common.player;
+
+import com.leonardobishop.quests.common.player.questprogressfile.QuestProgressFile;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+import java.util.UUID;
+
+public final class QPlayerData {
+
+ private final UUID playerUUID;
+ private final QPlayerPreferences playerPreferences;
+ private final QuestProgressFile questProgressFile;
+
+ public QPlayerData(final @NotNull UUID playerUUID, final @NotNull QPlayerPreferences playerPreferences, final @NotNull QuestProgressFile questProgressFile) {
+ this.playerUUID = Objects.requireNonNull(playerUUID, "playerUUID cannot be null");
+ this.playerPreferences = Objects.requireNonNull(playerPreferences, "playerPreferences cannot be null");
+ this.questProgressFile = Objects.requireNonNull(questProgressFile, "questProgressFile cannot be null");
+ }
+
+ public QPlayerData(final @NotNull QPlayerData playerData) {
+ Objects.requireNonNull(playerData, "playerData cannot be null");
+
+ this.playerUUID = playerData.playerUUID;
+ this.playerPreferences = playerData.playerPreferences;
+ this.questProgressFile = new QuestProgressFile(playerData.questProgressFile);
+ }
+
+ public @NotNull UUID playerUUID() {
+ return this.playerUUID;
+ }
+
+ public @NotNull QPlayerPreferences playerPreferences() {
+ return this.playerPreferences;
+ }
+
+ public @NotNull QuestProgressFile questProgressFile() {
+ return this.questProgressFile;
+ }
+
+ public void setModified(final boolean modified) {
+ this.questProgressFile.setModified(modified);
+ }
+}
diff --git a/common/src/main/java/com/leonardobishop/quests/common/player/QPlayerManager.java b/common/src/main/java/com/leonardobishop/quests/common/player/QPlayerManager.java
index 1475c44a..4c3704de 100644
--- a/common/src/main/java/com/leonardobishop/quests/common/player/QPlayerManager.java
+++ b/common/src/main/java/com/leonardobishop/quests/common/player/QPlayerManager.java
@@ -6,6 +6,7 @@ import com.leonardobishop.quests.common.questcontroller.QuestController;
import com.leonardobishop.quests.common.storage.StorageProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.UnmodifiableView;
import java.util.Collection;
import java.util.Collections;
@@ -19,17 +20,18 @@ import java.util.concurrent.ConcurrentHashMap;
* The QPlayerManager is responsible for keeping a reference to all players on the server and is used to
* obtain an instance of a player, load new players and save current players.
*/
-public class QPlayerManager {
+public final class QPlayerManager {
- private final Map<UUID, QPlayer> qPlayers = new ConcurrentHashMap<>();
private final Quests plugin;
private final StorageProvider storageProvider;
+ private final Map<UUID, QPlayer> qPlayerMap;
private QuestController activeQuestController;
- public QPlayerManager(Quests plugin, StorageProvider storageProvider, QuestController questController) {
- this.plugin = plugin;
- this.storageProvider = storageProvider;
- this.activeQuestController = questController;
+ public QPlayerManager(final @NotNull Quests plugin, final @NotNull StorageProvider storageProvider, final @NotNull QuestController questController) {
+ this.plugin = Objects.requireNonNull(plugin, "plugin cannot be null");
+ this.storageProvider = Objects.requireNonNull(storageProvider, "storageProvider cannot be null");
+ this.activeQuestController = Objects.requireNonNull(questController, "questController cannot be null");
+ this.qPlayerMap = new ConcurrentHashMap<>();
}
/**
@@ -38,7 +40,7 @@ public class QPlayerManager {
* @param uuid the uuid
* @return {@link QPlayer} if they are loaded, otherwise null
*/
- public @Nullable QPlayer getPlayer(@NotNull UUID uuid) {
+ public @Nullable QPlayer getPlayer(final @NotNull UUID uuid) {
Objects.requireNonNull(uuid, "uuid cannot be null");
// QPlayer qPlayer = qPlayers.get(uuid);
@@ -48,7 +50,7 @@ public class QPlayerManager {
// Thread.dumpStack();
// }
// }
- return qPlayers.get(uuid);
+ return this.qPlayerMap.get(uuid);
}
/**
@@ -56,12 +58,12 @@ public class QPlayerManager {
*
* @param uuid the uuid of the player
*/
- public void removePlayer(@NotNull UUID uuid) {
+ public void removePlayer(final @NotNull UUID uuid) {
Objects.requireNonNull(uuid, "uuid cannot be null");
- plugin.getQuestsLogger().debug("Unloading and saving player " + uuid + "...");
- CompletableFuture<Void> future = savePlayer(uuid);
- future.thenAccept((v) -> qPlayers.remove(uuid));
+ this.plugin.getQuestsLogger().debug("Unloading and saving player " + uuid + "...");
+ final CompletableFuture<Void> future = this.savePlayer(uuid);
+ future.thenAccept(unused -> this.qPlayerMap.remove(uuid));
}
/**
@@ -71,32 +73,30 @@ public class QPlayerManager {
* @param uuid the uuid of the player
* @return completable future
*/
- public CompletableFuture<Void> savePlayer(@NotNull UUID uuid) {
+ public @NotNull CompletableFuture<Void> savePlayer(final @NotNull UUID uuid) {
Objects.requireNonNull(uuid, "uuid cannot be null");
- QPlayer qPlayer = getPlayer(uuid);
- if (qPlayer == null) return CompletableFuture.completedFuture(null);
- return savePlayer(uuid, qPlayer.getQuestProgressFile());
+ final QPlayer qPlayer = this.getPlayer(uuid);
+ if (qPlayer == null) {
+ return CompletableFuture.completedFuture(null);
+ }
+
+ return this.savePlayer(qPlayer.getPlayerData());
}
/**
* Schedules a save for the player with a specified {@link QuestProgressFile}. The modified status of the
* specified progress file will be reset.
- *
- * @param uuid the uuid of the player
- * @param originalProgressFile the quest progress file to associate with and save
- * @return completable future
*/
- public CompletableFuture<Void> savePlayer(@NotNull UUID uuid, @NotNull QuestProgressFile originalProgressFile) {
- Objects.requireNonNull(uuid, "uuid cannot be null");
- Objects.requireNonNull(originalProgressFile, "originalProgressFile cannot be null");
+ public @NotNull CompletableFuture<Void> savePlayer(final @NotNull QPlayerData playerData) {
+ Objects.requireNonNull(playerData, "playerData cannot be null");
- CompletableFuture<Void> future = new CompletableFuture<>();
+ final CompletableFuture<Void> future = new CompletableFuture<>();
+ final QPlayerData clonedPlayerData = new QPlayerData(playerData);
+ playerData.setModified(false);
- QuestProgressFile clonedProgressFile = new QuestProgressFile(originalProgressFile);
- originalProgressFile.resetModified();
- plugin.getScheduler().doAsync(() -> {
- save(uuid, clonedProgressFile);
+ this.plugin.getScheduler().doAsync(() -> {
+ this.save(clonedPlayerData);
future.complete(null);
});
@@ -109,34 +109,35 @@ public class QPlayerManager {
*
* @param uuid the uuid of the player
*/
- public void savePlayerSync(@NotNull UUID uuid) {
+ public void savePlayerSync(final @NotNull UUID uuid) {
Objects.requireNonNull(uuid, "uuid cannot be null");
- QPlayer qPlayer = getPlayer(uuid);
- if (qPlayer == null) return;
- savePlayerSync(uuid, qPlayer.getQuestProgressFile());
+ final QPlayer qPlayer = this.getPlayer(uuid);
+ if (qPlayer == null) {
+ return;
+ }
+
+ this.savePlayerSync(qPlayer.getPlayerData());
}
/**
* Immediately saves the player with a specified {@link QuestProgressFile}, on the same thread. The modified status
* of the specified progress file is not changed.
- *
- * @param uuid the uuid of the player
- * @param questProgressFile the quest progress file to associate with and save
*/
- public void savePlayerSync(@NotNull UUID uuid, @NotNull QuestProgressFile questProgressFile) {
- save(uuid, questProgressFile);
+ public void savePlayerSync(final @NotNull QPlayerData playerData) {
+ this.save(playerData);
}
- private void save(@NotNull UUID uuid, @NotNull QuestProgressFile questProgressFile) {
- Objects.requireNonNull(uuid, "uuid cannot be null");
- Objects.requireNonNull(questProgressFile, "questProgressFile cannot be null");
+ private void save(@NotNull QPlayerData playerData) {
+ Objects.requireNonNull(playerData, "playerData cannot be null");
+
+ final String uuidString = playerData.playerUUID().toString();
+ this.plugin.getQuestsLogger().debug("Saving player " + uuidString + "...");
- plugin.getQuestsLogger().debug("Saving player " + uuid + "...");
- if (storageProvider.saveProgressFile(uuid, questProgressFile)) {
- plugin.getQuestsLogger().debug("Quest progress file saved for player " + uuid + ".");
+ if (this.storageProvider.savePlayerData(playerData)) {
+ this.plugin.getQuestsLogger().debug("Quest progress file saved for player " + uuidString + ".");
} else {
- plugin.getQuestsLogger().severe("Failed to save player " + uuid + "!");
+ this.plugin.getQuestsLogger().severe("Failed to save player " + uuidString + "!");
}
}
@@ -145,20 +146,21 @@ public class QPlayerManager {
*
* @param uuid the uuid of the player
*/
- public void dropPlayer(@NotNull UUID uuid) {
+ public void dropPlayer(final @NotNull UUID uuid) {
Objects.requireNonNull(uuid, "uuid cannot be null");
- plugin.getQuestsLogger().debug("Dropping player " + uuid + ".");
- qPlayers.remove(uuid);
+ this.plugin.getQuestsLogger().debug("Dropping player " + uuid + ".");
+ this.qPlayerMap.remove(uuid);
}
/**
* Gets all QPlayers loaded on the server
*
- * @return immutable map of quest players
+ * @return immutable collection of quest players
*/
- public Collection<QPlayer> getQPlayers() {
- return Collections.unmodifiableCollection(qPlayers.values());
+ @UnmodifiableView
+ public @NotNull Collection<QPlayer> getQPlayers() {
+ return Collections.unmodifiableCollection(this.qPlayerMap.values());
}
/**
@@ -168,20 +170,26 @@ public class QPlayerManager {
* @param uuid the uuid of the player
* @return completable future with the loaded player, or null if there was an error
*/
- public CompletableFuture<QPlayer> loadPlayer(UUID uuid) {
- plugin.getQuestsLogger().debug("Loading player " + uuid + "...");
+ public @NotNull CompletableFuture<QPlayer> loadPlayer(final @NotNull UUID uuid) {
+ Objects.requireNonNull(uuid, "uuid cannot be null");
+
+ final String uuidString = uuid.toString();
+ this.plugin.getQuestsLogger().debug("Loading player " + uuidString + "...");
+ final CompletableFuture<QPlayer> future = new CompletableFuture<>();
- CompletableFuture<QPlayer> future = new CompletableFuture<>();
- plugin.getScheduler().doAsync(() -> {
- QuestProgressFile questProgressFile = storageProvider.loadProgressFile(uuid);
- if (questProgressFile == null) {
- plugin.getQuestsLogger().debug("A problem occurred trying loading player " + uuid + "; quest progress file is null.");
+ this.plugin.getScheduler().doAsync(() -> {
+ final QPlayerData playerData = this.storageProvider.loadPlayerData(uuid);
+
+ if (playerData == null) {
+ this.plugin.getQuestsLogger().debug("A problem occurred trying loading player " + uuidString + "; quest progress file is null.");
future.complete(null);
return;
}
- QPlayer qPlayer = new QPlayer(plugin, uuid, new QPlayerPreferences(null), questProgressFile, activeQuestController);
- qPlayers.computeIfAbsent(uuid, s -> qPlayer);
- plugin.getQuestsLogger().debug("Quest progress file loaded for player " + uuid + ".");
+
+ final QPlayer qPlayer = new QPlayer(this.plugin, playerData, this.activeQuestController);
+ this.qPlayerMap.putIfAbsent(uuid, qPlayer);
+
+ this.plugin.getQuestsLogger().debug("Quest progress file loaded for player " + uuidString + ".");
future.complete(qPlayer);
});
@@ -193,17 +201,18 @@ public class QPlayerManager {
*
* @return {@link StorageProvider}
*/
- public StorageProvider getStorageProvider() {
- return storageProvider;
+ public @NotNull StorageProvider getStorageProvider() {
+ return this.storageProvider;
}
- public QuestController getActiveQuestController() {
- return activeQuestController;
+ public @NotNull QuestController getActiveQuestController() {
+ return this.activeQuestController;
}
- public void setActiveQuestController(QuestController activeQuestController) {
- this.activeQuestController = activeQuestController;
- for (QPlayer qPlayer : qPlayers.values()) {
+ public void setActiveQuestController(final @NotNull QuestController activeQuestController) {
+ this.activeQuestController = Objects.requireNonNull(activeQuestController, "activeQuestController cannot be null");
+
+ for (final QPlayer qPlayer : this.qPlayerMap.values()) {
qPlayer.setQuestController(activeQuestController);
}
}
diff --git a/common/src/main/java/com/leonardobishop/quests/common/player/questprogressfile/QuestProgress.java b/common/src/main/java/com/leonardobishop/quests/common/player/questprogressfile/QuestProgress.java
index 8e51d769..d8506b83 100644
--- a/common/src/main/java/com/leonardobishop/quests/common/player/questprogressfile/QuestProgress.java
+++ b/common/src/main/java/com/leonardobishop/quests/common/player/questprogressfile/QuestProgress.java
@@ -1,19 +1,23 @@
package com.leonardobishop.quests.common.player.questprogressfile;
import com.leonardobishop.quests.common.plugin.Quests;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
+import java.util.Set;
import java.util.UUID;
-public class QuestProgress {
+public final class QuestProgress {
private final Quests plugin;
-
- private final Map<String, TaskProgress> taskProgress = new HashMap<>();
- private final String questid;
- private final UUID player;
+ private final String questId;
+ private final UUID playerUUID;
+ private final Map<String, TaskProgress> taskProgressMap;
private boolean started;
private long startedDate;
@@ -22,29 +26,66 @@ public class QuestProgress {
private long completionDate;
private boolean modified;
- public QuestProgress(Quests plugin, String questid, boolean completed, boolean completedBefore, long completionDate, UUID player, boolean started, long startedDate) {
+ /**
+ * Constructs a QuestProgress.
+ *
+ * @param plugin the plugin instance
+ * @param questId the associated quest ID
+ * @param playerUUID the associated player UUID
+ * @param started whether the quest is started
+ * @param startedDate the date of the last quest start
+ * @param completed whether the quest is completed
+ * @param completedBefore whether the quest has been completed before
+ * @param completionDate the date of the last quest completion
+ * @param modified whether the object has been modified and needs to be saved
+ */
+ public QuestProgress(final @NotNull Quests plugin, final @NotNull String questId, final @NotNull UUID playerUUID, final boolean started, final long startedDate, final boolean completed, final boolean completedBefore, final long completionDate, final boolean modified) {
this.plugin = plugin;
- this.questid = questid;
+ this.questId = questId;
+ this.playerUUID = playerUUID;
+ this.taskProgressMap = new HashMap<>();
+ this.started = started;
+ this.startedDate = startedDate;
this.completed = completed;
this.completedBefore = completedBefore;
this.completionDate = completionDate;
- this.player = player;
- this.started = started;
- this.startedDate = startedDate;
+ this.modified = modified;
}
- public QuestProgress(Quests plugin, String questid, boolean completed, boolean completedBefore, long completionDate, UUID player, boolean started, long startedDate, boolean modified) {
- this(plugin, questid, completed, completedBefore, completionDate, player, started, startedDate);
- this.modified = modified;
+ /**
+ * Constructs a QuestProgress with {@link QuestProgress#modified} set to {@code false}.
+ *
+ * @param plugin the plugin instance
+ * @param questId the associated quest ID
+ * @param playerUUID the associated player UUID
+ * @param started whether the quest is started
+ * @param startedDate the date of the last quest start
+ * @param completed whether the quest is completed
+ * @param completedBefore whether the quest has been completed before
+ * @param completionDate the date of the last quest completion
+ */
+ public QuestProgress(final @NotNull Quests plugin, final @NotNull String questId, final @NotNull UUID playerUUID, final boolean started, final long startedDate, final boolean completed, final boolean completedBefore, final long completionDate) {
+ this(plugin, questId, playerUUID, started, startedDate, completed, completedBefore, completionDate, false);
}
- public QuestProgress(QuestProgress questProgress) {
+ /**
+ * Constructs a data-only clone from a QuestProgress instance.
+ *
+ * @param questProgress the quest progress instance
+ */
+ @ApiStatus.Internal
+ public QuestProgress(final @NotNull QuestProgress questProgress) {
+ final Set<Map.Entry<String, TaskProgress>> progressEntries = questProgress.taskProgressMap.entrySet();
+
this.plugin = questProgress.plugin;
- for (Map.Entry<String, TaskProgress> progressEntry : questProgress.taskProgress.entrySet()) {
- taskProgress.put(progressEntry.getKey(), new TaskProgress(progressEntry.getValue()));
+ this.questId = questProgress.questId;
+ this.playerUUID = questProgress.playerUUID;
+ this.taskProgressMap = new HashMap<>(progressEntries.size());
+
+ for (final Map.Entry<String, TaskProgress> progressEntry : progressEntries) {
+ this.taskProgressMap.put(progressEntry.getKey(), new TaskProgress(progressEntry.getValue()));
}
- this.questid = questProgress.questid;
- this.player = questProgress.player;
+
this.started = questProgress.started;
this.startedDate = questProgress.startedDate;
this.completed = questProgress.completed;
@@ -53,120 +94,259 @@ public class QuestProgress {
this.modified = questProgress.modified;
}
- public String getQuestId() {
- return questid;
+ /**
+ * @return the associated quest ID
+ */
+ @Contract(pure = true)
+ public @NotNull String getQuestId() {
+ return this.questId;
}
- public boolean isCompleted() {
- return completed;
+ /**
+ * @return the associated player ID
+ * @see QuestProgress#getPlayerUUID()
+ */
+ @Deprecated(forRemoval = true)
+ @Contract(pure = true)
+ public @NotNull UUID getPlayer() {
+ return this.playerUUID;
}
- public void setCompleted(boolean completed) {
- this.completed = completed;
- this.modified = true;
+ /**
+ * @return the associated player ID
+ */
+ @Contract(pure = true)
+ public @NotNull UUID getPlayerUUID() {
+ return this.playerUUID;
+ }
+
+ /**
+ * @return mutable task progress map
+ */
+ @Contract(pure = true)
+ public @NotNull Map<String, TaskProgress> getTaskProgressMap() {
+ return this.taskProgressMap;
}
+ /**
+ * @return mutable task progress map values collection
+ * @see QuestProgress#getTaskProgress()
+ */
+ @Deprecated(forRemoval = true)
+ @Contract(pure = true)
+ public @NotNull Collection<TaskProgress> getTaskProgress() {
+ return this.taskProgressMap.values();
+ }
+
+ /**
+ * @return mutable task progress map values collection
+ */
+ @SuppressWarnings("unused")
+ @Contract(pure = true)
+ public @NotNull Collection<TaskProgress> getAllTaskProgress() {
+ return this.taskProgressMap.values();
+ }
+
+ /**
+ * Gets the {@link TaskProgress} for a specified task ID. Generates a new one if it does not exist.
+ *
+ * @param taskId the task ID to get the progress for
+ * @return {@link TaskProgress} or a blank generated one if the task does not exist
+ */
+ public @NotNull TaskProgress getTaskProgress(final @NotNull String taskId) {
+ final TaskProgress taskProgress = this.taskProgressMap.get(taskId);
+ if (taskProgress != null) {
+ return taskProgress;
+ }
+
+ final TaskProgress newTaskProgress = new TaskProgress(this, taskId, this.playerUUID, null, false, false);
+ this.addTaskProgress(newTaskProgress);
+ return newTaskProgress;
+ }
+
+ /**
+ * Gets the {@link TaskProgress} for a specified task ID. Returns null if it does not exist.
+ *
+ * @param taskId the task ID to get the progress for
+ * @return {@link TaskProgress} or null if the task does not exist
+ */
+ @SuppressWarnings("unused")
+ @Contract(pure = true)
+ public @Nullable TaskProgress getTaskProgressOrNull(final @NotNull String taskId) {
+ return this.taskProgressMap.get(taskId);
+ }
+
+ /**
+ * @param taskId the task ID to repair the progress for
+ */
+ @Deprecated(forRemoval = true)
+ @ApiStatus.Internal
+ public void repairTaskProgress(final @NotNull String taskId) {
+ final TaskProgress taskProgress = new TaskProgress(this, taskId, this.playerUUID, null, false, false);
+ this.addTaskProgress(taskProgress);
+ }
+
+ /**
+ * @param taskProgress the task progress to put into the task progress map
+ */
+ public void addTaskProgress(final @NotNull TaskProgress taskProgress) {
+ this.taskProgressMap.put(taskProgress.getTaskId(), taskProgress);
+ }
+
+ /**
+ * @return whether the quest is started
+ */
+ @Contract(pure = true)
public boolean isStarted() {
- return started;
+ return this.started;
}
- public void setStarted(boolean started) {
+ /**
+ * @param started whether the quest is started
+ */
+ public void setStarted(final boolean started) {
this.started = started;
this.modified = true;
}
+ /**
+ * @return the date of the last quest start
+ */
+ @Contract(pure = true)
public long getStartedDate() {
- return startedDate;
+ return this.startedDate;
}
- public void setStartedDate(long startedDate) {
+ /**
+ * @param startedDate the date of the last quest start
+ */
+ public void setStartedDate(final long startedDate) {
this.startedDate = startedDate;
this.modified = true;
}
- public long getCompletionDate() {
- return completionDate;
+ /**
+ * @return whether the quest is completed
+ */
+ @Contract(pure = true)
+ public boolean isCompleted() {
+ return this.completed;
}
- public void setCompletionDate(long completionDate) {
- this.completionDate = completionDate;
+ /**
+ * @param completed whether the quest is completed
+ */
+ public void setCompleted(final boolean completed) {
+ this.completed = completed;
this.modified = true;
}
- public UUID getPlayer() {
- return player;
- }
-
+ /**
+ * @return whether the quest has been completed before
+ */
+ @Contract(pure = true)
public boolean isCompletedBefore() {
- return completedBefore;
+ return this.completedBefore;
}
- public void setCompletedBefore(boolean completedBefore) {
+ /**
+ * @param completedBefore whether the quest has been completed before
+ */
+ public void setCompletedBefore(final boolean completedBefore) {
this.completedBefore = completedBefore;
this.modified = true;
}
- public void addTaskProgress(TaskProgress taskProgress) {
- this.taskProgress.put(taskProgress.getTaskId(), taskProgress);
+ /**
+ * @return the date of the last quest completion
+ */
+ @Contract(pure = true)
+ public long getCompletionDate() {
+ return this.completionDate;
}
- public Collection<TaskProgress> getTaskProgress() {
- return taskProgress.values();
+ /**
+ * @param completionDate the date of the last quest completion
+ */
+ public void setCompletionDate(final long completionDate) {
+ this.completionDate = completionDate;
+ this.modified = true;
}
- public Map<String, TaskProgress> getTaskProgressMap() {
- return taskProgress;
- }
+ /**
+ * @return whether the object has been modified and needs to be saved
+ */
+ @SuppressWarnings("unused")
+ @Contract(pure = true)
+ public boolean isModified() {
+ if (this.modified) {
+ return true;
+ }
- public TaskProgress getTaskProgress(String taskId) {
- TaskProgress tP = taskProgress.getOrDefault(taskId, null);
- if (tP == null) {
- repairTaskProgress(taskId);
- tP = taskProgress.getOrDefault(taskId, null);
+ for (final TaskProgress taskProgress : this.taskProgressMap.values()) {
+ if (taskProgress.isModified()) {
+ return true;
+ }
}
- return tP;
+
+ return false;
}
- public void repairTaskProgress(String taskid) {
- TaskProgress taskProgress = new TaskProgress(this, taskid, null, player, false, false);
- this.addTaskProgress(taskProgress);
+ /**
+ * It's equivalent to {@code QuestProgress#setModified(false)}.
+ *
+ * @see QuestProgress#setModified(boolean)
+ */
+ @Deprecated(forRemoval = true)
+ public void resetModified() {
+ this.setModified(false);
}
- public boolean isModified() {
- if (modified) return true;
- else {
- for (TaskProgress progress : this.taskProgress.values()) {
- if (progress.isModified()) return true;
- }
- return false;
+ /**
+ * @param modified whether the object has been modified and needs to be saved
+ */
+ public void setModified(final boolean modified) {
+ this.modified = modified;
+
+ for (final TaskProgress taskProgress : this.taskProgressMap.values()) {
+ taskProgress.setModified(modified);
}
}
+ /**
+ * Gets whether the object has non default values.
+ *
+ * <p>
+ * Fields checked are:<br>
+ * - {@link QuestProgress#started}<br>
+ * - {@link QuestProgress#startedDate}<br>
+ * - {@link QuestProgress#completed}<br>
+ * - {@link QuestProgress#completedBefore}<br>
+ * - {@link QuestProgress#completionDate}
+ * </p>
+ *
+ * @return whether the object has non default values
+ */
+ @Contract(pure = true)
public boolean hasNonDefaultValues() {
- if (this.started || this.startedDate != 0 || this.completed || this.completedBefore || this.completionDate != 0) return true;
- else {
- for (TaskProgress progress : this.taskProgress.values()) {
- if (progress.getProgress() != null || progress.isCompleted()) return true;
- }
- return false;
+ if (this.started || this.startedDate != 0L || this.completed || this.completedBefore || this.completionDate != 0L) {
+ return true;
}
- }
- public void queueForCompletionTest() {
- plugin.getQuestCompleter().queueSingular(this);
- }
-
- public void resetModified() {
- this.modified = false;
- for (TaskProgress progress : this.taskProgress.values()) {
- progress.resetModified();
+ for (final TaskProgress taskProgress : this.taskProgressMap.values()) {
+ if (taskProgress.getProgress() != null || taskProgress.isCompleted()) {
+ return true;
+ }
}
+
+ return false;
}
- public void setModified(boolean modified) {
- this.modified = modified;
- for (TaskProgress progress : this.taskProgress.values()) {
- progress.setModified(modified);
- }
+ /**
+ * Queues the {@link QuestProgress} instance for a completion test.
+ */
+ @SuppressWarnings("unused")
+ public void queueForCompletionTest() {
+ this.plugin.getQuestCompleter().queueSingular(this);
}
}
diff --git a/common/src/main/java/com/leonardobishop/quests/common/player/questprogressfile/QuestProgressFile.java b/common/src/main/java/com/leonardobishop/quests/common/player/questprogressfile/QuestProgressFile.java
index 0069eb12..7cadaa9a 100644
--- a/common/src/main/java/com/leonardobishop/quests/common/player/questprogressfile/QuestProgressFile.java
+++ b/common/src/main/java/com/leonardobishop/quests/common/player/questprogressfile/QuestProgressFile.java
@@ -4,62 +4,80 @@ import com.leonardobishop.quests.common.player.QPlayer;
import com.leonardobishop.quests.common.plugin.Quests;
import com.leonardobishop.quests.common.quest.Quest;
import com.leonardobishop.quests.common.quest.Task;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Represents underlying quest progress for a player.
*/
-public class QuestProgressFile {
+public final class QuestProgressFile {
- private final Map<String, QuestProgress> questProgress = new HashMap<>();
- private final UUID playerUUID;
private final Quests plugin;
+ private final UUID playerUUID;
+ private final Map<String, QuestProgress> questProgressMap;
- public QuestProgressFile(UUID playerUUID, Quests plugin) {
- this.playerUUID = playerUUID;
+ /**
+ * Constructs a QuestProgressFile.
+ *
+ * @param plugin the plugin instance
+ * @param playerUUID the associated player UUID
+ */
+ public QuestProgressFile(final @NotNull Quests plugin, final @NotNull UUID playerUUID) {
this.plugin = plugin;
+ this.playerUUID = playerUUID;
+ this.questProgressMap = new HashMap<>();
}
- public QuestProgressFile(QuestProgressFile questProgressFile) {
- for (Map.Entry<String, QuestProgress> progressEntry : questProgressFile.questProgress.entrySet()) {
- questProgress.put(progressEntry.getKey(), new QuestProgress(progressEntry.getValue()));
- }
- this.playerUUID = questProgressFile.playerUUID;
+ /**
+ * Constructs a data-only clone from a QuestProgressFile instance.
+ *
+ * @param questProgressFile the quest progress file instance
+ */
+ @ApiStatus.Internal
+ public QuestProgressFile(final @NotNull QuestProgressFile questProgressFile) {
+ final Set<Map.Entry<String, QuestProgress>> progressEntries = questProgressFile.questProgressMap.entrySet();
+
this.plugin = questProgressFile.plugin;
+ this.playerUUID = questProgressFile.playerUUID;
+ this.questProgressMap = new HashMap<>(progressEntries.size());
+
+ for (final Map.Entry<String, QuestProgress> progressEntry : progressEntries) {
+ this.questProgressMap.put(progressEntry.getKey(), new QuestProgress(progressEntry.getValue()));
+ }
}
- public void addQuestProgress(QuestProgress questProgress) {
- //TODO don't do here
-// if (Options.VERIFY_QUEST_EXISTS_ON_LOAD.getBooleanValue(true) && plugin.getQuestManager().getQuestById(questProgress.getQuestId()) == null) {
-// return;
-// }
- this.questProgress.put(questProgress.getQuestId(), questProgress);
+ /**
+ * @param questProgress the quest progress to put into the quest progress map
+ */
+ public void addQuestProgress(final @NotNull QuestProgress questProgress) {
+ // TODO don't do that here
+ //if (Options.VERIFY_QUEST_EXISTS_ON_LOAD.getBooleanValue(true) && plugin.getQuestManager().getQuestById(questProgress.getQuestId()) == null) {
+ // return;
+ //}
+ this.questProgressMap.put(questProgress.getQuestId(), questProgress);
}
/**
- * Gets all manually started quests.
- * Note: if quest autostart is enabled then this may produce unexpected results as quests are
- * not "started" by the player if autostart is true. Consider {@link QPlayer#hasStartedQuest(Quest)}
- * or {@link QPlayer#getEffectiveStartedQuests()} instead.
+ * Gets all manually started quests. If quest autostart is enabled then this may produce unexpected results as
+ * quests are not "started" by the player if autostart is true. Consider {@link QPlayer#hasStartedQuest(Quest)}
+ * or {@link QPlayer#getEffectiveStartedQuests()} usage instead.
*
* @return list of started quests
*/
- public List<Quest> getStartedQuests() {
- List<Quest> startedQuests = new ArrayList<>();
- for (QuestProgress questProgress : questProgress.values()) {
- Quest quest = plugin.getQuestManager().getQuestById(questProgress.getQuestId());
- if (quest != null && questProgress.isStarted()) {
- startedQuests.add(plugin.getQuestManager().getQuestById(questProgress.getQuestId()));
- }
- }
- return startedQuests;
+ @Contract(pure = true)
+ public @NotNull List<Quest> getStartedQuests() {
+ return this.getAllQuestsFromProgress(QuestsProgressFilter.STARTED);
}
/**
@@ -68,50 +86,86 @@ public class QuestProgressFile {
*
* @return {@code List<Quest>} all quests
*/
- public List<Quest> getAllQuestsFromProgress(QuestsProgressFilter filter) {
- List<Quest> questsProgress = new ArrayList<>();
- for (QuestProgress qProgress : questProgress.values()) {
- boolean condition = false;
- if (filter == QuestsProgressFilter.STARTED) {
- condition = qProgress.isStarted();
- } else if (filter == QuestsProgressFilter.COMPLETED_BEFORE) {
- condition = qProgress.isCompletedBefore();
- } else if (filter == QuestsProgressFilter.COMPLETED) {
- condition = qProgress.isCompleted();
- } else if (filter == QuestsProgressFilter.ALL) {
- condition = true;
+ @Contract(pure = true)
+ public @NotNull List<Quest> getAllQuestsFromProgress(final @NotNull QuestsProgressFilter filter) {
+ final List<Quest> quests = new ArrayList<>();
+
+ for (final QuestProgress questProgress : this.questProgressMap.values()) {
+ final boolean matches = filter.matches(questProgress);
+ if (!matches) {
+ continue;
}
- if (condition) {
- Quest quest = plugin.getQuestManager().getQuestById(qProgress.getQuestId());
- if (quest != null) {
- questsProgress.add(quest);
- }
+
+ final Quest quest = this.plugin.getQuestManager().getQuestById(questProgress.getQuestId());
+ if (quest == null) {
+ continue;
}
+
+ quests.add(quest);
}
- return questsProgress;
+
+ return quests;
}
public enum QuestsProgressFilter {
- ALL("all"),
- COMPLETED("completed"),
- COMPLETED_BEFORE("completedBefore"),
- STARTED("started");
+ ALL("all") {
+ @Override
+ @Contract(pure = true)
+ public boolean matches(final @NotNull QuestProgress questProgress) {
+ return true;
+ }
+ },
+ COMPLETED("completed") {
+ @Override
+ @Contract(pure = true)
+ public boolean matches(final @NotNull QuestProgress questProgress) {
+ return questProgress.isCompleted();
+ }
+ },
+ COMPLETED_BEFORE("completedBefore") {
+ @Override
+ @Contract(pure = true)
+ public boolean matches(final @NotNull QuestProgress questProgress) {
+ return questProgress.isCompletedBefore();
+ }
+ },
+ STARTED("started") {
+ @Override
+ @Contract(pure = true)
+ public boolean matches(final @NotNull QuestProgress questProgress) {
+ return questProgress.isStarted();
+ }
+ };
private final String legacy;
- QuestsProgressFilter(String legacy) {
+ QuestsProgressFilter(final @NotNull String legacy) {
this.legacy = legacy;
}
- public static QuestsProgressFilter fromLegacy(String filter) {
- for (QuestsProgressFilter filterEnum : QuestsProgressFilter.values()) {
- if (filterEnum.getLegacy().equals(filter)) return filterEnum;
- }
- return QuestsProgressFilter.ALL;
+ @SuppressWarnings("unused")
+ @Contract(pure = true)
+ public @NotNull String getLegacy() {
+ return this.legacy;
}
- public String getLegacy() {
- return legacy;
+ @Contract(pure = true)
+ public abstract boolean matches(final @NotNull QuestProgress questProgress);
+
+ // And some static things to improve legacy performance (is it even used?)
+
+ private static final QuestsProgressFilter[] FILTERS = QuestsProgressFilter.values();
+
+ private static final Map<String, QuestsProgressFilter> legacyToFilterMap = new HashMap<>(QuestsProgressFilter.FILTERS.length) {{
+ for (final QuestsProgressFilter questsProgressFilter : QuestsProgressFilter.FILTERS) {
+ this.put(questsProgressFilter.legacy, questsProgressFilter);
+ }
+ }};
+
+ @SuppressWarnings("unused")
+ @Contract(pure = true)
+ public static @NotNull QuestsProgressFilter fromLegacy(final @NotNull String legacy) {
+ return QuestsProgressFilter.legacyToFilterMap.getOrDefault(legacy, QuestsProgressFilter.ALL);
}
}
@@ -120,35 +174,50 @@ public class QuestProgressFile {
*
* @return {@code Collection<QuestProgress>} all quest progresses
*/
- public Collection<QuestProgress> getAllQuestProgress() {
- return questProgress.values();
+ @Contract(pure = true)
+ public @NotNull Collection<QuestProgress> getAllQuestProgress() {
+ return this.questProgressMap.values();
}
/**
- * Checks whether or not the player has {@link QuestProgress} for a specified quest
+ * Checks whether the player has {@link QuestProgress} for a specified quest
*
* @param quest the quest to check for
* @return true if they have quest progress
*/
- public boolean hasQuestProgress(Quest quest) {
- return questProgress.containsKey(quest.getId());
+ @Contract(pure = true)
+ public boolean hasQuestProgress(final @NotNull Quest quest) {
+ return this.questProgressMap.containsKey(quest.getId());
}
/**
* Gets the remaining cooldown before being able to start a specific quest.
*
* @param quest the quest to test for
- * @return 0 if no cooldown remaining or the cooldown is disabled, otherwise the cooldown in milliseconds
+ * @return 0 if no cooldown remaining, -1 if the cooldown is disabled or the quest is not completed,
+ * otherwise the cooldown in milliseconds
*/
- public long getCooldownFor(Quest quest) {
- QuestProgress questProgress = getQuestProgress(quest);
- if (quest.isCooldownEnabled() && questProgress.isCompleted()) {
- if (questProgress.getCompletionDate() > 0) {
- long date = questProgress.getCompletionDate();
- return (date + TimeUnit.MILLISECONDS.convert(quest.getCooldown(), TimeUnit.MINUTES)) - System.currentTimeMillis();
- }
+ @Contract(pure = true)
+ public long getCooldownFor(final @NotNull Quest quest) {
+ if (!quest.isCooldownEnabled()) {
+ return -1;
+ }
+
+ final QuestProgress questProgress = this.getQuestProgressOrNull(quest);
+ if (questProgress == null || !questProgress.isCompleted()) {
+ return -1;
+ }
+
+ final long completionDate = questProgress.getCompletionDate();
+ if (completionDate == 0) {
+ return -1;
}
- return 0;
+
+ final long currentTimeMillis = System.currentTimeMillis();
+ final long cooldownMillis = TimeUnit.MILLISECONDS.convert(quest.getCooldown(), TimeUnit.MINUTES);
+
+ // do the subtraction first to prevent overflow
+ return Math.max(0L, completionDate - currentTimeMillis + cooldownMillis);
}
/**
@@ -158,101 +227,134 @@ public class QuestProgressFile {
* @return 0 if no time remaining, -1 if the time limit is disabled or the quest is not started,
* otherwise the time left in milliseconds
*/
- public long getTimeRemainingFor(Quest quest) {
- QuestProgress questProgress = getQuestProgress(quest);
- if (quest.isTimeLimitEnabled() && questProgress.isStarted()) {
- return Math.max(
- questProgress.getStartedDate()
- + TimeUnit.MILLISECONDS.convert(quest.getTimeLimit(), TimeUnit.MINUTES)
- - System.currentTimeMillis()
- , 0);
+ @Contract(pure = true)
+ public long getTimeRemainingFor(final @NotNull Quest quest) {
+ if (!quest.isTimeLimitEnabled()) {
+ return -1;
+ }
+
+ final QuestProgress questProgress = this.getQuestProgressOrNull(quest);
+ if (questProgress == null || !questProgress.isStarted()) {
+ return -1;
+ }
+
+ final long startedDate = questProgress.getStartedDate();
+ if (startedDate == 0) {
+ return -1;
}
- return -1;
+
+ final long currentTimeMillis = System.currentTimeMillis();
+ final long timeLimitMillis = TimeUnit.MILLISECONDS.convert(quest.getTimeLimit(), TimeUnit.MINUTES);
+
+ // do the subtraction first to prevent overflow
+ return Math.max(0L, startedDate - currentTimeMillis + timeLimitMillis);
}
/**
- * Tests whether or not the player meets the requirements to start a specific quest.
+ * Tests whether the player meets the requirements to start a specific quest.
*
* @param quest the quest to test for
* @return true if they can start the quest
*/
- //TODO possibly move this
- public boolean hasMetRequirements(Quest quest) {
- for (String id : quest.getRequirements()) {
- Quest q = plugin.getQuestManager().getQuestById(id);
- if (q == null) {
- continue;
- }
- if (hasQuestProgress(q) && !getQuestProgress(q).isCompletedBefore()) {
+ // TODO possibly move this
+ @Contract(pure = true)
+ public boolean hasMetRequirements(final @NotNull Quest quest) {
+ for (final String requiredQuestId : quest.getRequirements()) {
+ final QuestProgress requiredQuestProgress = this.questProgressMap.get(requiredQuestId);
+ if (requiredQuestProgress == null || !requiredQuestProgress.isCompletedBefore()) {
+ // if we decide to change the method return type to states like "DOES_NOT_EXIST"
+ // or "COMPLETED_BEFORE" we will need to change the quest existance check order
return false;
- } else if (!hasQuestProgress(q)) {
+ }
+
+ final Quest requiredQuest = this.plugin.getQuestManager().getQuestById(requiredQuestId);
+ if (requiredQuest == null) {
+ // TODO not sure if we actually need this check however probably
+ // forcing the server owner to fix the quest options instead
+ // of just ignoring the fact it's broken is better?
return false;
}
}
+
return true;
}
/**
- * Get the {@link UUID} of the player this QuestProgressFile represents.
- *
- * @return UUID
+ * @return the associated player UUID
*/
- public UUID getPlayerUUID() {
- return playerUUID;
+ @Contract(pure = true)
+ public @NotNull UUID getPlayerUUID() {
+ return this.playerUUID;
}
/**
- * Get the {@link QuestProgress} for a specified {@link Quest}. Generates a new one if it does not exist.
+ * Gets the {@link QuestProgress} for a specified {@link Quest}. Generates a new one if it does not exist.
*
- * @param quest the quest to get progress for
+ * @param quest the quest to get the progress for
* @return {@link QuestProgress} or a blank generated one if the quest does not exist
*/
- public QuestProgress getQuestProgress(Quest quest) {
- QuestProgress qProgress = questProgress.get(quest.getId());
- return qProgress != null ? qProgress : generateBlankQuestProgress(quest);
+ public @NotNull QuestProgress getQuestProgress(final @NotNull Quest quest) {
+ final QuestProgress questProgress = this.getQuestProgressOrNull(quest);
+ return questProgress != null ? questProgress : this.generateBlankQuestProgress(quest);
}
/**
- * Tests whether or not the player has a specified {@link Quest} started.
+ * Gets the {@link QuestProgress} for a specified {@link Quest}. Returns null if it does not exist.
+ *
+ * @param quest the quest to get the progress for
+ * @return {@link QuestProgress} or null if the quest does not exist
+ */
+ @Contract(pure = true)
+ public @Nullable QuestProgress getQuestProgressOrNull(final @NotNull Quest quest) {
+ return this.questProgressMap.get(quest.getId());
+ }
+
+ /**
+ * Tests whether the player has a specified {@link Quest} started.
*
* @param quest the quest to check for
* @return true if player has the quest started
*/
- public boolean hasQuestStarted(Quest quest) {
- QuestProgress qProgress = questProgress.get(quest.getId());
- return qProgress != null && qProgress.isStarted();
+ @Contract(pure = true)
+ public boolean hasQuestStarted(final @NotNull Quest quest) {
+ final QuestProgress questProgress = this.getQuestProgressOrNull(quest);
+ return questProgress != null && questProgress.isStarted();
}
/**
- * Generate a new blank {@link QuestProgress} for a specified {@code quest}.
+ * Generate a new blank {@link QuestProgress} for a specified {@link Quest} with {@link QuestProgress#isModified()} set to {@code false}.
*
- * @param quest the quest to generate progress for
+ * @param quest the quest to generate the progress for
* @return the generated blank {@link QuestProgress}
*/
- public QuestProgress generateBlankQuestProgress(Quest quest) {
- return generateBlankQuestProgress(quest, false);
+ public @NotNull QuestProgress generateBlankQuestProgress(final @NotNull Quest quest) {
+ return this.generateBlankQuestProgress(quest, false);
}
/**
- * Generate a new blank {@link QuestProgress} for a specified {@code quest}.
+ * Generate a new blank {@link QuestProgress} for a specified {@link Quest}.
*
- * @param quest the quest to generate progress for
+ * @param quest the quest to generate the progress for
* @param modified the modified state of the quest
* @return the generated blank {@link QuestProgress}
*/
- public QuestProgress generateBlankQuestProgress(Quest quest, boolean modified) {
- QuestProgress questProgress = new QuestProgress(plugin, quest.getId(), false, false, 0, playerUUID, false, 0, modified);
- for (Task task : quest.getTasks()) {
- TaskProgress taskProgress = new TaskProgress(questProgress, task.getId(), null, playerUUID, false, modified);
+ public @NotNull QuestProgress generateBlankQuestProgress(final @NotNull Quest quest, final boolean modified) {
+ final QuestProgress questProgress = new QuestProgress(this.plugin, quest.getId(), this.playerUUID, false, 0L, false, false, 0L, modified);
+
+ for (final Task task : quest.getTasks()) {
+ final TaskProgress taskProgress = new TaskProgress(questProgress, task.getId(), this.playerUUID, null, false, modified);
questProgress.addTaskProgress(taskProgress);
}
- addQuestProgress(questProgress);
+ this.addQuestProgress(questProgress);
return questProgress;
}
+ /**
+ * Clears quest progress map.
+ */
public void clear() {
- questProgress.clear();
+ this.questProgressMap.clear();
}
/**
@@ -261,15 +363,17 @@ public class QuestProgressFile {
* set the modified flag in that case.
*/
public void reset() {
- for (QuestProgress questProgress : questProgress.values()) {
+ for (final QuestProgress questProgress : this.questProgressMap.values()) {
if (!questProgress.hasNonDefaultValues()) {
continue;
}
- Quest quest = plugin.getQuestManager().getQuestById(questProgress.getQuestId());
+
+ final Quest quest = this.plugin.getQuestManager().getQuestById(questProgress.getQuestId());
if (quest == null) {
continue;
}
- generateBlankQuestProgress(quest, true);
+
+ this.generateBlankQuestProgress(quest, true);
}
}
@@ -278,41 +382,61 @@ public class QuestProgressFile {
*/
@Deprecated
public void clean() {
- plugin.getQuestsLogger().debug("Cleaning file " + playerUUID + ".");
- if (!plugin.getTaskTypeManager().areRegistrationsOpen()) {
- ArrayList<String> invalidQuests = new ArrayList<>();
- for (String questId : this.questProgress.keySet()) {
- Quest q;
- if ((q = plugin.getQuestManager().getQuestById(questId)) == null) {
- invalidQuests.add(questId);
- } else {
- ArrayList<String> invalidTasks = new ArrayList<>();
- for (String taskId : this.questProgress.get(questId).getTaskProgressMap().keySet()) {
- if (q.getTaskById(taskId) == null) {
- invalidTasks.add(taskId);
- }
- }
- for (String taskId : invalidTasks) {
- this.questProgress.get(questId).getTaskProgressMap().remove(taskId);
+ this.plugin.getQuestsLogger().debug("Cleaning file " + this.playerUUID + ".");
+
+ if (!this.plugin.getTaskTypeManager().areRegistrationsOpen()) {
+ final List<String> invalidQuestIds = new ArrayList<>();
+
+ for (final Map.Entry<String, QuestProgress> questProgressEntry : this.questProgressMap.entrySet()) {
+ final String questId = questProgressEntry.getKey();
+
+ final Quest quest = this.plugin.getQuestManager().getQuestById(questId);
+ if (quest == null) {
+ invalidQuestIds.add(questId);
+
+ // tasks will be removed with the quest
+ continue;
+ }
+
+ final QuestProgress questProgress = questProgressEntry.getValue();
+ final Map<String, TaskProgress> taskProgressMap = questProgress.getTaskProgressMap();
+ final List<String> invalidTaskIds = new ArrayList<>();
+
+ for (final String taskId : taskProgressMap.keySet()) {
+ final Task task = quest.getTaskById(taskId);
+
+ if (task == null) {
+ invalidTaskIds.add(taskId);
}
}
+
+ for (final String taskId : invalidTaskIds) {
+ taskProgressMap.remove(taskId);
+ }
}
- for (String questId : invalidQuests) {
- this.questProgress.remove(questId);
+
+ for (final String questId : invalidQuestIds) {
+ this.questProgressMap.remove(questId);
}
}
}
+ /**
+ * It's equivalent to {@code QuestProgressFile#setModified(false)}.
+ *
+ * @see QuestProgressFile#setModified(boolean)
+ */
+ @Deprecated(forRemoval = true)
public void resetModified() {
- for (QuestProgress questProgress : questProgress.values()) {
- questProgress.resetModified();
- }
+ this.setModified(false);
}
- public void setModified(boolean modified) {
- for (QuestProgress questProgress : questProgress.values()) {
+ /**
+ * @param modified whether the object has been modified and needs to be saved
+ */
+ public void setModified(final boolean modified) {
+ for (final QuestProgress questProgress : this.questProgressMap.values()) {
questProgress.setModified(modified);
}
}
-
}
diff --git a/common/src/main/java/com/leonardobishop/quests/common/player/questprogressfile/TaskProgress.java b/common/src/main/java/com/leonardobishop/quests/common/player/questprogressfile/TaskProgress.java
index a78a4d25..9395634e 100644
--- a/common/src/main/java/com/leonardobishop/quests/common/player/questprogressfile/TaskProgress.java
+++ b/common/src/main/java/com/leonardobishop/quests/common/player/questprogressfile/TaskProgress.java
@@ -1,84 +1,163 @@
package com.leonardobishop.quests.common.player.questprogressfile;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Objects;
import java.util.UUID;
-public class TaskProgress {
+public final class TaskProgress {
- private final String taskid;
- private final UUID player;
+ private final QuestProgress questProgress;
+ private final String taskId;
+ private final UUID playerUUID;
- private QuestProgress linkedQuestProgress;
- private boolean modified;
private Object progress;
private boolean completed;
+ private boolean modified;
- public TaskProgress(QuestProgress linkedQuestProgress, String taskid, Object progress, UUID player, boolean completed) {
- this.linkedQuestProgress = linkedQuestProgress;
- this.taskid = taskid;
+ /**
+ * Constructs a TaskProgress.
+ *
+ * @param questProgress the quest progress
+ * @param taskId the associated task ID
+ * @param playerUUID the associated player UUID
+ * @param progress the progress object
+ * @param completed whether the task is completed
+ * @param modified whether the object has been modified and needs to be saved
+ */
+ public TaskProgress(final @Nullable QuestProgress questProgress, final @NotNull String taskId, final @NotNull UUID playerUUID, final @Nullable Object progress, final boolean completed, final boolean modified) {
+ this.questProgress = questProgress;
+ this.taskId = taskId;
+ this.playerUUID = playerUUID;
this.progress = progress;
- this.player = player;
this.completed = completed;
+ this.modified = modified;
}
- public TaskProgress(QuestProgress linkedQuestProgress, String taskid, Object progress, UUID player, boolean completed, boolean modified) {
- this(linkedQuestProgress, taskid, progress, player, completed);
- this.modified = modified;
+ /**
+ * Constructs a TaskProgress with {@link TaskProgress#modified} set to {@code false}.
+ *
+ * @param questProgress the quest progress
+ * @param taskId the associated task ID
+ * @param playerUUID the associated player UUID
+ * @param progress the progress object
+ * @param completed whether the task is completed
+ */
+ public TaskProgress(final @NotNull QuestProgress questProgress, final @NotNull String taskId, final @NotNull UUID playerUUID, final @Nullable Object progress, final boolean completed) {
+ this(questProgress, taskId, playerUUID, progress, completed, false);
}
- public TaskProgress(TaskProgress taskProgress) {
- this.taskid = taskProgress.taskid;
- this.player = taskProgress.player;
- this.modified = taskProgress.modified;
- this.progress = taskProgress.progress;
- this.completed = taskProgress.completed;
+ /**
+ * Constructs a data-only clone from a TaskProgress instance.
+ *
+ * @param taskProgress the task progress instance
+ */
+ @ApiStatus.Internal
+ public TaskProgress(final @NotNull TaskProgress taskProgress) {
+ this(null, taskProgress.taskId, taskProgress.playerUUID, taskProgress.progress, taskProgress.completed, taskProgress.modified);
}
- public String getTaskId() {
- return taskid;
+ /**
+ * @return the associated task ID
+ */
+ @Contract(pure = true)
+ public @NotNull String getTaskId() {
+ return this.taskId;
}
- public Object getProgress() {
- return progress;
+ /**
+ * @return the associated player ID
+ * @see QuestProgress#getPlayerUUID()
+ */
+ @Deprecated(forRemoval = true)
+ @Contract(pure = true)
+ public @NotNull UUID getPlayer() {
+ return this.playerUUID;
}
- public void setProgress(Object progress) {
- if (this.progress != progress) this.modified = true;
+ /**
+ * @return the associated player ID
+ */
+ @Contract(pure = true)
+ public @NotNull UUID getPlayerUUID() {
+ return this.playerUUID;
+ }
- this.progress = progress;
+ /**
+ * @return the progress object
+ */
+ @Contract(pure = true)
+ public @Nullable Object getProgress() {
+ return this.progress;
}
- public UUID getPlayer() {
- return player;
+ /**
+ * @param progress the progress object
+ */
+ public void setProgress(final @Nullable Object progress) {
+ if (Objects.equals(progress, this.progress)) {
+ return;
+ }
+
+ this.progress = progress;
+ this.modified = true;
}
+ /**
+ * @return whether the task is completed
+ */
+ @Contract(pure = true)
public boolean isCompleted() {
- return completed;
+ return this.completed;
}
- public void setCompleted(boolean complete) {
+ /**
+ * @param completed whether the task is completed
+ */
+ public void setCompleted(final boolean completed) {
+ if (this.questProgress == null) {
+ throw new UnsupportedOperationException("associated quest progress cannot be null");
+ }
+
// do not queue completion for already completed quests
// https://github.com/LMBishop/Quests/issues/543
- if (this.completed == complete) {
+ if (this.completed == completed) {
return;
}
- this.completed = complete;
+ this.completed = completed;
this.modified = true;
- if (complete) {
- linkedQuestProgress.queueForCompletionTest();
+ if (completed) {
+ this.questProgress.queueForCompletionTest();
}
}
+ /**
+ * @return whether the object has been modified and needs to be saved
+ */
+ @Contract(pure = true)
public boolean isModified() {
- return modified;
+ return this.modified;
}
+ /**
+ * It's equivalent to {@code TaskProgress#setModified(false)}.
+ *
+ * @see TaskProgress#setModified(boolean)
+ */
+ @Deprecated(forRemoval = true)
public void resetModified() {
- this.modified = false;
+ this.setModified(false);
}
- public void setModified(boolean modified) {
+ /**
+ * @param modified whether the object has been modified and needs to be saved
+ */
+ public void setModified(final boolean modified) {
this.modified = modified;
}
}
diff --git a/common/src/main/java/com/leonardobishop/quests/common/storage/StorageProvider.java b/common/src/main/java/com/leonardobishop/quests/common/storage/StorageProvider.java
index ba0ee8c8..a9c7b52f 100644
--- a/common/src/main/java/com/leonardobishop/quests/common/storage/StorageProvider.java
+++ b/common/src/main/java/com/leonardobishop/quests/common/storage/StorageProvider.java
@@ -1,61 +1,91 @@
package com.leonardobishop.quests.common.storage;
-import com.leonardobishop.quests.common.player.questprogressfile.QuestProgressFile;
+import com.leonardobishop.quests.common.player.QPlayerData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.io.IOException;
import java.util.List;
+import java.util.Objects;
import java.util.UUID;
/**
- * The storage provider is responsible for obtaining a QuestProgressFile for a specified UUID and for
- * writing a QuestProgressFile.
+ * The StorageProvider interface defines the contract for a storage system that handles the persistence
+ * of player data, such as player preferences and quest progress, for specific players identified by their UUIDs.
+ * Implementations of this interface are responsible for the actual storage and retrieval of this data.
*/
public interface StorageProvider {
- String getName();
+ /**
+ * Retrieves the name of this storage provider.
+ *
+ * @return the name of the storage provider
+ */
+ @NotNull String getName();
- void init();
+ /**
+ * Initializes the storage provider, preparing it for use. This method should be called before any
+ * other operations are performed. Initialization may involve setting up connections or loading necessary resources.
+ */
+ void init() throws IOException;
+ /**
+ * Shuts down the storage provider, ensuring that any open resources are properly closed and that
+ * any pending data is safely stored. This method should be called during the application's shutdown process.
+ */
void shutdown();
/**
- * Load a QuestProgressFile from the data source by a specific UUID
+ * Loads the player data associated with the given UUID from the storage.
*
- * @param uuid the UUID to load
- * @return {@link QuestProgressFile} or null
+ * @param uuid the unique identifier of the player whose data is to be loaded
+ * @return the {@link QPlayerData} for the player, or null if no data is found for the given UUID
*/
- @Nullable QuestProgressFile loadProgressFile(@NotNull UUID uuid);
+ @Nullable QPlayerData loadPlayerData(final @NotNull UUID uuid);
/**
- * Save a QuestProgressFile to the data source with a specific UUID
+ * Saves the given player data to the storage.
*
- * @param uuid the uuid to match the file to
- * @param questProgressFile the file to save
+ * @param playerData the {@link QPlayerData} object containing the player's data to be saved
+ * @return true if the data was successfully saved, false otherwise
*/
- boolean saveProgressFile(@NotNull UUID uuid, @NotNull QuestProgressFile questProgressFile);
+ boolean savePlayerData(final @NotNull QPlayerData playerData);
/**
- * Load all QuestProgressFiles
+ * Loads all player data available in the storage.
*
- * @return {@link List<QuestProgressFile>}
+ * @return a list of {@link QPlayerData} objects
*/
- @NotNull List<QuestProgressFile> loadAllProgressFiles();
+ @NotNull List<QPlayerData> loadAllPlayerData();
/**
- * Save a list of QuestProgressFiles
+ * Saves all provided player data to the storage.
*
- * @param files the list of QuestProgressFile to save
- **/
- void saveAllProgressFiles(List<QuestProgressFile> files);
+ * @param allPlayerData a list of {@link QPlayerData} objects to be saved
+ * @return true if the data was successfully saved, false otherwise
+ */
+ default boolean saveAllPlayerData(final @NotNull List<QPlayerData> allPlayerData) {
+ Objects.requireNonNull(allPlayerData, "allPlayerData cannot be null");
+
+ // fault check is not needed here as the method
+ // saving single player data already handles that,
+ // and it's actually the one we need to check
+
+ boolean result = true;
+
+ for (final QPlayerData playerData : allPlayerData) {
+ result &= this.savePlayerData(playerData);
+ }
+
+ return result;
+ }
/**
- * Whether this provider is 'similar' to another one.
- * Similarity is determined if the provider effectively points to the same data source.
+ * Compares this storage provider with another to determine if they are similar.
+ * Similarity is determined by effectively pointing to the same data source.
*
- * @param provider the provider to compare to
- * @return true if similar, false otherwise
+ * @param otherProvider another StorageProvider to compare against
+ * @return true if the two storage providers are considered similar, false otherwise
*/
- boolean isSimilar(StorageProvider provider);
-
+ boolean isSimilar(final @NotNull StorageProvider otherProvider);
}