From 709860e3e3ecd5dc9a9d219b63b4161f909d31f1 Mon Sep 17 00:00:00 2001 From: LMBishop <13875753+LMBishop@users.noreply.github.com> Date: Thu, 3 Jun 2021 13:05:28 +0100 Subject: Add init --- .../java/com/leonardobishop/quests/storage/StorageProvider.java | 1 + .../java/com/leonardobishop/quests/storage/YamlStorageProvider.java | 6 ++++++ 2 files changed, 7 insertions(+) (limited to 'src/main/java') diff --git a/src/main/java/com/leonardobishop/quests/storage/StorageProvider.java b/src/main/java/com/leonardobishop/quests/storage/StorageProvider.java index 65e4e091..78f27f54 100644 --- a/src/main/java/com/leonardobishop/quests/storage/StorageProvider.java +++ b/src/main/java/com/leonardobishop/quests/storage/StorageProvider.java @@ -6,6 +6,7 @@ import java.util.UUID; public interface StorageProvider { + void init(); QuestProgressFile loadProgressFile(UUID uuid); void saveProgressFile(UUID uuid, QuestProgressFile questProgressFile); diff --git a/src/main/java/com/leonardobishop/quests/storage/YamlStorageProvider.java b/src/main/java/com/leonardobishop/quests/storage/YamlStorageProvider.java index ababef3e..950f6d6e 100644 --- a/src/main/java/com/leonardobishop/quests/storage/YamlStorageProvider.java +++ b/src/main/java/com/leonardobishop/quests/storage/YamlStorageProvider.java @@ -31,6 +31,12 @@ public class YamlStorageProvider implements StorageProvider { return lock; } + @Override + public void init() { + File directory = new File(plugin.getDataFolder() + File.separator + "playerdata"); + directory.mkdirs(); + } + public QuestProgressFile loadProgressFile(UUID uuid) { ReentrantLock lock = lock(uuid); QuestProgressFile questProgressFile = new QuestProgressFile(uuid, plugin); -- cgit v1.2.3-70-g09d2 From e5a28b13be1e71eda33a1e3f5e8e56e5dad6c063 Mon Sep 17 00:00:00 2001 From: LMBishop <13875753+LMBishop@users.noreply.github.com> Date: Thu, 3 Jun 2021 18:04:28 +0100 Subject: Initial SQL storage implementation --- build.gradle | 2 + .../java/com/leonardobishop/quests/Quests.java | 5 +- .../quests/player/QPlayerManager.java | 4 +- .../quests/storage/MySqlStorageProvider.java | 258 +++++++++++++++++++++ src/main/resources/config.yml | 18 +- 5 files changed, 274 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java (limited to 'src/main/java') diff --git a/build.gradle b/build.gradle index d9f61c7b..a6a0f745 100644 --- a/build.gradle +++ b/build.gradle @@ -85,6 +85,8 @@ dependencies { implementation 'org.bstats:bstats-bukkit-lite:1.8' // HikariCP implementation 'com.zaxxer:HikariCP:4.0.3' + // slf4j + implementation 'org.slf4j:slf4j-simple:1.7.30' compileOnly fileTree(dir: 'libs', includes: ['*.jar']) } diff --git a/src/main/java/com/leonardobishop/quests/Quests.java b/src/main/java/com/leonardobishop/quests/Quests.java index cab89f6a..7d74283f 100644 --- a/src/main/java/com/leonardobishop/quests/Quests.java +++ b/src/main/java/com/leonardobishop/quests/Quests.java @@ -166,13 +166,14 @@ public class Quests extends JavaPlugin { questsLogger = new QuestsLogger(this, QuestsLogger.LoggingLevel.INFO); questCompleter = new QuestCompleter(this); + this.generateConfigurations(); + this.setupVersionSpecific(); + taskTypeManager = new TaskTypeManager(this); questManager = new QuestManager(this); qPlayerManager = new QPlayerManager(this); menuController = new MenuController(this); - this.generateConfigurations(); - this.setupVersionSpecific(); super.getCommand("quests").setExecutor(new QuestsCommand(this)); Bukkit.getPluginManager().registerEvents(new PlayerJoinListener(this), this); diff --git a/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java b/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java index f521d2de..bd7ccf37 100644 --- a/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java +++ b/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java @@ -6,6 +6,7 @@ import com.leonardobishop.quests.player.questprogressfile.QPlayerPreferences; import com.leonardobishop.quests.player.questprogressfile.QuestProgress; import com.leonardobishop.quests.player.questprogressfile.QuestProgressFile; import com.leonardobishop.quests.player.questprogressfile.TaskProgress; +import com.leonardobishop.quests.storage.MySqlStorageProvider; import com.leonardobishop.quests.storage.StorageProvider; import com.leonardobishop.quests.storage.YamlStorageProvider; import com.leonardobishop.quests.util.Options; @@ -24,7 +25,8 @@ public class QPlayerManager { private StorageProvider storageProvider; public QPlayerManager(Quests plugin) { - this.storageProvider = new YamlStorageProvider(plugin); + this.storageProvider = new MySqlStorageProvider(plugin, plugin.getConfig().getConfigurationSection("options.storage.database-settings")); + storageProvider.init(); this.plugin = plugin; } diff --git a/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java b/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java new file mode 100644 index 00000000..83147a19 --- /dev/null +++ b/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java @@ -0,0 +1,258 @@ +package com.leonardobishop.quests.storage; + +import com.leonardobishop.quests.Quests; +import com.leonardobishop.quests.player.questprogressfile.QuestProgress; +import com.leonardobishop.quests.player.questprogressfile.QuestProgressFile; +import com.leonardobishop.quests.player.questprogressfile.TaskProgress; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.MemorySection; +import org.bukkit.configuration.file.YamlConfiguration; + +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.List; +import java.util.Map; +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` (" + + " `id` INT NOT NULL AUTO_INCREMENT," + + " `uuid` VARCHAR(36) NOT NULL," + + " `quest_id` VARCHAR(50) NOT NULL," + + " `started` BOOL NOT NULL," + + " `completed` BOOL NOT NULL," + + " `completed_before` BOOL NOT NULL," + + " `completion_date` BIGINT NOT NULL," + + " PRIMARY KEY (`id`));"; + private static final String CREATE_TABLE_TASK_PROGRESS = + "CREATE TABLE IF NOT EXISTS `{prefix}task_progress` (" + + " `id` INT NOT NULL AUTO_INCREMENT," + + " `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," + + " PRIMARY KEY (`id`));"; + private static final String SELECT_PLAYER_QUEST_PROGRESS = + "SELECT quest_id, started, 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 FROM `{prefix}task_progress` WHERE uuid=?;"; + 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 INSERT_PLAYER_QUEST_PROGRESS = + "INSERT INTO `{prefix}quest_progress` (uuid, quest_id, started, completed, completed_before, completion_date) VALUES (?,?,?,?,?,?)"; + private static final String INSERT_PLAYER_TASK_PROGRESS = + "INSERT INTO `{prefix}task_progress` (uuid, quest_id, task_id, completed, progress) VALUES (?,?,?,?,?)"; + private static final String UPDATE_PLAYER_QUEST_PROGRESS = + "UPDATE `{prefix}quest_progress` SET started=?, completed=?, completed_before=?, completion_date=? WHERE uuid=? AND quest_id=?"; + private static final String UPDATE_PLAYER_TASK_PROGRESS = + "UPDATE `{prefix}task_progress` SET completed=?, progress=? WHERE uuid=? AND quest_id=? AND task_id=?"; + + private final HikariDataSource hikari; + private final String prefix; + private final Quests plugin; + private final Function statementProcessor; + + public MySqlStorageProvider(Quests plugin, ConfigurationSection configuration) { + this.plugin = plugin; + if (configuration == null) { + configuration = new YamlConfiguration(); + } + + 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); + + this.hikari = new HikariDataSource(config); + this.prefix = configuration.getString("database-settings.table-prefix", "quests_"); + this.statementProcessor = s -> s.replace("{prefix}", prefix); + } + + @Override + public void init() { + 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.executeBatch(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } + + @Override + public QuestProgressFile loadProgressFile(UUID uuid) { + QuestProgressFile questProgressFile = new QuestProgressFile(uuid, plugin); + try (Connection connection = hikari.getConnection()) { + plugin.getQuestsLogger().debug("Querying player " + uuid); + Map questProgressMap = new HashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(this.statementProcessor.apply(SELECT_PLAYER_QUEST_PROGRESS))) { + ps.setString(1, uuid.toString()); + + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String questId = rs.getString(1); + boolean started = rs.getBoolean(2); + boolean completed = rs.getBoolean(3); + boolean completedBefore = rs.getBoolean(4); + long completionDate = rs.getLong(5); + + QuestProgress questProgress = new QuestProgress(plugin, questId, completed, completedBefore, completionDate, uuid, started); + 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); + Object progress = rs.getObject(4); + + QuestProgress linkedQuestProgress = questProgressMap.get(questId); + if (linkedQuestProgress == null) continue; // lost quest progress ? + TaskProgress questProgress = new TaskProgress(linkedQuestProgress, taskId, progress, uuid, completed); + linkedQuestProgress.addTaskProgress(questProgress); + } + } + } + for (QuestProgress questProgress : questProgressMap.values()) { + questProgressFile.addQuestProgress(questProgress); + } + } catch (SQLException e) { + plugin.getQuestsLogger().severe("Failed to load player: " + uuid + "!"); + e.printStackTrace(); + } + return questProgressFile; + } + + @Override + public void saveProgressFile(UUID uuid, QuestProgressFile questProgressFile) { + try (Connection connection = hikari.getConnection()) { + plugin.getQuestsLogger().debug("Saving player " + uuid); + List knownQuestIds = new ArrayList<>(); + Map> knownTaskIds = new HashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(this.statementProcessor.apply(SELECT_KNOWN_PLAYER_QUEST_PROGRESS))) { + ps.setString(1, uuid.toString()); + + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + knownQuestIds.add(rs.getString(0)); + } + } + } + try (PreparedStatement ps = connection.prepareStatement(this.statementProcessor.apply(SELECT_KNOWN_PLAYER_TASK_PROGRESS))) { + ps.setString(1, uuid.toString()); + + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String questId = rs.getString(0); + String taskId = rs.getString(1); + + knownTaskIds.putIfAbsent(questId, new ArrayList<>()); + knownTaskIds.get(questId).add(taskId); + } + } + } + + try (PreparedStatement insertQuestProgress = connection.prepareStatement(this.statementProcessor.apply(INSERT_PLAYER_QUEST_PROGRESS)); + PreparedStatement insertTaskProgress = connection.prepareStatement(this.statementProcessor.apply(INSERT_PLAYER_TASK_PROGRESS)); + PreparedStatement updateQuestProgress = connection.prepareStatement(this.statementProcessor.apply(UPDATE_PLAYER_QUEST_PROGRESS)); + PreparedStatement updateTaskProgress = connection.prepareStatement(this.statementProcessor.apply(UPDATE_PLAYER_TASK_PROGRESS))) { + + List questProgressValues = new ArrayList<>(questProgressFile.getAllQuestProgress()); + for (QuestProgress questProgress : questProgressValues) { + String questId = questProgress.getQuestId(); + if (knownQuestIds.contains(questId)) { + updateQuestProgress.setBoolean(1, questProgress.isStarted()); + updateQuestProgress.setBoolean(2, questProgress.isCompleted()); + updateQuestProgress.setBoolean(3, questProgress.isCompletedBefore()); + updateQuestProgress.setLong(4, questProgress.getCompletionDate()); + updateQuestProgress.setString(5, uuid.toString()); + updateQuestProgress.setString(6, questId); + updateQuestProgress.addBatch(); + } else { + insertQuestProgress.setString(1, uuid.toString()); + insertQuestProgress.setString(2, questProgress.getQuestId()); + insertQuestProgress.setBoolean(3, questProgress.isStarted()); + insertQuestProgress.setBoolean(4, questProgress.isCompleted()); + insertQuestProgress.setBoolean(5, questProgress.isCompletedBefore()); + insertQuestProgress.setLong(6, questProgress.getCompletionDate()); + insertQuestProgress.addBatch(); + } + List taskIds = knownTaskIds.getOrDefault(questProgress.getQuestId(), Collections.emptyList()); + for (TaskProgress taskProgress : questProgress.getTaskProgress()) { + if (taskIds.contains(taskProgress.getTaskId())) { + updateTaskProgress.setBoolean(1, taskProgress.isCompleted()); + updateTaskProgress.setObject(2, taskProgress.getProgress()); + updateTaskProgress.setString(3, uuid.toString()); + updateTaskProgress.setString(4, questProgress.getQuestId()); + updateTaskProgress.setString(5, taskProgress.getTaskId()); + updateTaskProgress.addBatch(); + } else { + insertTaskProgress.setString(1, uuid.toString()); + insertTaskProgress.setString(2, questProgress.getQuestId()); + insertTaskProgress.setString(3, taskProgress.getTaskId()); + insertTaskProgress.setBoolean(4, taskProgress.isCompleted()); + insertTaskProgress.setObject(5, taskProgress.getProgress()); + insertTaskProgress.addBatch(); + } + } + } + + System.out.println(insertQuestProgress); + insertQuestProgress.executeBatch(); + System.out.println(insertTaskProgress); + insertTaskProgress.executeBatch(); + System.out.println(updateQuestProgress); + updateQuestProgress.executeBatch(); + System.out.println(updateTaskProgress); + updateTaskProgress.executeBatch(); + } + } catch (SQLException e) { + plugin.getQuestsLogger().severe("Failed to save player: " + uuid + "!"); + e.printStackTrace(); + } + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index b4c94332..b2ab5871 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -240,25 +240,23 @@ options: provider: "yaml" # The following is only applicable for database storage providers (e.g. mysql) database-settings: - # The name of the database. This database should already exist! - database-name: "minecraft" network: + # The name of the database. This database should already exist! + database: "minecraft" username: "root" password: "" - address: "localhost" - port: 3306 + # 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 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) - minimum-pool-size: 8 + minimum-idle: 8 # The maximum time (in milliseconds) to keep a single connection open (def=1800000 - 30 min) - connection-lifetime: 1800000 - # The time (in milliseconds) to ping the database (0 to disable, def=0) - heartbeat-time: 0 - # The time (in milliseconds) the plugin will wait for a response by the database (def=0) - timeout: 5000 + maximum-lifetime: 1800000 + # The time (in milliseconds) the plugin will wait for a response by the database (def=500) + connection-timeout: 5000 # The prefix each table will use table-prefix: "quests_" -- cgit v1.2.3-70-g09d2 From e87c40a581a5dde310cba7a168e532c837f2efc6 Mon Sep 17 00:00:00 2001 From: LMBishop <13875753+LMBishop@users.noreply.github.com> Date: Thu, 3 Jun 2021 18:33:45 +0100 Subject: Add types to task progress - This restricts the possible types for task progress to int, double and float --- .../quests/storage/MySqlStorageProvider.java | 95 +++++++++++++++++----- 1 file changed, 75 insertions(+), 20 deletions(-) (limited to 'src/main/java') diff --git a/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java b/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java index 83147a19..6a69a3a9 100644 --- a/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java +++ b/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java @@ -37,17 +37,18 @@ public class MySqlStorageProvider implements StorageProvider { " PRIMARY KEY (`id`));"; private static final String CREATE_TABLE_TASK_PROGRESS = "CREATE TABLE IF NOT EXISTS `{prefix}task_progress` (" + - " `id` INT NOT NULL AUTO_INCREMENT," + - " `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," + + " `id` INT NOT NULL AUTO_INCREMENT," + + " `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 (`id`));"; private static final String SELECT_PLAYER_QUEST_PROGRESS = "SELECT quest_id, started, 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 FROM `{prefix}task_progress` WHERE uuid=?;"; + "SELECT quest_id, task_id, completed, progress, data_type FROM `{prefix}task_progress` WHERE uuid=?;"; 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 = @@ -55,11 +56,11 @@ public class MySqlStorageProvider implements StorageProvider { private static final String INSERT_PLAYER_QUEST_PROGRESS = "INSERT INTO `{prefix}quest_progress` (uuid, quest_id, started, completed, completed_before, completion_date) VALUES (?,?,?,?,?,?)"; private static final String INSERT_PLAYER_TASK_PROGRESS = - "INSERT INTO `{prefix}task_progress` (uuid, quest_id, task_id, completed, progress) VALUES (?,?,?,?,?)"; + "INSERT INTO `{prefix}task_progress` (uuid, quest_id, task_id, completed, progress, data_type) VALUES (?,?,?,?,?,?)"; private static final String UPDATE_PLAYER_QUEST_PROGRESS = "UPDATE `{prefix}quest_progress` SET started=?, completed=?, completed_before=?, completion_date=? WHERE uuid=? AND quest_id=?"; private static final String UPDATE_PLAYER_TASK_PROGRESS = - "UPDATE `{prefix}task_progress` SET completed=?, progress=? WHERE uuid=? AND quest_id=? AND task_id=?"; + "UPDATE `{prefix}task_progress` SET completed=?, progress=?, data_type=? WHERE uuid=? AND quest_id=? AND task_id=?"; private final HikariDataSource hikari; private final String prefix; @@ -148,7 +149,36 @@ public class MySqlStorageProvider implements StorageProvider { String questId = rs.getString(1); String taskId = rs.getString(2); boolean completed = rs.getBoolean(3); - Object progress = rs.getObject(4); + 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 { + 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; // lost quest progress ? @@ -178,7 +208,7 @@ public class MySqlStorageProvider implements StorageProvider { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { - knownQuestIds.add(rs.getString(0)); + knownQuestIds.add(rs.getString(1)); } } } @@ -187,8 +217,8 @@ public class MySqlStorageProvider implements StorageProvider { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { - String questId = rs.getString(0); - String taskId = rs.getString(1); + String questId = rs.getString(1); + String taskId = rs.getString(2); knownTaskIds.putIfAbsent(questId, new ArrayList<>()); knownTaskIds.get(questId).add(taskId); @@ -223,19 +253,44 @@ public class MySqlStorageProvider implements StorageProvider { } List taskIds = knownTaskIds.getOrDefault(questProgress.getQuestId(), Collections.emptyList()); for (TaskProgress taskProgress : questProgress.getTaskProgress()) { - if (taskIds.contains(taskProgress.getTaskId())) { + 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 { + plugin.getQuestsLogger().warning("Cannot store progress for task '" + + taskId + "' in quest '" + questId + "' for player " + uuid + + " since type " + progress.getClass().getName() + " cannot be encoded!"); + continue; + } + if (taskIds.contains(taskId)) { updateTaskProgress.setBoolean(1, taskProgress.isCompleted()); - updateTaskProgress.setObject(2, taskProgress.getProgress()); - updateTaskProgress.setString(3, uuid.toString()); - updateTaskProgress.setString(4, questProgress.getQuestId()); - updateTaskProgress.setString(5, taskProgress.getTaskId()); + updateTaskProgress.setString(2, encodedProgress); + updateTaskProgress.setString(3, type); + updateTaskProgress.setString(4, uuid.toString()); + updateTaskProgress.setString(5, questId); + updateTaskProgress.setString(6, taskId); updateTaskProgress.addBatch(); } else { insertTaskProgress.setString(1, uuid.toString()); - insertTaskProgress.setString(2, questProgress.getQuestId()); + insertTaskProgress.setString(2, questId); insertTaskProgress.setString(3, taskProgress.getTaskId()); insertTaskProgress.setBoolean(4, taskProgress.isCompleted()); - insertTaskProgress.setObject(5, taskProgress.getProgress()); + insertTaskProgress.setString(5, encodedProgress); + insertTaskProgress.setString(6, type); insertTaskProgress.addBatch(); } } -- cgit v1.2.3-70-g09d2 From 22819bf95e3d097c4c5dededf774854afc360eaf Mon Sep 17 00:00:00 2001 From: LMBishop <13875753+LMBishop@users.noreply.github.com> Date: Thu, 3 Jun 2021 18:41:00 +0100 Subject: Make storage provider configurable --- .../com/leonardobishop/quests/player/QPlayerManager.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'src/main/java') diff --git a/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java b/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java index bd7ccf37..836479a1 100644 --- a/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java +++ b/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java @@ -25,9 +25,18 @@ public class QPlayerManager { private StorageProvider storageProvider; public QPlayerManager(Quests plugin) { - this.storageProvider = new MySqlStorageProvider(plugin, plugin.getConfig().getConfigurationSection("options.storage.database-settings")); - storageProvider.init(); this.plugin = plugin; + + String configuredProvider = plugin.getConfig().getString("options.storage.provider", "yaml"); + if (configuredProvider.equalsIgnoreCase("yaml")) { + this.storageProvider = new YamlStorageProvider(plugin); + } else if (configuredProvider.equalsIgnoreCase("mysql")) { + this.storageProvider = new MySqlStorageProvider(plugin, plugin.getConfig().getConfigurationSection("options.storage.database-settings")); + } else { + plugin.getQuestsLogger().warning("No valid storage provider is configured - Quests will use YAML storage as a default"); + this.storageProvider = new YamlStorageProvider(plugin); + } + storageProvider.init(); } private final Map qPlayers = new ConcurrentHashMap<>(); -- cgit v1.2.3-70-g09d2 From 0e73490abce721bd9a15dc3b16d016ff7823b3cf Mon Sep 17 00:00:00 2001 From: LMBishop <13875753+LMBishop@users.noreply.github.com> Date: Thu, 3 Jun 2021 18:46:11 +0100 Subject: Properly close Hikari --- src/main/java/com/leonardobishop/quests/Quests.java | 3 +++ .../java/com/leonardobishop/quests/player/QPlayerManager.java | 9 +++++++++ .../com/leonardobishop/quests/storage/MySqlStorageProvider.java | 5 +++++ .../java/com/leonardobishop/quests/storage/StorageProvider.java | 1 + .../com/leonardobishop/quests/storage/YamlStorageProvider.java | 5 +++++ 5 files changed, 23 insertions(+) (limited to 'src/main/java') diff --git a/src/main/java/com/leonardobishop/quests/Quests.java b/src/main/java/com/leonardobishop/quests/Quests.java index 7d74283f..5400c265 100644 --- a/src/main/java/com/leonardobishop/quests/Quests.java +++ b/src/main/java/com/leonardobishop/quests/Quests.java @@ -307,6 +307,9 @@ public class Quests extends JavaPlugin { } catch (Exception ignored) { } } if (placeholderAPIHook != null) placeholderAPIHook.unregisterExpansion(); + try { + qPlayerManager.getStorageProvider().shutdown(); + } catch (Exception ignored) { } } public void reloadQuests() { diff --git a/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java b/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java index 836479a1..0509799f 100644 --- a/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java +++ b/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java @@ -119,4 +119,13 @@ public class QPlayerManager { return new QPlayer(uuid, questProgressFile, new QPlayerPreferences(null), plugin); }); } + + /** + * Gets the current storage provider which loads and saves players. + * + * @return {@link StorageProvider} + */ + public StorageProvider getStorageProvider() { + return storageProvider; + } } diff --git a/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java b/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java index 6a69a3a9..5789d8a0 100644 --- a/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java +++ b/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java @@ -119,6 +119,11 @@ public class MySqlStorageProvider implements StorageProvider { } } + @Override + public void shutdown() { + if (hikari != null) hikari.close(); + } + @Override public QuestProgressFile loadProgressFile(UUID uuid) { QuestProgressFile questProgressFile = new QuestProgressFile(uuid, plugin); diff --git a/src/main/java/com/leonardobishop/quests/storage/StorageProvider.java b/src/main/java/com/leonardobishop/quests/storage/StorageProvider.java index 78f27f54..f1d00203 100644 --- a/src/main/java/com/leonardobishop/quests/storage/StorageProvider.java +++ b/src/main/java/com/leonardobishop/quests/storage/StorageProvider.java @@ -7,6 +7,7 @@ import java.util.UUID; public interface StorageProvider { void init(); + void shutdown(); QuestProgressFile loadProgressFile(UUID uuid); void saveProgressFile(UUID uuid, QuestProgressFile questProgressFile); diff --git a/src/main/java/com/leonardobishop/quests/storage/YamlStorageProvider.java b/src/main/java/com/leonardobishop/quests/storage/YamlStorageProvider.java index 950f6d6e..4c803baa 100644 --- a/src/main/java/com/leonardobishop/quests/storage/YamlStorageProvider.java +++ b/src/main/java/com/leonardobishop/quests/storage/YamlStorageProvider.java @@ -37,6 +37,11 @@ public class YamlStorageProvider implements StorageProvider { directory.mkdirs(); } + @Override + public void shutdown() { + // no impl + } + public QuestProgressFile loadProgressFile(UUID uuid) { ReentrantLock lock = lock(uuid); QuestProgressFile questProgressFile = new QuestProgressFile(uuid, plugin); -- cgit v1.2.3-70-g09d2 From 159109f5c28e8611171ce0d492393ccd3382231e Mon Sep 17 00:00:00 2001 From: LMBishop <13875753+LMBishop@users.noreply.github.com> Date: Sat, 5 Jun 2021 14:49:21 +0100 Subject: Reimplement modified status --- .../java/com/leonardobishop/quests/Quests.java | 2 +- .../quests/QuestsAutosaveRunnable.java | 3 +- .../quests/command/QuestsCommand.java | 8 ++-- .../quests/listener/PlayerLeaveListener.java | 3 +- .../quests/player/QPlayerManager.java | 54 ++++++++++++++++------ .../player/questprogressfile/QuestProgress.java | 5 +- .../questprogressfile/QuestProgressFile.java | 6 +++ .../player/questprogressfile/TaskProgress.java | 4 +- .../quests/storage/MySqlStorageProvider.java | 9 ++-- 9 files changed, 61 insertions(+), 33 deletions(-) (limited to 'src/main/java') diff --git a/src/main/java/com/leonardobishop/quests/Quests.java b/src/main/java/com/leonardobishop/quests/Quests.java index 5400c265..21cd8a6e 100644 --- a/src/main/java/com/leonardobishop/quests/Quests.java +++ b/src/main/java/com/leonardobishop/quests/Quests.java @@ -303,7 +303,7 @@ public class Quests extends JavaPlugin { } for (QPlayer qPlayer : qPlayerManager.getQPlayers()) { try { - qPlayerManager.savePlayer(qPlayer.getPlayerUUID()); + qPlayerManager.savePlayerSync(qPlayer.getPlayerUUID()); } catch (Exception ignored) { } } if (placeholderAPIHook != null) placeholderAPIHook.unregisterExpansion(); diff --git a/src/main/java/com/leonardobishop/quests/QuestsAutosaveRunnable.java b/src/main/java/com/leonardobishop/quests/QuestsAutosaveRunnable.java index c042fd32..eaa37d87 100644 --- a/src/main/java/com/leonardobishop/quests/QuestsAutosaveRunnable.java +++ b/src/main/java/com/leonardobishop/quests/QuestsAutosaveRunnable.java @@ -37,8 +37,7 @@ public class QuestsAutosaveRunnable extends BukkitRunnable { } if (Bukkit.getPlayer(player) != null) { - QuestProgressFile clonedProgressFile = new QuestProgressFile(plugin.getPlayerManager().getPlayer(player).getQuestProgressFile()); - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> plugin.getPlayerManager().savePlayer(player, clonedProgressFile)); + plugin.getPlayerManager().savePlayer(player); } } diff --git a/src/main/java/com/leonardobishop/quests/command/QuestsCommand.java b/src/main/java/com/leonardobishop/quests/command/QuestsCommand.java index 47aa050e..d33baa65 100644 --- a/src/main/java/com/leonardobishop/quests/command/QuestsCommand.java +++ b/src/main/java/com/leonardobishop/quests/command/QuestsCommand.java @@ -285,7 +285,7 @@ public class QuestsCommand implements TabExecutor { } QuestProgressFile questProgressFile = qPlayer.getQuestProgressFile(); questProgressFile.clear(); - plugin.getPlayerManager().savePlayer(uuid, questProgressFile); + plugin.getPlayerManager().savePlayerSync(uuid, questProgressFile); if (Bukkit.getPlayer(uuid) == null) { plugin.getPlayerManager().dropPlayer(uuid); } @@ -355,7 +355,7 @@ public class QuestsCommand implements TabExecutor { } if (args[2].equalsIgnoreCase("reset")) { questProgressFile.generateBlankQuestProgress(quest); - plugin.getPlayerManager().savePlayer(uuid, questProgressFile); + plugin.getPlayerManager().savePlayerSync(uuid, questProgressFile); sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_RESET_SUCCESS.getMessage().replace("{player}", name).replace("{quest}", quest.getId())); success = true; } else if (args[2].equalsIgnoreCase("start")) { @@ -382,12 +382,12 @@ public class QuestsCommand implements TabExecutor { sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_FAILCATEGORYPERMISSION.getMessage().replace("{player}", name).replace("{quest}", quest.getId())); return true; } - plugin.getPlayerManager().savePlayer(uuid, questProgressFile); + plugin.getPlayerManager().savePlayerSync(uuid, questProgressFile); sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_SUCCESS.getMessage().replace("{player}", name).replace("{quest}", quest.getId())); success = true; } else if (args[2].equalsIgnoreCase("complete")) { qPlayer.completeQuest(quest); - plugin.getPlayerManager().savePlayer(uuid, questProgressFile); + plugin.getPlayerManager().savePlayerSync(uuid, questProgressFile); sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_COMPLETE_SUCCESS.getMessage().replace("{player}", name).replace("{quest}", quest.getId())); success = true; } diff --git a/src/main/java/com/leonardobishop/quests/listener/PlayerLeaveListener.java b/src/main/java/com/leonardobishop/quests/listener/PlayerLeaveListener.java index 99cad4e2..d246e1f6 100644 --- a/src/main/java/com/leonardobishop/quests/listener/PlayerLeaveListener.java +++ b/src/main/java/com/leonardobishop/quests/listener/PlayerLeaveListener.java @@ -20,8 +20,7 @@ public class PlayerLeaveListener implements Listener { public void onEvent(PlayerQuitEvent event) { QPlayer qPlayer = plugin.getPlayerManager().getPlayer(event.getPlayer().getUniqueId()); if (qPlayer == null) return; - QuestProgressFile clonedProgressFile = new QuestProgressFile(qPlayer.getQuestProgressFile()); - Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> plugin.getPlayerManager().removePlayer(qPlayer.getPlayerUUID(), clonedProgressFile)); + plugin.getPlayerManager().removePlayer(qPlayer.getPlayerUUID()); } } diff --git a/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java b/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java index 0509799f..2cd9cbfd 100644 --- a/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java +++ b/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java @@ -59,37 +59,65 @@ public class QPlayerManager { } /** - * Unloads and saves the player to disk. + * Unloads and schedules a save for the player. See {@link QPlayerManager#savePlayer(UUID)} * * @param uuid the uuid of the player - * @param questProgressFile the quest progress file to save */ - public void removePlayer(UUID uuid, QuestProgressFile questProgressFile) { + public void removePlayer(UUID uuid) { plugin.getQuestsLogger().debug("Unloading and saving player " + uuid + ". Main thread: " + Bukkit.isPrimaryThread()); qPlayers.computeIfPresent(uuid, (mapUUID, qPlayer) -> { - storageProvider.saveProgressFile(uuid, questProgressFile); + savePlayer(uuid); return null; }); } /** - * Saves the player to disk with a specified {@link QuestProgressFile}. + * Schedules a save for the player with the {@link QuestProgressFile} associated by the {@link QPlayerManager}. + * The modified status of the progress file will be reset. * * @param uuid the uuid of the player - * @param questProgressFile the quest progress file to associate with and save */ - public void savePlayer(UUID uuid, QuestProgressFile questProgressFile) { - plugin.getQuestsLogger().debug("Saving player " + uuid + ". Main thread: " + Bukkit.isPrimaryThread()); - storageProvider.saveProgressFile(uuid, questProgressFile); + public void savePlayer(UUID uuid) { + savePlayer(uuid, getPlayer(uuid).getQuestProgressFile()); } /** - * Saves the player to disk using the {@link QuestProgressFile} associated by the {@link QPlayerManager} + * 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 */ - public void savePlayer(UUID uuid) { - savePlayer(uuid, getPlayer(uuid).getQuestProgressFile()); + public void savePlayer(UUID uuid, QuestProgressFile originalProgressFile) { + QuestProgressFile clonedProgressFile = new QuestProgressFile(originalProgressFile); + originalProgressFile.resetModified(); + plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> save(uuid, clonedProgressFile)); + } + + /** + * Immediately saves the player with the {@link QuestProgressFile} associated by the {@link QPlayerManager}, + * on the same thread. The modified status of the specified progress file is not changed. + * + * @param uuid the uuid of the player + */ + public void savePlayerSync(UUID uuid) { + savePlayerSync(uuid, getPlayer(uuid).getQuestProgressFile()); + } + + /** + * 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(UUID uuid, QuestProgressFile questProgressFile) { + save(uuid, questProgressFile); + } + + private void save(UUID uuid, QuestProgressFile questProgressFile) { + plugin.getQuestsLogger().debug("Saving player " + uuid + ". Main thread: " + Bukkit.isPrimaryThread()); + storageProvider.saveProgressFile(uuid, questProgressFile); } /** @@ -107,7 +135,7 @@ public class QPlayerManager { } /** - * Load the player from disk if they exist, otherwise create a new {@link QuestProgressFile}. + * Load the player if they exist, otherwise create a new {@link QuestProgressFile}. * This will have no effect if player is already loaded. Can be invoked asynchronously. * * @param uuid the uuid of the player diff --git a/src/main/java/com/leonardobishop/quests/player/questprogressfile/QuestProgress.java b/src/main/java/com/leonardobishop/quests/player/questprogressfile/QuestProgress.java index 7d795fed..d7cac084 100644 --- a/src/main/java/com/leonardobishop/quests/player/questprogressfile/QuestProgress.java +++ b/src/main/java/com/leonardobishop/quests/player/questprogressfile/QuestProgress.java @@ -120,8 +120,7 @@ public class QuestProgress { this.addTaskProgress(taskProgress); } - @Deprecated // this shit is annoying to maintain - public boolean isWorthSaving() { + public boolean isModified() { if (modified) return true; else { for (TaskProgress progress : this.taskProgress.values()) { @@ -138,7 +137,7 @@ public class QuestProgress { public void resetModified() { this.modified = false; for (TaskProgress progress : this.taskProgress.values()) { - progress.setModified(false); + progress.resetModified(); } } } diff --git a/src/main/java/com/leonardobishop/quests/player/questprogressfile/QuestProgressFile.java b/src/main/java/com/leonardobishop/quests/player/questprogressfile/QuestProgressFile.java index 0ddf3525..ccdb1db1 100644 --- a/src/main/java/com/leonardobishop/quests/player/questprogressfile/QuestProgressFile.java +++ b/src/main/java/com/leonardobishop/quests/player/questprogressfile/QuestProgressFile.java @@ -244,4 +244,10 @@ public class QuestProgressFile { } } + public void resetModified() { + for (QuestProgress questProgress : questProgress.values()) { + questProgress.resetModified(); + } + } + } diff --git a/src/main/java/com/leonardobishop/quests/player/questprogressfile/TaskProgress.java b/src/main/java/com/leonardobishop/quests/player/questprogressfile/TaskProgress.java index 14e1bea0..e0943ff3 100644 --- a/src/main/java/com/leonardobishop/quests/player/questprogressfile/TaskProgress.java +++ b/src/main/java/com/leonardobishop/quests/player/questprogressfile/TaskProgress.java @@ -67,7 +67,7 @@ public class TaskProgress { return modified; } - public void setModified(boolean modified) { - this.modified = modified; + public void resetModified() { + this.modified = false; } } diff --git a/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java b/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java index 5789d8a0..bac0b5c9 100644 --- a/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java +++ b/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java @@ -7,7 +7,6 @@ import com.leonardobishop.quests.player.questprogressfile.TaskProgress; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.MemorySection; import org.bukkit.configuration.file.YamlConfiguration; import java.sql.Connection; @@ -205,7 +204,7 @@ public class MySqlStorageProvider implements StorageProvider { @Override public void saveProgressFile(UUID uuid, QuestProgressFile questProgressFile) { try (Connection connection = hikari.getConnection()) { - plugin.getQuestsLogger().debug("Saving player " + uuid); + plugin.getQuestsLogger().debug("Getting known entries for player " + uuid); List knownQuestIds = new ArrayList<>(); Map> knownTaskIds = new HashMap<>(); try (PreparedStatement ps = connection.prepareStatement(this.statementProcessor.apply(SELECT_KNOWN_PLAYER_QUEST_PROGRESS))) { @@ -238,6 +237,8 @@ public class MySqlStorageProvider implements StorageProvider { List questProgressValues = new ArrayList<>(questProgressFile.getAllQuestProgress()); for (QuestProgress questProgress : questProgressValues) { + if (!questProgress.isModified()) continue; + String questId = questProgress.getQuestId(); if (knownQuestIds.contains(questId)) { updateQuestProgress.setBoolean(1, questProgress.isStarted()); @@ -301,13 +302,9 @@ public class MySqlStorageProvider implements StorageProvider { } } - System.out.println(insertQuestProgress); insertQuestProgress.executeBatch(); - System.out.println(insertTaskProgress); insertTaskProgress.executeBatch(); - System.out.println(updateQuestProgress); updateQuestProgress.executeBatch(); - System.out.println(updateTaskProgress); updateTaskProgress.executeBatch(); } } catch (SQLException e) { -- cgit v1.2.3-70-g09d2 From bf1a2b93a512ca7aa8208191d9db49be731ec280 Mon Sep 17 00:00:00 2001 From: LMBishop <13875753+LMBishop@users.noreply.github.com> Date: Sat, 5 Jun 2021 22:01:03 +0100 Subject: Remove clean option since it is stupid --- src/main/java/com/leonardobishop/quests/Quests.java | 6 ------ .../leonardobishop/quests/listener/PlayerJoinListener.java | 7 ------- .../quests/player/questprogressfile/QuestProgressFile.java | 4 ++++ .../leonardobishop/quests/storage/MySqlStorageProvider.java | 2 ++ .../leonardobishop/quests/storage/YamlStorageProvider.java | 2 +- src/main/java/com/leonardobishop/quests/util/Options.java | 3 +-- src/main/resources/config.yml | 12 ++++++------ 7 files changed, 14 insertions(+), 22 deletions(-) (limited to 'src/main/java') diff --git a/src/main/java/com/leonardobishop/quests/Quests.java b/src/main/java/com/leonardobishop/quests/Quests.java index 21cd8a6e..bfbc0e60 100644 --- a/src/main/java/com/leonardobishop/quests/Quests.java +++ b/src/main/java/com/leonardobishop/quests/Quests.java @@ -248,12 +248,6 @@ public class Quests extends JavaPlugin { taskTypeManager.closeRegistrations(); reloadQuests(); -// if (!questsConfigLoader.getBrokenFiles().isEmpty()) { -// this.getQuestsLogger().severe("Quests has failed to load the following files:"); -// for (Map.Entry entry : questsConfigLoader.getBrokenFiles().entrySet()) { -// this.getQuestsLogger().severe(" - " + entry.getKey() + ": " + entry.getValue().getMessage()); -// } -// } for (Player player : Bukkit.getOnlinePlayers()) { qPlayerManager.loadPlayer(player.getUniqueId()); diff --git a/src/main/java/com/leonardobishop/quests/listener/PlayerJoinListener.java b/src/main/java/com/leonardobishop/quests/listener/PlayerJoinListener.java index fcee6b40..5cdb465e 100644 --- a/src/main/java/com/leonardobishop/quests/listener/PlayerJoinListener.java +++ b/src/main/java/com/leonardobishop/quests/listener/PlayerJoinListener.java @@ -30,13 +30,6 @@ public class PlayerJoinListener implements Listener { public void onEvent(PlayerJoinEvent event) { UUID playerUuid = event.getPlayer().getUniqueId(); plugin.getPlayerManager().loadPlayer(playerUuid); - if (Options.SOFT_CLEAN_QUESTSPROGRESSFILE_ON_JOIN.getBooleanValue()) { - QPlayer qPlayer = plugin.getPlayerManager().getPlayer(playerUuid); - qPlayer.getQuestProgressFile().clean(); - if (Options.PUSH_SOFT_CLEAN_TO_DISK.getBooleanValue()) { - plugin.getPlayerManager().savePlayer(playerUuid, qPlayer.getQuestProgressFile()); - } - } if (plugin.getDescription().getVersion().contains("beta") && event.getPlayer().hasPermission("quests.admin")) { event.getPlayer().sendMessage(Messages.BETA_REMINDER.getMessage()); } diff --git a/src/main/java/com/leonardobishop/quests/player/questprogressfile/QuestProgressFile.java b/src/main/java/com/leonardobishop/quests/player/questprogressfile/QuestProgressFile.java index ccdb1db1..3f6d9900 100644 --- a/src/main/java/com/leonardobishop/quests/player/questprogressfile/QuestProgressFile.java +++ b/src/main/java/com/leonardobishop/quests/player/questprogressfile/QuestProgressFile.java @@ -4,6 +4,7 @@ import com.leonardobishop.quests.Quests; import com.leonardobishop.quests.player.QPlayer; import com.leonardobishop.quests.quest.Quest; import com.leonardobishop.quests.quest.Task; +import com.leonardobishop.quests.util.Options; import org.bukkit.Bukkit; import org.bukkit.configuration.file.YamlConfiguration; @@ -40,6 +41,9 @@ public class QuestProgressFile { } public void addQuestProgress(QuestProgress questProgress) { + if (Options.VERIFY_QUEST_EXISTS_ON_LOAD.getBooleanValue(true) && plugin.getQuestManager().getQuestById(questProgress.getQuestId()) == null) { + return; + } this.questProgress.put(questProgress.getQuestId(), questProgress); } diff --git a/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java b/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java index bac0b5c9..c2b30734 100644 --- a/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java +++ b/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java @@ -4,6 +4,7 @@ import com.leonardobishop.quests.Quests; import com.leonardobishop.quests.player.questprogressfile.QuestProgress; import com.leonardobishop.quests.player.questprogressfile.QuestProgressFile; import com.leonardobishop.quests.player.questprogressfile.TaskProgress; +import com.leonardobishop.quests.util.Options; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.bukkit.configuration.ConfigurationSection; @@ -17,6 +18,7 @@ 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.UUID; diff --git a/src/main/java/com/leonardobishop/quests/storage/YamlStorageProvider.java b/src/main/java/com/leonardobishop/quests/storage/YamlStorageProvider.java index 4c803baa..8ba30a4e 100644 --- a/src/main/java/com/leonardobishop/quests/storage/YamlStorageProvider.java +++ b/src/main/java/com/leonardobishop/quests/storage/YamlStorageProvider.java @@ -108,8 +108,8 @@ public class YamlStorageProvider implements StorageProvider { } YamlConfiguration data = YamlConfiguration.loadConfiguration(file); - data.set("quest-progress", null); for (QuestProgress questProgress : questProgressValues) { + if (!questProgress.isModified()) continue; data.set("quest-progress." + questProgress.getQuestId() + ".started", questProgress.isStarted()); data.set("quest-progress." + questProgress.getQuestId() + ".completed", questProgress.isCompleted()); data.set("quest-progress." + questProgress.getQuestId() + ".completed-before", questProgress.isCompletedBefore()); diff --git a/src/main/java/com/leonardobishop/quests/util/Options.java b/src/main/java/com/leonardobishop/quests/util/Options.java index 34922f18..0ad0a41d 100644 --- a/src/main/java/com/leonardobishop/quests/util/Options.java +++ b/src/main/java/com/leonardobishop/quests/util/Options.java @@ -25,8 +25,7 @@ public enum Options { GUITITLE_QUEST_CANCEL("options.guinames.quest-cancel"), ALLOW_QUEST_CANCEL("options.allow-quest-cancel"), ALLOW_QUEST_TRACK("options.allow-quest-track"), - SOFT_CLEAN_QUESTSPROGRESSFILE_ON_JOIN("options.soft-clean-questsprogressfile-on-join"), - PUSH_SOFT_CLEAN_TO_DISK("options.tab-completion.push-soft-clean-to-disk"), + VERIFY_QUEST_EXISTS_ON_LOAD("options.verify-quest-exists-on-load"), TAB_COMPLETE_ENABLED("options.tab-completion.enabled"), ERROR_CHECKING_OVERRIDE("options.error-checking.override-errors"), QUEST_AUTOSTART("options.quest-autostart"), diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index b2ab5871..a23c98dd 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -213,12 +213,12 @@ options: quest-autotrack: true # How much quests should log, 0 = errors only, 1 = warnings, 2 = info, 3 = debug verbose-logging-level: 2 - # Automatically clean player's quest progress files when they join. - # These changes will not be reflected to disk. - # Useful if you frequently add and remove quests on a production server. Equivalent of executing /q a moddata clean, without overwriting the file. - soft-clean-questsprogressfile-on-join: false - # The above, but overwriting the file on disk with the cleaned version, so it does not soft clean on every join. - push-soft-clean-to-disk: false + # Verify quests exist when a player's data is loaded - inconsistencies may arise when + # players progress on specific quests and those quests are later removed. The problem is that their progress + # is still kept in the quest progress file, which may lead to issues such as players reaching a quest started + # limit when the quests they had active no longer exist - having this option enabled prevents + # non-existent quests from being loaded + verify-quest-exists-on-load: true performance-tweaking: # The following are measured in server ticks, multiply SECONDS by 20 to get the number of ticks. quest-queue-executor-interval: 1 # how frequently Quests should execute the next check in the completion queue (def=1 - 0.05s) - increase this value if you are struggling with performance quest-autosave-interval: 12000 # how frequently online players data will be autosaved (def=12000 - 10 minutes) -- cgit v1.2.3-70-g09d2 From f3f60e13d8f4d7af64d44f23f7cb33dae7a9ae9c Mon Sep 17 00:00:00 2001 From: LMBishop <13875753+LMBishop@users.noreply.github.com> Date: Tue, 8 Jun 2021 19:11:46 +0100 Subject: Update schema --- .../quests/storage/MySqlStorageProvider.java | 113 ++++++--------------- 1 file changed, 32 insertions(+), 81 deletions(-) (limited to 'src/main/java') diff --git a/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java b/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java index c2b30734..fefd53ba 100644 --- a/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java +++ b/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java @@ -28,24 +28,22 @@ public class MySqlStorageProvider implements StorageProvider { private static final String CREATE_TABLE_QUEST_PROGRESS = "CREATE TABLE IF NOT EXISTS `{prefix}quest_progress` (" + - " `id` INT NOT NULL AUTO_INCREMENT," + " `uuid` VARCHAR(36) NOT NULL," + " `quest_id` VARCHAR(50) NOT NULL," + " `started` BOOL NOT NULL," + " `completed` BOOL NOT NULL," + " `completed_before` BOOL NOT NULL," + " `completion_date` BIGINT NOT NULL," + - " PRIMARY KEY (`id`));"; + " PRIMARY KEY (`uuid`, `quest_id`));"; private static final String CREATE_TABLE_TASK_PROGRESS = "CREATE TABLE IF NOT EXISTS `{prefix}task_progress` (" + - " `id` INT NOT NULL AUTO_INCREMENT," + " `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 (`id`));"; + " PRIMARY KEY (`uuid`, `quest_id`, `task_id`));"; private static final String SELECT_PLAYER_QUEST_PROGRESS = "SELECT quest_id, started, completed, completed_before, completion_date FROM `{prefix}quest_progress` WHERE uuid=?;"; private static final String SELECT_PLAYER_TASK_PROGRESS = @@ -54,14 +52,10 @@ public class MySqlStorageProvider implements StorageProvider { "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 INSERT_PLAYER_QUEST_PROGRESS = - "INSERT INTO `{prefix}quest_progress` (uuid, quest_id, started, completed, completed_before, completion_date) VALUES (?,?,?,?,?,?)"; - private static final String INSERT_PLAYER_TASK_PROGRESS = - "INSERT INTO `{prefix}task_progress` (uuid, quest_id, task_id, completed, progress, data_type) VALUES (?,?,?,?,?,?)"; - private static final String UPDATE_PLAYER_QUEST_PROGRESS = - "UPDATE `{prefix}quest_progress` SET started=?, completed=?, completed_before=?, completion_date=? WHERE uuid=? AND quest_id=?"; - private static final String UPDATE_PLAYER_TASK_PROGRESS = - "UPDATE `{prefix}task_progress` SET completed=?, progress=?, data_type=? WHERE uuid=? AND quest_id=? AND task_id=?"; + private static final String WRITE_PLAYER_QUEST_PROGRESS = + "INSERT INTO `{prefix}quest_progress` (uuid, quest_id, started, completed, completed_before, completion_date) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE started=?, 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 HikariDataSource hikari; private final String prefix; @@ -206,60 +200,26 @@ public class MySqlStorageProvider implements StorageProvider { @Override public void saveProgressFile(UUID uuid, QuestProgressFile questProgressFile) { try (Connection connection = hikari.getConnection()) { - plugin.getQuestsLogger().debug("Getting known entries for player " + uuid); - List knownQuestIds = new ArrayList<>(); - Map> knownTaskIds = new HashMap<>(); - try (PreparedStatement ps = connection.prepareStatement(this.statementProcessor.apply(SELECT_KNOWN_PLAYER_QUEST_PROGRESS))) { - ps.setString(1, uuid.toString()); - - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - knownQuestIds.add(rs.getString(1)); - } - } - } - try (PreparedStatement ps = connection.prepareStatement(this.statementProcessor.apply(SELECT_KNOWN_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); - - knownTaskIds.putIfAbsent(questId, new ArrayList<>()); - knownTaskIds.get(questId).add(taskId); - } - } - } - - try (PreparedStatement insertQuestProgress = connection.prepareStatement(this.statementProcessor.apply(INSERT_PLAYER_QUEST_PROGRESS)); - PreparedStatement insertTaskProgress = connection.prepareStatement(this.statementProcessor.apply(INSERT_PLAYER_TASK_PROGRESS)); - PreparedStatement updateQuestProgress = connection.prepareStatement(this.statementProcessor.apply(UPDATE_PLAYER_QUEST_PROGRESS)); - PreparedStatement updateTaskProgress = connection.prepareStatement(this.statementProcessor.apply(UPDATE_PLAYER_TASK_PROGRESS))) { + try (PreparedStatement writeQuestProgress = connection.prepareStatement(this.statementProcessor.apply(WRITE_PLAYER_QUEST_PROGRESS)); + PreparedStatement writeTaskProgress = connection.prepareStatement(this.statementProcessor.apply(WRITE_PLAYER_TASK_PROGRESS))) { List questProgressValues = new ArrayList<>(questProgressFile.getAllQuestProgress()); for (QuestProgress questProgress : questProgressValues) { if (!questProgress.isModified()) continue; String questId = questProgress.getQuestId(); - if (knownQuestIds.contains(questId)) { - updateQuestProgress.setBoolean(1, questProgress.isStarted()); - updateQuestProgress.setBoolean(2, questProgress.isCompleted()); - updateQuestProgress.setBoolean(3, questProgress.isCompletedBefore()); - updateQuestProgress.setLong(4, questProgress.getCompletionDate()); - updateQuestProgress.setString(5, uuid.toString()); - updateQuestProgress.setString(6, questId); - updateQuestProgress.addBatch(); - } else { - insertQuestProgress.setString(1, uuid.toString()); - insertQuestProgress.setString(2, questProgress.getQuestId()); - insertQuestProgress.setBoolean(3, questProgress.isStarted()); - insertQuestProgress.setBoolean(4, questProgress.isCompleted()); - insertQuestProgress.setBoolean(5, questProgress.isCompletedBefore()); - insertQuestProgress.setLong(6, questProgress.getCompletionDate()); - insertQuestProgress.addBatch(); - } - List taskIds = knownTaskIds.getOrDefault(questProgress.getQuestId(), Collections.emptyList()); + writeQuestProgress.setString(1, uuid.toString()); + writeQuestProgress.setString(2, questProgress.getQuestId()); + writeQuestProgress.setBoolean(3, questProgress.isStarted()); + writeQuestProgress.setBoolean(4, questProgress.isCompleted()); + writeQuestProgress.setBoolean(5, questProgress.isCompletedBefore()); + writeQuestProgress.setLong(6, questProgress.getCompletionDate()); + writeQuestProgress.setBoolean(7, questProgress.isStarted()); + writeQuestProgress.setBoolean(8, questProgress.isCompleted()); + writeQuestProgress.setBoolean(9, questProgress.isCompletedBefore()); + writeQuestProgress.setLong(10, questProgress.getCompletionDate()); + writeQuestProgress.addBatch(); + for (TaskProgress taskProgress : questProgress.getTaskProgress()) { String taskId = taskProgress.getTaskId(); @@ -284,30 +244,21 @@ public class MySqlStorageProvider implements StorageProvider { + " since type " + progress.getClass().getName() + " cannot be encoded!"); continue; } - if (taskIds.contains(taskId)) { - updateTaskProgress.setBoolean(1, taskProgress.isCompleted()); - updateTaskProgress.setString(2, encodedProgress); - updateTaskProgress.setString(3, type); - updateTaskProgress.setString(4, uuid.toString()); - updateTaskProgress.setString(5, questId); - updateTaskProgress.setString(6, taskId); - updateTaskProgress.addBatch(); - } else { - insertTaskProgress.setString(1, uuid.toString()); - insertTaskProgress.setString(2, questId); - insertTaskProgress.setString(3, taskProgress.getTaskId()); - insertTaskProgress.setBoolean(4, taskProgress.isCompleted()); - insertTaskProgress.setString(5, encodedProgress); - insertTaskProgress.setString(6, type); - insertTaskProgress.addBatch(); - } + 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(); } } - insertQuestProgress.executeBatch(); - insertTaskProgress.executeBatch(); - updateQuestProgress.executeBatch(); - updateTaskProgress.executeBatch(); + writeQuestProgress.executeBatch(); + writeTaskProgress.executeBatch(); } } catch (SQLException e) { plugin.getQuestsLogger().severe("Failed to save player: " + uuid + "!"); -- cgit v1.2.3-70-g09d2 From 73cb35b98774ca3668f9fcb49e75e8f3ae2ed56f Mon Sep 17 00:00:00 2001 From: LMBishop <13875753+LMBishop@users.noreply.github.com> Date: Tue, 8 Jun 2021 21:46:06 +0100 Subject: Allow qPlayer to be null --- .../com/leonardobishop/quests/QuestCompleter.java | 3 + .../java/com/leonardobishop/quests/Quests.java | 1 - .../quests/api/QuestsPlaceholders.java | 1 + .../quests/command/QuestsCommand.java | 132 ++++++++++----------- .../quests/listener/PlayerJoinListener.java | 5 +- .../com/leonardobishop/quests/player/QPlayer.java | 11 +- .../quests/player/QPlayerManager.java | 15 ++- .../quest/tasktype/type/MiningCertainTaskType.java | 3 + .../type/dependent/EssentialsBalanceTaskType.java | 3 + .../quests/storage/MySqlStorageProvider.java | 26 ++-- .../com/leonardobishop/quests/util/Messages.java | 1 + src/main/resources/config.yml | 1 + 12 files changed, 117 insertions(+), 85 deletions(-) (limited to 'src/main/java') diff --git a/src/main/java/com/leonardobishop/quests/QuestCompleter.java b/src/main/java/com/leonardobishop/quests/QuestCompleter.java index f31e5daa..d3ff8cf1 100644 --- a/src/main/java/com/leonardobishop/quests/QuestCompleter.java +++ b/src/main/java/com/leonardobishop/quests/QuestCompleter.java @@ -35,6 +35,8 @@ public class QuestCompleter implements Runnable { Player player = Bukkit.getPlayer(questProgress.getPlayer()); if (player != null && player.isOnline()) { QPlayer qPlayer = plugin.getPlayerManager().getPlayer(player.getUniqueId()); + if (qPlayer == null) return; + Quest quest = plugin.getQuestManager().getQuestById(questProgress.getQuestId()); if (!qPlayer.hasStartedQuest(quest)) return; @@ -52,6 +54,7 @@ public class QuestCompleter implements Runnable { Player player = Bukkit.getPlayer(questProgressFile.getPlayerUUID()); if (player != null && player.isOnline()) { QPlayer qPlayer = plugin.getPlayerManager().getPlayer(player.getUniqueId()); + if (qPlayer == null) return; for (QuestProgress questProgress : questProgressFile.getAllQuestProgress()) { Quest quest = plugin.getQuestManager().getQuestById(questProgress.getQuestId()); if (quest == null) continue; diff --git a/src/main/java/com/leonardobishop/quests/Quests.java b/src/main/java/com/leonardobishop/quests/Quests.java index bfbc0e60..6308f9de 100644 --- a/src/main/java/com/leonardobishop/quests/Quests.java +++ b/src/main/java/com/leonardobishop/quests/Quests.java @@ -174,7 +174,6 @@ public class Quests extends JavaPlugin { qPlayerManager = new QPlayerManager(this); menuController = new MenuController(this); - super.getCommand("quests").setExecutor(new QuestsCommand(this)); Bukkit.getPluginManager().registerEvents(new PlayerJoinListener(this), this); Bukkit.getPluginManager().registerEvents(menuController, this); diff --git a/src/main/java/com/leonardobishop/quests/api/QuestsPlaceholders.java b/src/main/java/com/leonardobishop/quests/api/QuestsPlaceholders.java index 9a402ed5..5c82cf6b 100644 --- a/src/main/java/com/leonardobishop/quests/api/QuestsPlaceholders.java +++ b/src/main/java/com/leonardobishop/quests/api/QuestsPlaceholders.java @@ -70,6 +70,7 @@ public class QuestsPlaceholders extends PlaceholderExpansion implements Cacheabl if (save) args = Arrays.copyOf(args, args.length - 1); final QPlayer qPlayer = plugin.getPlayerManager().getPlayer(p.getUniqueId()); + if (qPlayer == null) return "Data not loaded"; String split = args[args.length - 1]; String result = "null"; diff --git a/src/main/java/com/leonardobishop/quests/command/QuestsCommand.java b/src/main/java/com/leonardobishop/quests/command/QuestsCommand.java index d33baa65..d18f977c 100644 --- a/src/main/java/com/leonardobishop/quests/command/QuestsCommand.java +++ b/src/main/java/com/leonardobishop/quests/command/QuestsCommand.java @@ -78,6 +78,10 @@ public class QuestsCommand implements TabExecutor { if (args.length == 0 && sender instanceof Player) { Player player = (Player) sender; QPlayer qPlayer = plugin.getPlayerManager().getPlayer(player.getUniqueId()); + if (qPlayer == null) { + player.sendMessage(Messages.COMMAND_DATA_NOT_LOADED.getMessage()); + return true; + } qPlayer.openQuests(); return true; } else if (args.length >= 1) { @@ -90,10 +94,11 @@ public class QuestsCommand implements TabExecutor { showAdminHelp(sender, "moddata"); return true; } else if (args[1].equalsIgnoreCase("reload")) { + sender.sendMessage(ChatColor.GRAY + "Please note that some options, such as storage, require a full restart for chances to take effect."); plugin.reloadConfig(); plugin.reloadQuests(); - showProblems(sender); - sender.sendMessage(ChatColor.GRAY + "Quests successfully reloaded."); + if (!plugin.getQuestsConfigLoader().getFilesWithProblems().isEmpty()) showProblems(sender); + sender.sendMessage(ChatColor.GREEN + "Quests successfully reloaded."); return true; } else if (args[1].equalsIgnoreCase("config")) { showProblems(sender); @@ -156,6 +161,7 @@ public class QuestsCommand implements TabExecutor { showAdminHelp(sender, "opengui"); return true; } else if (args[1].equalsIgnoreCase("moddata")) { + // TODO remove me if (args[2].equalsIgnoreCase("clean")) { FileVisitor fileVisitor = new SimpleFileVisitor() { @Override @@ -260,36 +266,16 @@ public class QuestsCommand implements TabExecutor { showAdminHelp(sender, "opengui"); return true; } else if (args[1].equalsIgnoreCase("moddata")) { - OfflinePlayer ofp = Bukkit.getOfflinePlayer(args[3]); - UUID uuid; - String name; - // Player.class is a superclass for OfflinePlayer. - // getofflinePlayer return a player regardless if exists or not - if (ofp.hasPlayedBefore()) { - uuid = ofp.getUniqueId(); - name = ofp.getName(); - } else { - sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_PLAYERNOTFOUND.getMessage().replace("{player}", args[3])); - return true; - } + QPlayer qPlayer = getOtherPlayer(sender, args[3]); + if (qPlayer == null) return true; if (args[2].equalsIgnoreCase("fullreset")) { - QPlayer qPlayer = plugin.getPlayerManager().getPlayer(uuid); - if (qPlayer == null) { - sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_LOADDATA.getMessage().replace("{player}", name)); - plugin.getPlayerManager().loadPlayer(uuid); - qPlayer = plugin.getPlayerManager().getPlayer(uuid); //get again - } - if (qPlayer == null) { - sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_NODATA.getMessage().replace("{player}", name)); - return true; - } QuestProgressFile questProgressFile = qPlayer.getQuestProgressFile(); questProgressFile.clear(); - plugin.getPlayerManager().savePlayerSync(uuid, questProgressFile); - if (Bukkit.getPlayer(uuid) == null) { - plugin.getPlayerManager().dropPlayer(uuid); + plugin.getPlayerManager().savePlayerSync(qPlayer.getPlayerUUID(), questProgressFile); + if (Bukkit.getPlayer(qPlayer.getPlayerUUID()) == null) { + plugin.getPlayerManager().dropPlayer(qPlayer.getPlayerUUID()); } - sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_FULLRESET.getMessage().replace("{player}", name)); + sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_FULLRESET.getMessage().replace("{player}", args[3])); return true; } showAdminHelp(sender, "moddata"); @@ -326,26 +312,8 @@ public class QuestsCommand implements TabExecutor { } } else if (args[1].equalsIgnoreCase("moddata")) { boolean success = false; - OfflinePlayer ofp = Bukkit.getOfflinePlayer(args[3]); - UUID uuid; - String name; - if (ofp.hasPlayedBefore()) { - uuid = ofp.getUniqueId(); - name = ofp.getName(); - } else { - sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_PLAYERNOTFOUND.getMessage().replace("{player}", args[3])); - return true; - } - QPlayer qPlayer = plugin.getPlayerManager().getPlayer(uuid); - if (qPlayer == null) { - sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_LOADDATA.getMessage().replace("{player}", name)); - plugin.getPlayerManager().loadPlayer(uuid); - } - if (qPlayer == null) { - sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_NODATA.getMessage().replace("{player}", name)); - success = true; - } - qPlayer = plugin.getPlayerManager().getPlayer(uuid); //get again + QPlayer qPlayer = getOtherPlayer(sender, args[3]); + if (qPlayer == null) return true; QuestProgressFile questProgressFile = qPlayer.getQuestProgressFile(); Quest quest = plugin.getQuestManager().getQuestById(args[4]); if (quest == null) { @@ -355,47 +323,47 @@ public class QuestsCommand implements TabExecutor { } if (args[2].equalsIgnoreCase("reset")) { questProgressFile.generateBlankQuestProgress(quest); - plugin.getPlayerManager().savePlayerSync(uuid, questProgressFile); - sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_RESET_SUCCESS.getMessage().replace("{player}", name).replace("{quest}", quest.getId())); + plugin.getPlayerManager().savePlayerSync(qPlayer.getPlayerUUID(), questProgressFile); + sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_RESET_SUCCESS.getMessage().replace("{player}", args[3]).replace("{quest}", quest.getId())); success = true; } else if (args[2].equalsIgnoreCase("start")) { QuestStartResult response = qPlayer.startQuest(quest); if (response == QuestStartResult.QUEST_LIMIT_REACHED) { - sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_FAILLIMIT.getMessage().replace("{player}", name).replace("{quest}", quest.getId())); + sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_FAILLIMIT.getMessage().replace("{player}", args[3]).replace("{quest}", quest.getId())); return true; } else if (response == QuestStartResult.QUEST_ALREADY_COMPLETED) { - sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_FAILCOMPLETE.getMessage().replace("{player}", name).replace("{quest}", quest.getId())); + sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_FAILCOMPLETE.getMessage().replace("{player}", args[3]).replace("{quest}", quest.getId())); return true; } else if (response == QuestStartResult.QUEST_COOLDOWN) { - sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_FAILCOOLDOWN.getMessage().replace("{player}", name).replace("{quest}", quest.getId())); + sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_FAILCOOLDOWN.getMessage().replace("{player}", args[3]).replace("{quest}", quest.getId())); return true; } else if (response == QuestStartResult.QUEST_LOCKED) { - sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_FAILLOCKED.getMessage().replace("{player}", name).replace("{quest}", quest.getId())); + sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_FAILLOCKED.getMessage().replace("{player}", args[3]).replace("{quest}", quest.getId())); return true; } else if (response == QuestStartResult.QUEST_ALREADY_STARTED) { - sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_FAILSTARTED.getMessage().replace("{player}", name).replace("{quest}", quest.getId())); + sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_FAILSTARTED.getMessage().replace("{player}", args[3]).replace("{quest}", quest.getId())); return true; } else if (response == QuestStartResult.QUEST_NO_PERMISSION) { - sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_FAILPERMISSION.getMessage().replace("{player}", name).replace("{quest}", quest.getId())); + sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_FAILPERMISSION.getMessage().replace("{player}", args[3]).replace("{quest}", quest.getId())); return true; } else if (response == QuestStartResult.NO_PERMISSION_FOR_CATEGORY) { - sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_FAILCATEGORYPERMISSION.getMessage().replace("{player}", name).replace("{quest}", quest.getId())); + sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_FAILCATEGORYPERMISSION.getMessage().replace("{player}", args[3]).replace("{quest}", quest.getId())); return true; } - plugin.getPlayerManager().savePlayerSync(uuid, questProgressFile); - sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_SUCCESS.getMessage().replace("{player}", name).replace("{quest}", quest.getId())); + plugin.getPlayerManager().savePlayerSync(qPlayer.getPlayerUUID(), questProgressFile); + sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_START_SUCCESS.getMessage().replace("{player}", args[3]).replace("{quest}", quest.getId())); success = true; } else if (args[2].equalsIgnoreCase("complete")) { qPlayer.completeQuest(quest); - plugin.getPlayerManager().savePlayerSync(uuid, questProgressFile); - sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_COMPLETE_SUCCESS.getMessage().replace("{player}", name).replace("{quest}", quest.getId())); + plugin.getPlayerManager().savePlayerSync(qPlayer.getPlayerUUID(), questProgressFile); + sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_COMPLETE_SUCCESS.getMessage().replace("{player}", args[3]).replace("{quest}", quest.getId())); success = true; } if (!success) { showAdminHelp(sender, "moddata"); } - if (Bukkit.getPlayer(uuid) == null) { - plugin.getPlayerManager().dropPlayer(uuid); + if (Bukkit.getPlayer(qPlayer.getPlayerUUID()) == null) { + plugin.getPlayerManager().dropPlayer(qPlayer.getPlayerUUID()); } return true; } @@ -409,7 +377,7 @@ public class QuestsCommand implements TabExecutor { Quest quest = plugin.getQuestManager().getQuestById(args[1]); QPlayer qPlayer = plugin.getPlayerManager().getPlayer(player.getUniqueId()); if (qPlayer == null) { - sender.sendMessage(ChatColor.RED + "Your quest progress file has not been loaded yet."); + player.sendMessage(Messages.COMMAND_DATA_NOT_LOADED.getMessage()); return true; } if (quest == null) { @@ -434,10 +402,14 @@ public class QuestsCommand implements TabExecutor { Player player = (Player) sender; if (args.length >= 2) { Category category = plugin.getQuestManager().getCategoryById(args[1]); + QPlayer qPlayer = plugin.getPlayerManager().getPlayer(player.getUniqueId()); + if (qPlayer == null) { + player.sendMessage(Messages.COMMAND_DATA_NOT_LOADED.getMessage()); + return true; + } if (category == null) { sender.sendMessage(Messages.COMMAND_CATEGORY_OPEN_DOESNTEXIST.getMessage().replace("{category}", args[1])); } else { - QPlayer qPlayer = plugin.getPlayerManager().getPlayer(player.getUniqueId()); qPlayer.openCategory(category, null, false); return true; } @@ -447,7 +419,7 @@ public class QuestsCommand implements TabExecutor { Player player = (Player) sender; QPlayer qPlayer = plugin.getPlayerManager().getPlayer(player.getUniqueId()); if (qPlayer == null) { - sender.sendMessage(ChatColor.RED + "Your quest progress file has not been loaded yet."); + player.sendMessage(Messages.COMMAND_DATA_NOT_LOADED.getMessage()); return true; } List validQuests = new ArrayList<>(); @@ -467,6 +439,10 @@ public class QuestsCommand implements TabExecutor { } else if (sender instanceof Player && (args[0].equalsIgnoreCase("started"))) { Player player = (Player) sender; QPlayer qPlayer = plugin.getPlayerManager().getPlayer(player.getUniqueId()); + if (qPlayer == null) { + player.sendMessage(Messages.COMMAND_DATA_NOT_LOADED.getMessage()); + return true; + } qPlayer.openStartedQuests(); return true; } @@ -477,6 +453,30 @@ public class QuestsCommand implements TabExecutor { return true; } + private QPlayer getOtherPlayer(CommandSender sender, String name) { + OfflinePlayer ofp = Bukkit.getOfflinePlayer(name); + UUID uuid; + String username; + if (ofp.hasPlayedBefore()) { + uuid = ofp.getUniqueId(); + username = ofp.getName(); + } else { + sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_PLAYERNOTFOUND.getMessage().replace("{player}", name)); + return null; + } + QPlayer qPlayer = plugin.getPlayerManager().getPlayer(uuid); + if (qPlayer == null) { + sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_LOADDATA.getMessage().replace("{player}", username)); + plugin.getPlayerManager().loadPlayer(uuid); + qPlayer = plugin.getPlayerManager().getPlayer(uuid); + } + if (qPlayer == null) { + sender.sendMessage(Messages.COMMAND_QUEST_ADMIN_NODATA.getMessage().replace("{player}", username)); + return null; + } + return qPlayer; + } + private void showProblems(CommandSender sender) { if (!plugin.getQuestsConfigLoader().getFilesWithProblems().isEmpty()) { // sender.sendMessage(ChatColor.DARK_GRAY.toString() + "----"); diff --git a/src/main/java/com/leonardobishop/quests/listener/PlayerJoinListener.java b/src/main/java/com/leonardobishop/quests/listener/PlayerJoinListener.java index 5cdb465e..53916c32 100644 --- a/src/main/java/com/leonardobishop/quests/listener/PlayerJoinListener.java +++ b/src/main/java/com/leonardobishop/quests/listener/PlayerJoinListener.java @@ -38,8 +38,11 @@ public class PlayerJoinListener implements Listener { Bukkit.getScheduler().runTaskLater(this.plugin, () -> event.getPlayer().sendMessage(plugin.getUpdater().getMessage()), 50L); } + QPlayer qPlayer = plugin.getPlayerManager().getPlayer(playerUuid); + if (qPlayer == null) return; + // run a full check to check for any missed quest completions - plugin.getQuestCompleter().queueFullCheck(plugin.getPlayerManager().getPlayer(playerUuid).getQuestProgressFile()); + plugin.getQuestCompleter().queueFullCheck(qPlayer.getQuestProgressFile()); } } diff --git a/src/main/java/com/leonardobishop/quests/player/QPlayer.java b/src/main/java/com/leonardobishop/quests/player/QPlayer.java index 872b02d2..4e04566b 100644 --- a/src/main/java/com/leonardobishop/quests/player/QPlayer.java +++ b/src/main/java/com/leonardobishop/quests/player/QPlayer.java @@ -78,10 +78,9 @@ public class QPlayer { } Player player = Bukkit.getPlayer(uuid); if (player != null) { - QPlayer questPlayer = QuestsAPI.getPlayerManager().getPlayer(uuid); String questFinishMessage = Messages.QUEST_COMPLETE.getMessage().replace("{quest}", quest.getDisplayNameStripped()); // PlayerFinishQuestEvent -- start - PlayerFinishQuestEvent questFinishEvent = new PlayerFinishQuestEvent(player, questPlayer, questProgress, questFinishMessage); + PlayerFinishQuestEvent questFinishEvent = new PlayerFinishQuestEvent(player, this, questProgress, questFinishMessage); Bukkit.getPluginManager().callEvent(questFinishEvent); // PlayerFinishQuestEvent -- end Bukkit.getServer().getScheduler().runTask(plugin, () -> { @@ -375,10 +374,10 @@ public class QPlayer { } if (Options.CATEGORIES_ENABLED.getBooleanValue()) { - CategoryQMenu categoryQMenu = new CategoryQMenu(plugin, plugin.getPlayerManager().getPlayer(player.getUniqueId())); + CategoryQMenu categoryQMenu = new CategoryQMenu(plugin, this); List questMenus = new ArrayList<>(); for (Category category : plugin.getQuestManager().getCategories()) { - QuestQMenu questQMenu = new QuestQMenu(plugin, plugin.getPlayerManager().getPlayer(player.getUniqueId()), category.getId(), categoryQMenu); + QuestQMenu questQMenu = new QuestQMenu(plugin, this, category.getId(), categoryQMenu); List quests = new ArrayList<>(); for (String questid : category.getRegisteredQuestIds()) { Quest quest = plugin.getQuestManager().getQuestById(questid); @@ -393,7 +392,7 @@ public class QPlayer { plugin.getMenuController().openMenu(player, categoryQMenu, 1); } else { - QuestQMenu questQMenu = new QuestQMenu(plugin, plugin.getPlayerManager().getPlayer(player.getUniqueId()), "", null); + QuestQMenu questQMenu = new QuestQMenu(plugin, this, "", null); List quests = new ArrayList<>(); for (Map.Entry entry : plugin.getQuestManager().getQuests().entrySet()) { quests.add(entry.getValue()); @@ -414,7 +413,7 @@ public class QPlayer { return; } - StartedQMenu startedQMenu = new StartedQMenu(plugin, plugin.getPlayerManager().getPlayer(player.getUniqueId())); + StartedQMenu startedQMenu = new StartedQMenu(plugin, this); List quests = new ArrayList<>(); for (Map.Entry entry : plugin.getQuestManager().getQuests().entrySet()) { quests.add(new QuestSortWrapper(plugin, entry.getValue())); diff --git a/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java b/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java index 2cd9cbfd..41c704e3 100644 --- a/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java +++ b/src/main/java/com/leonardobishop/quests/player/QPlayerManager.java @@ -36,7 +36,11 @@ public class QPlayerManager { plugin.getQuestsLogger().warning("No valid storage provider is configured - Quests will use YAML storage as a default"); this.storageProvider = new YamlStorageProvider(plugin); } - storageProvider.init(); + try { + storageProvider.init(); + } catch (Exception ignored) { + plugin.getQuestsLogger().severe("An error occurred initialising the storage provider."); + } } private final Map qPlayers = new ConcurrentHashMap<>(); @@ -78,7 +82,9 @@ public class QPlayerManager { * @param uuid the uuid of the player */ public void savePlayer(UUID uuid) { - savePlayer(uuid, getPlayer(uuid).getQuestProgressFile()); + QPlayer qPlayer = getPlayer(uuid); + if (qPlayer == null) return; + savePlayer(uuid, qPlayer.getQuestProgressFile()); } /** @@ -101,7 +107,9 @@ public class QPlayerManager { * @param uuid the uuid of the player */ public void savePlayerSync(UUID uuid) { - savePlayerSync(uuid, getPlayer(uuid).getQuestProgressFile()); + QPlayer qPlayer = getPlayer(uuid); + if (qPlayer == null) return; + savePlayerSync(uuid, qPlayer.getQuestProgressFile()); } /** @@ -144,6 +152,7 @@ public class QPlayerManager { plugin.getQuestsLogger().debug("Loading player " + uuid + ". Main thread: " + Bukkit.isPrimaryThread()); qPlayers.computeIfAbsent(uuid, s -> { QuestProgressFile questProgressFile = storageProvider.loadProgressFile(uuid); + if (questProgressFile == null) return null; return new QPlayer(uuid, questProgressFile, new QPlayerPreferences(null), plugin); }); } diff --git a/src/main/java/com/leonardobishop/quests/quest/tasktype/type/MiningCertainTaskType.java b/src/main/java/com/leonardobishop/quests/quest/tasktype/type/MiningCertainTaskType.java index da497808..125bfecc 100644 --- a/src/main/java/com/leonardobishop/quests/quest/tasktype/type/MiningCertainTaskType.java +++ b/src/main/java/com/leonardobishop/quests/quest/tasktype/type/MiningCertainTaskType.java @@ -127,6 +127,9 @@ public final class MiningCertainTaskType extends TaskType { if (event.getPlayer().hasMetadata("NPC")) return; QPlayer qPlayer = QuestsAPI.getPlayerManager().getPlayer(event.getPlayer().getUniqueId()); + if (qPlayer == null) { + return; + } for (Quest quest : super.getRegisteredQuests()) { if (qPlayer.hasStartedQuest(quest)) { diff --git a/src/main/java/com/leonardobishop/quests/quest/tasktype/type/dependent/EssentialsBalanceTaskType.java b/src/main/java/com/leonardobishop/quests/quest/tasktype/type/dependent/EssentialsBalanceTaskType.java index 581cbafb..22cef8a2 100644 --- a/src/main/java/com/leonardobishop/quests/quest/tasktype/type/dependent/EssentialsBalanceTaskType.java +++ b/src/main/java/com/leonardobishop/quests/quest/tasktype/type/dependent/EssentialsBalanceTaskType.java @@ -47,6 +47,9 @@ public class EssentialsBalanceTaskType extends TaskType { Essentials ess = (Essentials) Bukkit.getPluginManager().getPlugin("Essentials"); if (player != null && player.isOnline() && ess != null) { QPlayer qPlayer = QuestsAPI.getPlayerManager().getPlayer(playerUUID); + if (qPlayer == null) { + return; + } QuestProgressFile questProgressFile = qPlayer.getQuestProgressFile(); QuestProgress questProgress = questProgressFile.getQuestProgress(quest); TaskProgress taskProgress = questProgress.getTaskProgress(task.getId()); diff --git a/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java b/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java index fefd53ba..02692919 100644 --- a/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java +++ b/src/main/java/com/leonardobishop/quests/storage/MySqlStorageProvider.java @@ -57,17 +57,23 @@ public class MySqlStorageProvider implements StorageProvider { 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 HikariDataSource hikari; - private final String prefix; + private final ConfigurationSection configuration; private final Quests plugin; - private final Function statementProcessor; + private HikariDataSource hikari; + private String prefix; + private Function statementProcessor; + private boolean fault; public MySqlStorageProvider(Quests plugin, ConfigurationSection configuration) { this.plugin = plugin; if (configuration == null) { configuration = new YamlConfiguration(); } + this.configuration = configuration; + } + @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; @@ -94,13 +100,14 @@ public class MySqlStorageProvider implements StorageProvider { config.addDataSourceProperty("elideSetAutoCommits", true); config.addDataSourceProperty("maintainTimeStats", false); - this.hikari = new HikariDataSource(config); + try { + this.hikari = new HikariDataSource(config); + } catch (Exception e) { + e.printStackTrace(); + fault = true; + } this.prefix = configuration.getString("database-settings.table-prefix", "quests_"); this.statementProcessor = s -> s.replace("{prefix}", prefix); - } - - @Override - public void init() { try (Connection connection = hikari.getConnection()) { try (Statement s = connection.createStatement()) { plugin.getQuestsLogger().debug("Creating default tables"); @@ -121,6 +128,7 @@ public class MySqlStorageProvider implements StorageProvider { @Override public QuestProgressFile loadProgressFile(UUID uuid) { + if (fault) return null; QuestProgressFile questProgressFile = new QuestProgressFile(uuid, plugin); try (Connection connection = hikari.getConnection()) { plugin.getQuestsLogger().debug("Querying player " + uuid); @@ -193,12 +201,14 @@ public class MySqlStorageProvider implements StorageProvider { } catch (SQLException e) { plugin.getQuestsLogger().severe("Failed to load player: " + uuid + "!"); e.printStackTrace(); + return null; } return questProgressFile; } @Override public void saveProgressFile(UUID uuid, QuestProgressFile questProgressFile) { + if (fault) return; 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))) { diff --git a/src/main/java/com/leonardobishop/quests/util/Messages.java b/src/main/java/com/leonardobishop/quests/util/Messages.java index ba9e42da..a86a4144 100644 --- a/src/main/java/com/leonardobishop/quests/util/Messages.java +++ b/src/main/java/com/leonardobishop/quests/util/Messages.java @@ -22,6 +22,7 @@ public enum Messages { QUEST_CATEGORY_PERMISSION("messages.quest-category-permission"), QUEST_CANCEL_NOTSTARTED("messages.quest-cancel-notstarted"), QUEST_UPDATER("messages.quest-updater"), + COMMAND_DATA_NOT_LOADED("messages.command-data-not-loaded"), COMMAND_SUB_DOESNTEXIST("messages.command-sub-doesntexist"), COMMAND_QUEST_START_DOESNTEXIST("messages.command-quest-start-doesntexist"), COMMAND_QUEST_GENERAL_DOESNTEXIST("messages.command-quest-general-doesntexist"), diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 4b8a4f2b..2c2e09c7 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -334,6 +334,7 @@ messages: quest-category-quest-permission: "&7You do not have permission to start this quest since it is in a category you do not have permission to view." quest-cancel-notstarted: "&7You have not started this quest." quest-updater: "&cQuests > &7A new version &c{newver} &7was found on Spigot (your version: &c{oldver}&7). Please update me! <3 - Link: {link}" + command-data-not-loaded: "&4Your quests progress file has not been loaded; you cannot use quests. If this issue persists, contact an admin." command-sub-doesntexist: "&7The specified subcommand '&c{sub}' &7does not exist." command-quest-start-doesntexist: "&7The specified quest '&c{quest}&7' does not exist." command-quest-general-doesntexist: "&7The specified quest '&c{quest}&7' does not exist." -- cgit v1.2.3-70-g09d2