From 2ae7ac3185304a2692010788ea02e7e98a905b9b Mon Sep 17 00:00:00 2001 From: rooooose-b <142353909+rooooose-b@users.noreply.github.com> Date: Wed, 6 Mar 2024 22:52:32 +0100 Subject: [PATCH] Revert "fix: Delete src/main/java/io/github/Alathra directory" This reverts commit 9d9f1165bb037a355a7d9572e9914b6bca947f69. --- .../Alathra/AlathraSkills/AlathraSkills.java | 117 +++++++ .../Alathra/AlathraSkills/Reloadable.java | 21 ++ .../AlathraSkills/api/SkillsManager.java | 50 +++ .../api/SkillsPlayerManager.java | 34 ++ .../AlathraSkills/command/CommandHandler.java | 47 +++ .../AlathraSkills/command/ExampleCommand.java | 25 ++ .../AlathraSkills/config/ConfigHandler.java | 47 +++ .../AlathraSkills/db/DatabaseHandler.java | 303 ++++++++++++++++++ .../AlathraSkills/db/DatabaseQueries.java | 256 +++++++++++++++ .../AlathraSkills/db/DatabaseType.java | 287 +++++++++++++++++ .../db/flyway/DatabaseMigrationException.java | 7 + .../db/flyway/DatabaseMigrationHandler.java | 101 ++++++ .../db/flyway/FlywayMigrationsProvider.java | 20 ++ .../AlathraSkills/db/jooq/JooqContext.java | 92 ++++++ .../Alathra/AlathraSkills/gui/GuiHelper.java | 53 +++ .../gui/main/PopulateBorders.java | 16 + .../gui/main/PopulateButtons.java | 11 + .../gui/main/PopulateContent.java | 11 + .../gui/skill/PopulateBorders.java | 16 + .../gui/skill/PopulateButtons.java | 11 + .../gui/skill/PopulateContent.java | 11 + .../AlathraSkills/hooks/VaultHook.java | 150 +++++++++ .../Alathra/AlathraSkills/skills/Skill.java | 75 +++++ .../AlathraSkills/skills/SkillCategory.java | 51 +++ .../AlathraSkills/skills/SkillsPlayer.java | 73 +++++ .../categories/FarmingSkillCategory.java | 24 ++ .../categories/MiningSkillCategory.java | 25 ++ .../categories/WoodcuttingSkillCategory.java | 25 ++ .../Alathra/AlathraSkills/utility/Cfg.java | 19 ++ .../Alathra/AlathraSkills/utility/DB.java | 39 +++ .../Alathra/AlathraSkills/utility/Logger.java | 21 ++ 31 files changed, 2038 insertions(+) create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/AlathraSkills.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/Reloadable.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/api/SkillsManager.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/api/SkillsPlayerManager.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/command/CommandHandler.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/command/ExampleCommand.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/config/ConfigHandler.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/db/DatabaseHandler.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/db/DatabaseQueries.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/db/DatabaseType.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/db/flyway/DatabaseMigrationException.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/db/flyway/DatabaseMigrationHandler.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/db/flyway/FlywayMigrationsProvider.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/db/jooq/JooqContext.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/gui/GuiHelper.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/gui/main/PopulateBorders.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/gui/main/PopulateButtons.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/gui/main/PopulateContent.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/gui/skill/PopulateBorders.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/gui/skill/PopulateButtons.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/gui/skill/PopulateContent.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/hooks/VaultHook.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/skills/Skill.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/skills/SkillCategory.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/skills/SkillsPlayer.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/skills/categories/FarmingSkillCategory.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/skills/categories/MiningSkillCategory.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/skills/categories/WoodcuttingSkillCategory.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/utility/Cfg.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/utility/DB.java create mode 100644 src/main/java/io/github/Alathra/AlathraSkills/utility/Logger.java diff --git a/src/main/java/io/github/Alathra/AlathraSkills/AlathraSkills.java b/src/main/java/io/github/Alathra/AlathraSkills/AlathraSkills.java new file mode 100644 index 0000000..3eee9e5 --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/AlathraSkills.java @@ -0,0 +1,117 @@ +package io.github.alathra.alathraskills; + +import com.github.milkdrinkers.colorparser.ColorParser; + +import io.github.alathra.alathraskills.api.SkillsManager; +import io.github.alathra.alathraskills.api.SkillsPlayerManager; +import io.github.alathra.alathraskills.command.CommandHandler; +import io.github.alathra.alathraskills.config.ConfigHandler; +import io.github.alathra.alathraskills.db.DatabaseHandler; +import io.github.alathra.alathraskills.db.testing.TestGetExerienceCommand; +import io.github.alathra.alathraskills.hooks.VaultHook; +import io.github.alathra.alathraskills.listeners.ListenerHandler; +import io.github.alathra.alathraskills.utility.Logger; + +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +public class AlathraSkills extends JavaPlugin { + private static AlathraSkills instance; + private ConfigHandler configHandler; + private DatabaseHandler databaseHandler; + private CommandHandler commandHandler; + private ListenerHandler listenerHandler; + private static VaultHook vaultHook; + + // Internal managers + private static SkillsManager skillsManager; + private static SkillsPlayerManager skillsPlayerManager; + + public static AlathraSkills getInstance() { + return instance; + } + + @Override + public void onLoad() { + instance = this; + configHandler = new ConfigHandler(instance); + databaseHandler = new DatabaseHandler(instance); + commandHandler = new CommandHandler(instance); + listenerHandler = new ListenerHandler(instance); + vaultHook = new VaultHook(instance); + skillsManager = new SkillsManager(instance); + skillsPlayerManager = new SkillsPlayerManager(instance); + + configHandler.onLoad(); + databaseHandler.onLoad(); + commandHandler.onLoad(); + listenerHandler.onLoad(); + vaultHook.onLoad(); + skillsManager.onLoad(); + skillsPlayerManager.onLoad(); + } + + @Override + public void onEnable() { + configHandler.onEnable(); + databaseHandler.onEnable(); + commandHandler.onEnable(); + listenerHandler.onEnable(); + vaultHook.onEnable(); + skillsManager.onEnable(); + skillsPlayerManager.onEnable(); + + if (vaultHook.isVaultLoaded()) { + Logger.get().info(ColorParser.of("Vault has been found on this server. Vault support enabled.").build()); + } else { + Logger.get().warn(ColorParser.of("Vault is not installed on this server. Vault support has been disabled.").build()); + } + + } + + @Override + public void onDisable() { + configHandler.onDisable(); + databaseHandler.onDisable(); + commandHandler.onDisable(); + listenerHandler.onDisable(); + vaultHook.onDisable(); + skillsManager.onDisable(); + skillsPlayerManager.onDisable(); + } + + /** + * Gets data handler. + * + * @return the data handler + */ + @NotNull + public DatabaseHandler getDataHandler() { + return databaseHandler; + } + + /** + * Gets config handler. + * + * @return the config handler + */ + @NotNull + public ConfigHandler getConfigHandler() { + return configHandler; + } + + /** + * Gets vault hook. + * + * @return the vault hook + */ + @NotNull + public static VaultHook getVaultHook() { + return vaultHook; + } + + @NotNull + public static SkillsManager getSkillsManager() { + return skillsManager; + } +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/Reloadable.java b/src/main/java/io/github/Alathra/AlathraSkills/Reloadable.java new file mode 100644 index 0000000..20aa8bc --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/Reloadable.java @@ -0,0 +1,21 @@ +package io.github.alathra.alathraskills; + +/** + * Implemented in classes that should support being reloaded IE executing the methods during runtime after startup. + */ +public interface Reloadable { + /** + * On plugin load. + */ + void onLoad(); + + /** + * On plugin enable. + */ + void onEnable(); + + /** + * On plugin disable. + */ + void onDisable(); +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/api/SkillsManager.java b/src/main/java/io/github/Alathra/AlathraSkills/api/SkillsManager.java new file mode 100644 index 0000000..1d1f7b0 --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/api/SkillsManager.java @@ -0,0 +1,50 @@ +package io.github.alathra.alathraskills.api; + +import java.util.HashMap; + +import io.github.alathra.alathraskills.AlathraSkills; +import io.github.alathra.alathraskills.Reloadable; +import io.github.alathra.alathraskills.skills.SkillCategory; +import io.github.alathra.alathraskills.skills.categories.FarmingSkillCategory; +import io.github.alathra.alathraskills.skills.categories.MiningSkillCategory; +import io.github.alathra.alathraskills.skills.categories.WoodcuttingSkillCategory; + +public class SkillsManager implements Reloadable { + + public static int FARMING_SKILL_ID = 1; + public static int MINING_SKILL_ID = 2; + public static int WOODCUTTING_SKILL_ID = 3; + + private final AlathraSkills plugin; + + // Id, SkillCategory + public HashMap skillCategories = new HashMap<>(); + + public SkillsManager(AlathraSkills plugin) { + this.plugin = plugin; + } + + @Override + public void onLoad() { + // TODO Auto-generated method stub + + } + + @Override + public void onEnable() { + loadSkillCategories(); + } + + @Override + public void onDisable() { + // TODO Auto-generated method stub + + } + + public void loadSkillCategories() { + skillCategories.put(1, new FarmingSkillCategory(1)); + skillCategories.put(2, new MiningSkillCategory(2)); + skillCategories.put(3, new WoodcuttingSkillCategory(3)); + } + +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/api/SkillsPlayerManager.java b/src/main/java/io/github/Alathra/AlathraSkills/api/SkillsPlayerManager.java new file mode 100644 index 0000000..43ccc5e --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/api/SkillsPlayerManager.java @@ -0,0 +1,34 @@ +package io.github.alathra.alathraskills.api; + +import io.github.alathra.alathraskills.AlathraSkills; +import io.github.alathra.alathraskills.Reloadable; + +public class SkillsPlayerManager implements Reloadable { + + private final AlathraSkills plugin; + + public SkillsPlayerManager(AlathraSkills plugin) { + this.plugin = plugin; + } + + @Override + public void onLoad() { + // TODO Auto-generated method stub + + } + + @Override + public void onEnable() { + // TODO Auto-generated method stub + + } + + @Override + public void onDisable() { + // TODO Auto-generated method stub + + } + //TODO + // https://github.com/Rumsfield/konquest/blob/main/api/src/main/java/com/github/rumsfield/konquest/api/manager/KonquestPlayerManager.java + +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/command/CommandHandler.java b/src/main/java/io/github/Alathra/AlathraSkills/command/CommandHandler.java new file mode 100644 index 0000000..1e36196 --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/command/CommandHandler.java @@ -0,0 +1,47 @@ +package io.github.alathra.alathraskills.command; + +import io.github.alathra.alathraskills.AlathraSkills; +import io.github.alathra.alathraskills.Reloadable; +import io.github.alathra.alathraskills.db.testing.TestDeleteAllSkillsCommand; +import io.github.alathra.alathraskills.db.testing.TestGetAllSkillsCommand; +import io.github.alathra.alathraskills.db.testing.TestGetExerienceCommand; +import io.github.alathra.alathraskills.db.testing.TestHasSkillCommand; +import io.github.alathra.alathraskills.db.testing.TestSetExerienceCommand; +import io.github.alathra.alathraskills.db.testing.TestSetSkillCommand; +import dev.jorel.commandapi.CommandAPI; +import dev.jorel.commandapi.CommandAPIBukkitConfig; + +/** + * A class to handle registration of commands. + */ +public class CommandHandler implements Reloadable { + private final AlathraSkills plugin; + + public CommandHandler(AlathraSkills plugin) { + this.plugin = plugin; + } + + @Override + public void onLoad() { + CommandAPI.onLoad(new CommandAPIBukkitConfig(plugin).shouldHookPaperReload(true).silentLogs(true)); + } + + @Override + public void onEnable() { + CommandAPI.onEnable(); + + // Register commands here + new TestGetExerienceCommand(); + new TestSetExerienceCommand(); + new TestDeleteAllSkillsCommand(); + new TestGetAllSkillsCommand(); + new TestSetSkillCommand(); + new TestHasSkillCommand(); + new ExampleCommand(); + } + + @Override + public void onDisable() { + CommandAPI.onDisable(); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/Alathra/AlathraSkills/command/ExampleCommand.java b/src/main/java/io/github/Alathra/AlathraSkills/command/ExampleCommand.java new file mode 100644 index 0000000..c28a9af --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/command/ExampleCommand.java @@ -0,0 +1,25 @@ +package io.github.alathra.alathraskills.command; + +import com.github.milkdrinkers.colorparser.ColorParser; +import dev.jorel.commandapi.CommandAPICommand; +import dev.jorel.commandapi.executors.CommandArguments; +import org.bukkit.command.CommandSender; + +public class ExampleCommand { + public ExampleCommand() { + new CommandAPICommand("example") + .withFullDescription("Example command.") + .withShortDescription("Example command.") + .withPermission("example.command") + .executes(this::example) + .register(); + } + + private void example(CommandSender sender, CommandArguments args) { + sender.sendMessage( + ColorParser.of("Read more about CommandAPI &9here.") + .parseLegacy() // Parse legacy color codes + .build() + ); + } +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/config/ConfigHandler.java b/src/main/java/io/github/Alathra/AlathraSkills/config/ConfigHandler.java new file mode 100644 index 0000000..d9eb10f --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/config/ConfigHandler.java @@ -0,0 +1,47 @@ +package io.github.alathra.alathraskills.config; + +import io.github.alathra.alathraskills.AlathraSkills; +import io.github.alathra.alathraskills.Reloadable; +import com.github.milkdrinkers.Crate.Config; + +import javax.inject.Singleton; + +/** + * A class that generates/loads & provides access to a configuration file. + */ +@Singleton +public class ConfigHandler implements Reloadable { + private final AlathraSkills plugin; + private Config cfg; + + /** + * Instantiates a new Config handler. + * + * @param plugin the plugin instance + */ + public ConfigHandler(AlathraSkills plugin) { + this.plugin = plugin; + } + + @Override + public void onLoad() { + cfg = new Config("config", plugin.getDataFolder().getPath(), plugin.getResource("config.yml")); // Create a config file from the template in our resources folder + } + + @Override + public void onEnable() { + } + + @Override + public void onDisable() { + } + + /** + * Gets examplePlugin config object. + * + * @return the examplePlugin config object + */ + public Config getConfig() { + return cfg; + } +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/db/DatabaseHandler.java b/src/main/java/io/github/Alathra/AlathraSkills/db/DatabaseHandler.java new file mode 100644 index 0000000..ca3634e --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/db/DatabaseHandler.java @@ -0,0 +1,303 @@ +package io.github.alathra.alathraskills.db; + +import io.github.alathra.alathraskills.AlathraSkills; +import io.github.alathra.alathraskills.Reloadable; +import io.github.alathra.alathraskills.db.flyway.DatabaseMigrationException; +import io.github.alathra.alathraskills.db.flyway.DatabaseMigrationHandler; +import io.github.alathra.alathraskills.db.jooq.JooqContext; +import io.github.alathra.alathraskills.utility.Cfg; +import io.github.alathra.alathraskills.utility.Logger; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.jetbrains.annotations.NotNull; + +import javax.inject.Singleton; +import java.io.File; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Map; + +/** + * Class that handles HikariCP connection pool, jOOQ and Flyway migrations. + */ +@Singleton +public class DatabaseHandler implements Reloadable { + private final AlathraSkills plugin; + private boolean isConnected = false; + private HikariDataSource hikariDataSource; + private DatabaseType database; + private JooqContext jooqContext; + + /** + * Instantiates a new Data handler. + * + * @param plugin the plugin instance + */ + public DatabaseHandler(AlathraSkills plugin) { + this.plugin = plugin; + } + + /** + * On plugin load. + */ + @Override + public void onLoad() { + try { + openConnection(); + isConnected = true; + } catch (Exception e) { + Logger.get().error("[DB] Database initialization error: ", e); + } finally { + if (!isConnected()) { + Logger.get().warn("[DB] Failed to initialize database connection. Functionality will be limited."); + } + } + } + + /** + * On plugin enable. + */ + @Override + public void onEnable() { + } + + /** + * On plugin disable. + */ + @Override + public void onDisable() { + try { + closeDatabaseConnection(); + isConnected = false; + } catch (Exception e) { + Logger.get().error("[DB] Error closing database connections:", e); + } + } + + /** + * Returns if the database is setup and functioning properly. + * + * @return the boolean + */ + public boolean isConnected() { + return isConnected; + } + + /** + * Gets db. + * + * @return the db + */ + public DatabaseType getDB() { + return database; + } + + /** + * Gets jooq context. + * + * @return the jooq context + */ + public JooqContext getJooqContext() { + return jooqContext; + } + + /** + * Gets a connection from the connection pool. + * + * @return the connection + * @throws SQLException the sql exception + */ + @NotNull + public Connection getConnection() throws SQLException { + if (hikariDataSource == null) + throw new SQLException("[DB] Unable to getConnection a connection from the pool. (HikariDataSource returned null)"); + + final Connection connection = hikariDataSource.getConnection(); + if (connection == null) + throw new SQLException("[DB] Unable to getConnection a connection from the pool. (HikariDataSource#getConnection returned null)"); + + return connection; + } + + /** + * Creates a connection pool using HikariCP and executes Flyway migrations. + */ + private void openConnection() { + if (hikariDataSource != null) { + Logger.get().warn("[DB] Attempted to create a new database pool while running!"); + return; + } + + final HikariConfig hikariConfig = new HikariConfig(); + final String vendorName = Cfg.get().get("db.type", DatabaseType.HSQLDB.getDriverName()); + final DatabaseType db = DatabaseType.getDatabaseTypeFromJdbcPrefix(vendorName); + + if (db == null) { + Logger.get().warn("[DB] Invalid database type was specified!"); + return; + } + + final String host = Cfg.get().get("db.host", "127.0.0.1"); + final String port = Cfg.get().get("db.port", "3306"); + final String database = Cfg.get().get("db.database", "database"); + final String username = Cfg.get().get("db.user", "root"); + final String password = Cfg.get().get("db.pass", ""); + + final String connectionProperties = switch (db) { + case H2 -> DatabaseType.H2.formatJdbcConnectionProperties( + Map.of( + "AUTO_SERVER", "TRUE", + "MODE", "MySQL", // MySQL support mode + "CASE_INSENSITIVE_IDENTIFIERS", "TRUE", + "IGNORECASE", "TRUE" + ) + ); + case HSQLDB -> DatabaseType.HSQLDB.formatJdbcConnectionProperties( + Map.of( + "sql.syntax_mys", "true", // MySQL support mode + // Prevent execution of multiple queries in one Statement + "sql.restrict_exec", true, + // Make the names of generated indexes the same as the names of the constraints + "sql.sys_index_names", true, + /* + * Enforce SQL standards on + * 1.) table and column names + * 2.) ambiguous column references + * 3.) illegal type conversions + */ + "sql.enforce_names", true, + "sql.enforce_refs", true, + "sql.enforce_types", true, + // Respect interrupt status during query execution + "hsqldb.tx_interrupt_rollback", true, + // Use CACHED tables by default + "hsqldb.default_table_type", "cached", + // Needed for use with connection init-SQL (hikariConf.setConnectionInitSql) + "allowMultiQueries", true, + // Help debug in case of exceptions + "dumpQueriesOnException", true + ) + ); + case MARIADB -> DatabaseType.MARIADB.formatJdbcConnectionProperties( + Map.of( + // Performance improvements + "autocommit", false, + "defaultFetchSize", 1000, + + // Help debug in case of deadlock + "includeInnodbStatusInDeadlockExceptions", true, + "includeThreadDumpInDeadlockExceptions", true, + + // https://github.com/brettwooldridge/HikariCP/wiki/Rapid-Recovery#mysql + "socketTimeout", 14000L, + // Needed for use with connection init-SQL (hikariConf.setConnectionInitSql) + "allowMultiQueries", true, + // Help debug in case of exceptions + "dumpQueriesOnException", true + ) + ); + case MYSQL -> DatabaseType.MYSQL.formatJdbcConnectionProperties( + Map.of( + // Performance improvements + "autocommit", false, + "defaultFetchSize", 1000, + + // Help debug in case of deadlock + "includeInnodbStatusInDeadlockExceptions", true, + "includeThreadDumpInDeadlockExceptions", true, + + // https://github.com/brettwooldridge/HikariCP/wiki/Rapid-Recovery#mysql + "socketTimeout", 14000L, + // Needed for use with connection init-SQL (hikariConf.setConnectionInitSql) + "allowMultiQueries", true, + // Help debug in case of exceptions + "dumpQueriesOnException", true + ) + ); + }; + + switch (db) { + case HSQLDB, H2 -> { + final String subfolder = "data"; + final String fileName = "database"; + final String fileExtension = switch (db) { + case HSQLDB -> ".hsql"; + case H2 -> ".mv.db"; + default -> ""; + }; + final String fileNameWithExtension = fileName + fileExtension; + + // Set credentials for HSQL and H2 + hikariConfig.setUsername("SA"); + hikariConfig.setPassword(""); + + hikariConfig.setDataSourceClassName(db.getDataSourceClassName()); + hikariConfig.addDataSourceProperty("url", "jdbc:%s:%s".formatted( + db.getJdbcPrefix(), + plugin.getDataFolder().getAbsolutePath() + File.separatorChar + subfolder + File.separatorChar + (db.equals(DatabaseType.H2) ? fileName : fileNameWithExtension) + connectionProperties + )); + } + case MYSQL, MARIADB -> { + hikariConfig.setDataSourceClassName(db.getDataSourceClassName()); + hikariConfig.addDataSourceProperty("url", "jdbc:%s://%s%s/%s".formatted( + db.getJdbcPrefix(), + host, + ":%s".formatted(port), + database + connectionProperties + )); + + hikariConfig.setUsername(username); + hikariConfig.setPassword(password); + + hikariConfig.setKeepaliveTime(0); + } + } + + hikariConfig.setConnectionTimeout(14000); + hikariConfig.setMaxLifetime(25000000); + hikariConfig.setInitializationFailTimeout(-1); // We try to create tables after this anyways which will error if no connection + + final int poolSize = Cfg.get().getOrDefault("db.poolsize", 10); + hikariConfig.setMaximumPoolSize(poolSize); + hikariConfig.setMinimumIdle(poolSize); + + hikariConfig.setPoolName("%s-hikari".formatted(AlathraSkills.getInstance().getName())); + hikariConfig.setAutoCommit(true); + hikariConfig.setTransactionIsolation("TRANSACTION_REPEATABLE_READ"); + hikariConfig.setIsolateInternalQueries(true); + hikariConfig.setConnectionInitSql(db.getConnectionInitSql()); + + this.hikariDataSource = new HikariDataSource(hikariConfig); + this.database = db; + this.jooqContext = new JooqContext(db.getSQLDialect()); + + try { + new DatabaseMigrationHandler(Cfg.get(), hikariDataSource, db) + .migrate(); + } catch (DatabaseMigrationException e) { + Logger.get().error("[DB] Failed to migrate database. Please backup your database and report the issue.", e); + } + } + + /** + * Closes the connection pool. + */ + private void closeDatabaseConnection() throws Exception { + Logger.get().info("[DB] Closing database connection..."); + + if (hikariDataSource == null) { + Logger.get().error("[DB] Skipped closing database connection because the data source is null. Was there a previous error which needs to be fixed? Check your console logs!"); + return; + } + + if (hikariDataSource.isClosed()) { + Logger.get().info("[DB] Skipped closing database connection: connection is already closed."); + return; + } + + hikariDataSource.close(); + hikariDataSource = null; + + Logger.get().info("[DB] Closed database connection."); + } +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/db/DatabaseQueries.java b/src/main/java/io/github/Alathra/AlathraSkills/db/DatabaseQueries.java new file mode 100644 index 0000000..3afb78d --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/db/DatabaseQueries.java @@ -0,0 +1,256 @@ +package io.github.alathra.alathraskills.db; + +import io.github.alathra.alathraskills.db.schema.tables.records.PlayerSkillinfoRecord; +import io.github.alathra.alathraskills.utility.DB; +import io.github.alathra.alathraskills.utility.Logger; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jooq.DSLContext; +import org.jooq.Record1; +import org.jooq.Record2; +import org.jooq.Result; +import org.jooq.exception.DataAccessException; + +import java.nio.ByteBuffer; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.UUID; + +import static io.github.alathra.alathraskills.db.schema.Tables.PLAYER_SKILLCATEGORYINFO; +import static io.github.alathra.alathraskills.db.schema.Tables.PLAYER_SKILLINFO; + +/** + * A holder class for all SQL queries + */ +public abstract class DatabaseQueries { + + + /** + * Attempts to save skill category experience to DB. + * + * @param uuid + * @param skillCategoryId + * @param experience + */ + + public static void saveSkillCategoryExperience(UUID uuid, int skillCategoryId, Float experience) { + try ( + Connection con = DB.getConnection() + ) { + DSLContext context = DB.getContext(con); + + context + .insertInto(PLAYER_SKILLCATEGORYINFO, + PLAYER_SKILLCATEGORYINFO.UUID, + PLAYER_SKILLCATEGORYINFO.SKILLCATEGORYID, + PLAYER_SKILLCATEGORYINFO.EXPERIENCE) + .values( + convertUUIDToBytes(uuid), + skillCategoryId, + experience.doubleValue() + ) + .onDuplicateKeyUpdate() + .set(PLAYER_SKILLCATEGORYINFO.EXPERIENCE, experience.doubleValue()) + .execute(); + } catch (DataAccessException e) { + Logger.get().error("SQL Query threw an error!", e); + } catch (SQLException e) { + Logger.get().error("SQL Query threw an error!", e); + } + } + + public static void saveSkillCategoryExperience(Player p, int skillCategoryId, float experience) { + saveSkillCategoryExperience(p.getUniqueId(), skillCategoryId, experience); + } + + /** + * Attempts to save an unlocked skill to DB. + * + * @param uuid + * @param skillId + */ + + public static void saveSkillInfo(UUID uuid, int skillId) { + try ( + Connection con = DB.getConnection() + ) { + DSLContext context = DB.getContext(con); + + context + .insertInto(PLAYER_SKILLINFO, + PLAYER_SKILLINFO.UUID, + PLAYER_SKILLINFO.SKILLID) + .values( + convertUUIDToBytes(uuid), + skillId + ) + .onDuplicateKeyIgnore() + .execute(); + } catch (DataAccessException e) { + Logger.get().error("SQL Query threw an error!", e); + } catch (SQLException e) { + Logger.get().error("SQL Query threw an error!", e); + } + } + + public static void saveSkillInfo(Player p, int skillId) { + saveSkillInfo(p.getUniqueId(), skillId); + } + + /** + * Fetches skill category experience. + * + * @param uuid + * @param skillCategoryId + * @return record containing skill category experience. + */ + + public static Record1 getSkillCategoryExperience(UUID uuid, int skillCategoryId) { + try ( + Connection con = DB.getConnection() + ) { + DSLContext context = DB.getContext(con); + + return context + .select(PLAYER_SKILLCATEGORYINFO.EXPERIENCE) + .from(PLAYER_SKILLCATEGORYINFO) + .where(PLAYER_SKILLCATEGORYINFO.UUID.equal(convertUUIDToBytes(uuid))) + .and(PLAYER_SKILLCATEGORYINFO.SKILLCATEGORYID.equal(skillCategoryId)) + .fetchOne(); + } catch (DataAccessException e) { + Logger.get().error("SQL Query threw an error!", e); + return null; + } catch (SQLException e) { + Logger.get().error("SQL Query threw an error!", e); + return null; + } + } + + public static Record1 getSkillCategoryExperience(Player p, int skillCategoryId) { + return getSkillCategoryExperience(p.getUniqueId(), skillCategoryId); + } + + /** + * Convenience method for {@link #getSkillCategoryExperience(UUID, int)} + * + * @param uuid + * @param skillCategoryId + * @return skill category experience as float. + */ + + public static float getSkillCategoryExperienceFloat(UUID uuid, int skillCategoryId) { + Record1 returnRecord = getSkillCategoryExperience(uuid, skillCategoryId); + if (returnRecord == null) { + return 0; + } + return ((Double) returnRecord.getValue("EXPERIENCE")).floatValue(); + } + + public static float getSkillCategoryExperienceFloat(Player p, int skillCategoryId) { + return getSkillCategoryExperienceFloat(p.getUniqueId(), skillCategoryId); + } + + /** + * Checks if player has a skill unlocked. + * + * @param uuid + * @param skillId + * @return whether the player has a skill unlocked, and null if error. + */ + + public static Boolean doesPlayerHaveSkill(UUID uuid, int skillId) { + try ( + Connection con = DB.getConnection() + ) { + DSLContext context = DB.getContext(con); + + return context + .fetchExists(context + .selectFrom(PLAYER_SKILLINFO) + .where(PLAYER_SKILLINFO.UUID.equal(convertUUIDToBytes(uuid))) + .and(PLAYER_SKILLINFO.SKILLID.equal(skillId))); + } catch (DataAccessException e) { + Logger.get().error("SQL Query threw an error!", e); + return null; + } catch (SQLException e) { + Logger.get().error("SQL Query threw an error!", e); + return null; + } + } + + public static Boolean doesPlayerHaveSkill(Player p, int skillId) { + return doesPlayerHaveSkill(p.getUniqueId(), skillId); + } + + /** + * Fetches all skills a player has + * + * @param uuid + * @return all skill records a player is associated with. + */ + + public static Result fetchPlayerSkills(UUID uuid) { + try ( + Connection con = DB.getConnection() + ) { + DSLContext context = DB.getContext(con); + + return context + .fetch(context + .selectFrom(PLAYER_SKILLINFO) + .where(PLAYER_SKILLINFO.UUID.equal(convertUUIDToBytes(uuid)))); + } catch (DataAccessException e) { + Logger.get().error("SQL Query threw an error!", e); + return null; + } catch (SQLException e) { + Logger.get().error("SQL Query threw an error!", e); + return null; + } + } + + public static Result fetchPlayerSkills(Player p) { + return fetchPlayerSkills(p.getUniqueId()); + } + + /** + * Deletes all skills a player has + * + * @param uuid + */ + + public static void deletePlayerSkills(UUID uuid) { + try ( + Connection con = DB.getConnection() + ) { + DSLContext context = DB.getContext(con); + + context + .delete(PLAYER_SKILLINFO + .where(PLAYER_SKILLINFO.UUID.equal(convertUUIDToBytes(uuid)))) + .execute(); + } catch (DataAccessException e) { + Logger.get().error("SQL Query threw an error!", e); + } catch (SQLException e) { + Logger.get().error("SQL Query threw an error!", e); + } + } + + public static void deletePlayerSkills(Player p) { + deletePlayerSkills(p.getUniqueId()); + } + + public static byte[] convertUUIDToBytes(UUID uuid) { + ByteBuffer bb = ByteBuffer.wrap(new byte[16]); + bb.putLong(uuid.getMostSignificantBits()); + bb.putLong(uuid.getLeastSignificantBits()); + return bb.array(); + } + + public static UUID convertBytesToUUID(byte[] bytes) { + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + long high = byteBuffer.getLong(); + long low = byteBuffer.getLong(); + return new UUID(high, low); + } +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/db/DatabaseType.java b/src/main/java/io/github/Alathra/AlathraSkills/db/DatabaseType.java new file mode 100644 index 0000000..aabe91e --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/db/DatabaseType.java @@ -0,0 +1,287 @@ +package io.github.alathra.alathraskills.db; + +import com.mysql.cj.jdbc.MysqlDataSource; +import org.h2.jdbcx.JdbcDataSource; +import org.hsqldb.jdbc.JDBCDataSource; +import org.jetbrains.annotations.Nullable; +import org.jooq.SQLDialect; +import org.mariadb.jdbc.MariaDbDataSource; + +import java.util.List; +import java.util.Map; + +/** + * Enum containing information for handling different Database types. + */ +public enum DatabaseType { + /** + * HSQLDB database type. + */ + HSQLDB("HSQLDB", org.hsqldb.jdbcDriver.class.getName(), JDBCDataSource.class.getName(), "hsqldb", ';', ';'), + /** + * H2 Database Engine database type. + */ + H2("H2", org.h2.Driver.class.getName(), JdbcDataSource.class.getName(), "h2", ';', ';'), + /** + * MySQL database type. + */ + MYSQL("MySQL", com.mysql.jdbc.Driver.class.getName(), MysqlDataSource.class.getName(), "mysql", '?', '&'), + /** + * MariaDB database type. + */ + MARIADB("MariaDB", org.mariadb.jdbc.Driver.class.getName(), MariaDbDataSource.class.getName(), "mariadb", '?', '&'), + ; + + private final String driverName; + private final String driverClassName; + private final String dataSourceClassName; + private final String jdbcPrefix; + private final char jdbcPropertyPrefix; + private final char jdbcPropertySeparator; + + DatabaseType(String driverName, String driverClassName, String dataSourceClassName, String jdbcPrefix, char jdbcPropertyPrefix, char jdbcPropertySeparator) { + this.driverName = driverName; + this.driverClassName = driverClassName; + this.dataSourceClassName = dataSourceClassName; + this.jdbcPrefix = jdbcPrefix; + this.jdbcPropertyPrefix = jdbcPropertyPrefix; + this.jdbcPropertySeparator = jdbcPropertySeparator; + } + + /** + * Gets driver name. + * + * @return the driver name + */ + public String getDriverName() { + return driverName; + } + + /** + * Gets driver class name. + * + * @return the driver class name + */ + public String getDriverClassName() { + return driverClassName; + } + + /** + * Gets data source class name. + * + * @return the data source class name + */ + public String getDataSourceClassName() { + return dataSourceClassName; + } + + /** + * Gets the jdbc prefix. + * + * @return the jdbc prefix like mysql or mariadb + */ + public String getJdbcPrefix() { + return jdbcPrefix; + } + + /** + * Gets the jdbc property prefix. + * + * @return the jdbc property prefix like {@literal ;} or {@literal ?} + */ + public char getJdbcPropertyPrefix() { + return jdbcPropertyPrefix; + } + + /** + * Gets the jdbc property separator. + * + * @return the jdbc property separator like {@literal ;} or {@literal &} + */ + public char getJdbcPropertySeparator() { + return jdbcPropertySeparator; + } + + /** + * Format a map of connection properties into a valid jdbc connection properties string. + * + * @param properties the properties + * @return the string + */ + public String formatJdbcConnectionProperties(Map properties) { + if (properties.isEmpty()) return ""; + + List connectionProperties = properties.entrySet().stream() + .map(map -> "%s=%s".formatted(map.getKey(), map.getValue())) + .toList(); + + return jdbcPropertyPrefix + String.join(Character.toString(jdbcPropertySeparator), connectionProperties); + } + + + /** + * Gets database type from jdbc prefix. + * + * @param prefix the prefix + * @return the database type or null + */ + @Nullable + public static DatabaseType getDatabaseTypeFromJdbcPrefix(String prefix) { + for (DatabaseType type : DatabaseType.values()) { + if (type.equals(prefix.toLowerCase())) { + return type; + }; + } + + return null; + } + + @Override + public String toString() { + return getDriverName(); + } + + /** + * Check if this DatabaseType is equal to another. + * + * @param DatabaseType the database type + * @return boolean + */ + public boolean equals(DatabaseType DatabaseType) { + return this.equals(DatabaseType.getDriverName()); + } + + /** + * Check if this DatabaseType is equal to another. + * + * @param databaseTypeName the database type name + * @return boolean + */ + public boolean equals(String databaseTypeName) { + return this.getDriverName().equalsIgnoreCase(databaseTypeName); + } + + /** + * Gets sql dialect for this DatabaseType. + * + * @return the sql dialect + */ + public SQLDialect getSQLDialect() { + return switch (this) { + case HSQLDB -> SQLDialect.HSQLDB; + case H2 -> SQLDialect.H2; + case MYSQL -> SQLDialect.MYSQL; + case MARIADB -> SQLDialect.MARIADB; + }; + } + + /** + * Gets column suffix for this DatabaseType. + * + * @return the column suffix + */ + public String getColumnSuffix() { + return switch (this) { + case HSQLDB -> ""; + case H2, MARIADB, MYSQL -> " VIRTUAL"; + }; + } + + /** + * Gets table defaults for this DatabaseType. + * + * @return the table defaults + */ + public String getTableDefaults() { + return switch (this) { + case H2, HSQLDB -> ""; + case MARIADB, MYSQL -> " CHARACTER SET utf8mb4 COLLATE utf8mb4_bin"; + }; + } + + /** + * Gets uuid type for this DatabaseType. + * + * @return the uuid type + */ + public String getUuidType() { + return switch (this) { + case HSQLDB -> "UUID"; + case H2, MARIADB, MYSQL -> "BINARY(16)"; + }; + } + + /** + * Gets inet type for this DatabaseType. + * + * @return the inet type + */ + public String getInetType() { + return "VARBINARY(16)"; + } + + /** + * Gets binary type for this DatabaseType. + * + * @return the binary type + */ + public String getBinaryType() { + return "BLOB"; + } + + /** + * Gets alter view statement for this DatabaseType. + * + * @return the alter view statement + */ + public String getAlterViewStatement() { + return switch (this) { + case H2, HSQLDB, MYSQL, MARIADB -> "ALTER VIEW"; + }; + } + + /** + * Gets connection init sql for this DatabaseType. + * + * @return the connection init sql + */ + public String getConnectionInitSql() { + return switch (this) { + case HSQLDB -> "SET DATABASE TRANSACTION CONTROL MVLOCKS;"; + case MARIADB -> "SET NAMES utf8mb4 COLLATE utf8mb4_bin; " + setSqlModes( + // MariaDB defaults + "STRICT_TRANS_TABLES", + "ERROR_FOR_DIVISION_BY_ZERO", + "NO_AUTO_CREATE_USER", + "NO_ENGINE_SUBSTITUTION", + // ANSI SQL Compliance + "ANSI", + "NO_BACKSLASH_ESCAPES", + "SIMULTANEOUS_ASSIGNMENT", // MDEV-13417 + "NO_ZERO_IN_DATE", + "NO_ZERO_DATE"); + case H2 -> ""; + case MYSQL -> "SET NAMES utf8mb4 COLLATE utf8mb4_bin; " + setSqlModes( + // MySQL defaults + "STRICT_TRANS_TABLES", + "ERROR_FOR_DIVISION_BY_ZERO", + "NO_ENGINE_SUBSTITUTION", + // ANSI SQL Compliance + "ANSI", + "NO_BACKSLASH_ESCAPES", + "NO_ZERO_IN_DATE", + "NO_ZERO_DATE"); + }; + } + + /** + * Generates a concatenated SQL query from the specified parameters. + * + * @param sqlModes database parameters + * @return string + */ + private static String setSqlModes(String... sqlModes) { + final String modes = String.join(",", sqlModes); + return "SET @@SQL_MODE = CONCAT(@@SQL_MODE, '," + modes + "')"; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/Alathra/AlathraSkills/db/flyway/DatabaseMigrationException.java b/src/main/java/io/github/Alathra/AlathraSkills/db/flyway/DatabaseMigrationException.java new file mode 100644 index 0000000..f1092ad --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/db/flyway/DatabaseMigrationException.java @@ -0,0 +1,7 @@ +package io.github.alathra.alathraskills.db.flyway; + +public class DatabaseMigrationException extends Exception { + public DatabaseMigrationException(Throwable t) { + super(t); + } +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/db/flyway/DatabaseMigrationHandler.java b/src/main/java/io/github/Alathra/AlathraSkills/db/flyway/DatabaseMigrationHandler.java new file mode 100644 index 0000000..b8bbd2c --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/db/flyway/DatabaseMigrationHandler.java @@ -0,0 +1,101 @@ +package io.github.alathra.alathraskills.db.flyway; + +import io.github.alathra.alathraskills.db.DatabaseType; +import com.github.milkdrinkers.Crate.Config; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.ClassProvider; +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.migration.JavaMigration; + +import javax.sql.DataSource; +import java.util.List; +import java.util.Map; + +/** + * Handles Flyway migrations. + */ +public class DatabaseMigrationHandler { + // List of Java migrations + private final List> migrations = List.of( + ); + + private final Config config; + private final DataSource dataSource; + private final DatabaseType databaseType; + private Flyway flyway; + + /** + * Instantiates a new Database migration handler. + * + * @param config the config + * @param dataSource the data source + * @param databaseType the database type + */ + public DatabaseMigrationHandler(Config config, DataSource dataSource, DatabaseType databaseType) { + this.config = config; + this.dataSource = dataSource; + this.databaseType = databaseType; + initializeFlywayInstance(); + } + + private void initializeFlywayInstance() { + final ClassProvider javaMigrationClassProvider = new FlywayMigrationsProvider(migrations); + final String SQL_TABLE_PREFIX = config.getOrDefault("db.prefix", "example_"); + final Map SQL_PLACEHOLDERS = Map.of( + "tablePrefix", SQL_TABLE_PREFIX, + "columnSuffix", databaseType.getColumnSuffix(), + "tableDefaults", databaseType.getTableDefaults(), + "uuidType", databaseType.getUuidType(), + "inetType", databaseType.getInetType(), + "binaryType", databaseType.getBinaryType(), + "alterViewStatement", databaseType.getAlterViewStatement() + ); + + this.flyway = Flyway + .configure(getClass().getClassLoader()) + .baselineOnMigrate(true) + .baselineVersion("0.0") + .validateMigrationNaming(true) + .javaMigrationClassProvider(javaMigrationClassProvider) + .dataSource(dataSource) + .locations( + "classpath:database-migrations", + "db/migration" + ) + .table(SQL_TABLE_PREFIX + "schema_history") // Configure tables and migrations + .placeholders(SQL_PLACEHOLDERS) + .load(); + } + + /** + * Execute Flyway migration. All pending migrations will be applied in order. + * + * @throws DatabaseMigrationException database migration exception + */ + public void migrate() throws DatabaseMigrationException { + try { + this.flyway.migrate(); + } catch (FlywayException e) { + throw new DatabaseMigrationException(e); + } + } + + /** + * Executes Flyway repair. Repairs the Flyway schema history table. This will perform the following actions: + * + *
    + *
  • Remove any failed migrations on databases without DDL transactions (User objects left behind must still be cleaned up manually)
  • + *
  • Realign the checksums, descriptions and types of the applied migration
  • + *
+ * + * @throws DatabaseMigrationException database migration exception + */ + public void repair() throws DatabaseMigrationException { + try { + if (config.getOrDefault("db.repair", false)) + this.flyway.repair(); + } catch (FlywayException e) { + throw new DatabaseMigrationException(e); + } + } +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/db/flyway/FlywayMigrationsProvider.java b/src/main/java/io/github/Alathra/AlathraSkills/db/flyway/FlywayMigrationsProvider.java new file mode 100644 index 0000000..86ad0ba --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/db/flyway/FlywayMigrationsProvider.java @@ -0,0 +1,20 @@ +package io.github.alathra.alathraskills.db.flyway; + +import org.flywaydb.core.api.ClassProvider; +import org.flywaydb.core.api.migration.JavaMigration; + +import java.util.Collection; +import java.util.List; + +public record FlywayMigrationsProvider( + List> migrations +) implements ClassProvider { + public FlywayMigrationsProvider(List> migrations) { + this.migrations = List.copyOf(migrations); + } + + @Override + public Collection> getClasses() { + return migrations; + } +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/db/jooq/JooqContext.java b/src/main/java/io/github/Alathra/AlathraSkills/db/jooq/JooqContext.java new file mode 100644 index 0000000..aaecdfd --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/db/jooq/JooqContext.java @@ -0,0 +1,92 @@ +package io.github.alathra.alathraskills.db.jooq; + +import io.github.alathra.alathraskills.utility.Cfg; +import org.jooq.*; +import org.jooq.conf.*; +import org.jooq.exception.DataAccessException; +import org.jooq.impl.DefaultConfiguration; +import org.jooq.tools.JooqLogger; + +import java.sql.Connection; +import java.util.regex.Pattern; + +/** + * Utility class for getting jOOQ Context to use with HikariCP. + */ +public final class JooqContext { + static { + JooqLogger.globalThreshold(Log.Level.ERROR); // Silence JOOQ warnings + } + + private static final String TABLE_PREFIX = Cfg.get().getOrDefault("db.prefix", "alathra_skills_"); // The table prefix as grabbed from config + private static final Pattern MATCH_ALL_EXCEPT_INFORMATION_SCHEMA = Pattern.compile("^(?!INFORMATION_SCHEMA)(.*?)$"); + private static final Pattern MATCH_ALL = Pattern.compile("^(.*?)$"); + private static final String REPLACEMENT = "%s$0".formatted(TABLE_PREFIX); // + private final SQLDialect dialect; + + /** + * Instantiates a new Jooq context. + * + * @param dialect the getSQLDialect + */ + public JooqContext(SQLDialect dialect) { + this.dialect = dialect; + } + + /** + * Create DSL Context. + * + * @param connection the connection + * @return the dsl context + */ + public DSLContext createContext(Connection connection) { + record SimpleConnectionProvider(Connection connection) implements ConnectionProvider { + + @Override + public Connection acquire() throws DataAccessException { + return connection; + } + + @Override + public void release(Connection connection) throws DataAccessException {} + } + return createWith(new SimpleConnectionProvider(connection)); + } + + /** + * Applies default configuration + * + * @param connectionProvider A connection lifecycle handler API. + * @return DSLContext + */ + private DSLContext createWith(ConnectionProvider connectionProvider) { + return new DefaultConfiguration() + .set(connectionProvider) + .set(dialect) + .set(createSettings()) + .set(new ExecuteListenerProvider[0]) + .dsl(); + } + + /** + * Returns base settings for DSL Contexts. + * + * @return Settings + */ + private Settings createSettings() { + return new Settings() + .withBackslashEscaping(BackslashEscaping.OFF) + .withRenderSchema(false) + .withRenderMapping(new RenderMapping() // Support the tables having custom prefix + .withSchemata(new MappedSchema() + .withInputExpression(MATCH_ALL_EXCEPT_INFORMATION_SCHEMA) + .withTables(new MappedTable() + .withInputExpression(MATCH_ALL) + .withOutput(REPLACEMENT) + ) + ) + ) + .withRenderQuotedNames(RenderQuotedNames.ALWAYS) + .withRenderNameCase(RenderNameCase.LOWER); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/Alathra/AlathraSkills/gui/GuiHelper.java b/src/main/java/io/github/Alathra/AlathraSkills/gui/GuiHelper.java new file mode 100644 index 0000000..133edab --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/gui/GuiHelper.java @@ -0,0 +1,53 @@ +package io.github.alathra.alathraskills.gui; + +import com.github.milkdrinkers.colorparser.ColorParser; +import dev.triumphteam.gui.guis.Gui; + +public class GuiHelper { + + public enum GuiType { + MAIN, + SKILL + } + + public static Gui buildGui(GuiType type) { + Gui gui; + switch (type) { + case MAIN -> { + gui = Gui.gui() + .rows(6) + .title(ColorParser.of("[AlathraSkills]").build()) // TODO: config + .disableItemDrop() + .disableItemPlace() + .disableItemSwap() + .disableItemTake() + .create(); + } + case SKILL -> { + gui = Gui.gui() + .rows(3) + .title(ColorParser.of("[AlathraSkills]").build()) // TODO: config + .disableItemDrop() + .disableItemPlace() + .disableItemSwap() + .disableItemTake() + .create(); + } + default -> gui = null; + } + return gui; + } + + public static void populateMainGui(Gui gui) { + io.github.alathra.alathraskills.gui.main.PopulateBorders.populateBorders(gui); + io.github.alathra.alathraskills.gui.main.PopulateButtons.populateButtons(gui); + io.github.alathra.alathraskills.gui.main.PopulateContent.populateContent(gui); + } + + public static void populateSkillGui(Gui gui) { + io.github.alathra.alathraskills.gui.skill.PopulateBorders.populateBorders(gui); + io.github.alathra.alathraskills.gui.skill.PopulateButtons.populateButtons(gui); + io.github.alathra.alathraskills.gui.skill.PopulateContent.populateContent(gui); + } + +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/gui/main/PopulateBorders.java b/src/main/java/io/github/Alathra/AlathraSkills/gui/main/PopulateBorders.java new file mode 100644 index 0000000..92e8ece --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/gui/main/PopulateBorders.java @@ -0,0 +1,16 @@ +package io.github.alathra.alathraskills.gui.main; + +import dev.triumphteam.gui.builder.item.ItemBuilder; +import dev.triumphteam.gui.guis.Gui; +import dev.triumphteam.gui.guis.GuiItem; +import net.kyori.adventure.text.Component; +import org.bukkit.Material; + +public class PopulateBorders { + + public static void populateBorders(Gui gui) { + GuiItem greyGlassPane = ItemBuilder.from(Material.GRAY_STAINED_GLASS_PANE).name(Component.text("")).asGuiItem(); + gui.getFiller().fillBorder(greyGlassPane); + } + +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/gui/main/PopulateButtons.java b/src/main/java/io/github/Alathra/AlathraSkills/gui/main/PopulateButtons.java new file mode 100644 index 0000000..29e6316 --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/gui/main/PopulateButtons.java @@ -0,0 +1,11 @@ +package io.github.alathra.alathraskills.gui.main; + +import dev.triumphteam.gui.guis.Gui; + +public class PopulateButtons { + + public static void populateButtons(Gui gui) { + // TODO: Add buttons + } + +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/gui/main/PopulateContent.java b/src/main/java/io/github/Alathra/AlathraSkills/gui/main/PopulateContent.java new file mode 100644 index 0000000..3c95495 --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/gui/main/PopulateContent.java @@ -0,0 +1,11 @@ +package io.github.alathra.alathraskills.gui.main; + +import dev.triumphteam.gui.guis.Gui; + +public class PopulateContent { + + public static void populateContent(Gui gui) { + // TODO: Add content + } + +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/gui/skill/PopulateBorders.java b/src/main/java/io/github/Alathra/AlathraSkills/gui/skill/PopulateBorders.java new file mode 100644 index 0000000..e9db909 --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/gui/skill/PopulateBorders.java @@ -0,0 +1,16 @@ +package io.github.alathra.alathraskills.gui.skill; + +import dev.triumphteam.gui.builder.item.ItemBuilder; +import dev.triumphteam.gui.guis.Gui; +import dev.triumphteam.gui.guis.GuiItem; +import net.kyori.adventure.text.Component; +import org.bukkit.Material; + +public class PopulateBorders { + + public static void populateBorders(Gui gui) { + GuiItem greyGlassPane = ItemBuilder.from(Material.GRAY_STAINED_GLASS_PANE).name(Component.text("")).asGuiItem(); + gui.getFiller().fillBorder(greyGlassPane); + } + +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/gui/skill/PopulateButtons.java b/src/main/java/io/github/Alathra/AlathraSkills/gui/skill/PopulateButtons.java new file mode 100644 index 0000000..e835d63 --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/gui/skill/PopulateButtons.java @@ -0,0 +1,11 @@ +package io.github.alathra.alathraskills.gui.skill; + +import dev.triumphteam.gui.guis.Gui; + +public class PopulateButtons { + + public static void populateButtons (Gui gui) { + // TODO: Add buttons + } + +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/gui/skill/PopulateContent.java b/src/main/java/io/github/Alathra/AlathraSkills/gui/skill/PopulateContent.java new file mode 100644 index 0000000..227fa7b --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/gui/skill/PopulateContent.java @@ -0,0 +1,11 @@ +package io.github.alathra.alathraskills.gui.skill; + +import dev.triumphteam.gui.guis.Gui; + +public class PopulateContent { + + public static void populateContent(Gui gui) { + // TODO: Add content + } + +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/hooks/VaultHook.java b/src/main/java/io/github/Alathra/AlathraSkills/hooks/VaultHook.java new file mode 100644 index 0000000..a35cf43 --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/hooks/VaultHook.java @@ -0,0 +1,150 @@ +package io.github.alathra.alathraskills.hooks; + +import io.github.alathra.alathraskills.AlathraSkills; +import io.github.alathra.alathraskills.Reloadable; +import net.milkbowl.vault.chat.Chat; +import net.milkbowl.vault.economy.Economy; +import net.milkbowl.vault.permission.Permission; +import org.bukkit.plugin.RegisteredServiceProvider; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +/** + * A hook to interface with the Vault API. + */ +public class VaultHook implements Reloadable { + private final AlathraSkills plugin; + private @Nullable RegisteredServiceProvider rspEconomy; + private @Nullable RegisteredServiceProvider rspPermissions; + private @Nullable RegisteredServiceProvider rspChat; + + /** + * Instantiates a new Vault hook. + * + * @param plugin the plugin instance + */ + public VaultHook(AlathraSkills plugin) { + this.plugin = plugin; + } + + @Override + public void onLoad() { + } + + @Override + public void onEnable() { + if (!isVaultLoaded()) return; + + setEconomy(plugin.getServer().getServicesManager().getRegistration(Economy.class)); + setPermissions(plugin.getServer().getServicesManager().getRegistration(Permission.class)); + setChat(plugin.getServer().getServicesManager().getRegistration(Chat.class)); + } + + @Override + public void onDisable() { + if (!isVaultLoaded()) return; + + setEconomy(null); + setPermissions(null); + setChat(null); + } + + /** + * Check if Vault is present on the server. + * + * @return the boolean + */ + public boolean isVaultLoaded() { + return plugin.getServer().getPluginManager().isPluginEnabled("Vault"); + } + + /** + * Check if a vault economy plugin is loaded. + * + * @return boolean + */ + public boolean isEconomyLoaded() { + return rspEconomy != null && getEconomy() != null; + } + + /** + * Gets vault economy instance. Should only be used after {@link #isEconomyLoaded()}. + * + * @return vault instance + */ + public Economy getEconomy() { + if (rspEconomy == null) + throw new NullPointerException("The plugin tried to use Vault without it being loaded. Use the VaultHook#isVaultLoaded method before using vault methods."); + return rspEconomy.getProvider(); + } + + /** + * Sets the vault economy service provider. + * + * @param rsp The service provider providing {@link Economy} + */ + @ApiStatus.Internal + public void setEconomy(@Nullable RegisteredServiceProvider rsp) { + this.rspEconomy = rsp; + } + + /** + * Check if a vault permissions plugin is loaded. + * + * @return boolean + */ + public boolean isPermissionsLoaded() { + return rspPermissions != null && getPermissions() != null; + } + + /** + * Gets vault permissions instance. Should only be used after {@link #isPermissionsLoaded()}. + * + * @return vault instance + */ + public Permission getPermissions() { + if (rspPermissions == null) + throw new NullPointerException("The plugin tried to use Vault without it being loaded. Use the VaultHook#isVaultLoaded method before using vault methods."); + return rspPermissions.getProvider(); + } + + /** + * Sets the vault permissions service provider. + * + * @param rsp The service provider providing {@link Permission} + */ + @ApiStatus.Internal + public void setPermissions(@Nullable RegisteredServiceProvider rsp) { + this.rspPermissions = rsp; + } + + /** + * Check if a vault chat plugin is loaded. + * + * @return boolean + */ + public boolean isChatLoaded() { + return rspChat != null && getChat() != null; + } + + /** + * Gets vault Chat instance. Should only be used after {@link #isChatLoaded()}. + * + * @return vault instance + */ + public Chat getChat() { + if (rspChat == null) + throw new NullPointerException("The plugin tried to use Vault without it being loaded. Use the VaultHook#isVaultLoaded method before using vault methods."); + return rspChat.getProvider(); + } + + /** + * Sets the vault Chat service provider. + * + * @param rsp The service provider providing {@link Chat} + */ + @ApiStatus.Internal + public void setChat(@Nullable RegisteredServiceProvider rsp) { + this.rspChat = rsp; + } +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/skills/Skill.java b/src/main/java/io/github/Alathra/AlathraSkills/skills/Skill.java new file mode 100644 index 0000000..c380199 --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/skills/Skill.java @@ -0,0 +1,75 @@ +package io.github.alathra.alathraskills.skills; + +import java.util.ArrayList; + +import org.bukkit.inventory.ItemStack; + +public abstract class Skill { + + private int id; + private String name; + private String description; + + private ItemStack icon; + private SkillCategory category; + + // Optional + private ArrayList parents; + private ArrayList childrens; + + public Skill(int id, String name, String description, ItemStack icon, SkillCategory category) { + this.id = id; + this.name = name; + this.description = description; + this.icon = icon; + this.category = category; + } + + public SkillCategory getCategory() { + return category; + } + public void setCategory(SkillCategory category) { + this.category = category; + } + public ItemStack getIcon() { + return icon; + } + public void setIcon(ItemStack icon) { + this.icon = icon; + } + public String getDescription() { + return description; + } + public void setDescription(String description) { + this.description = description; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public int getId() { + return id; + } + public void setId(int id) { + this.id = id; + } + + public ArrayList getParents() { + return parents; + } + + public void setParents(ArrayList parents) { + this.parents = parents; + } + + public ArrayList getChildrens() { + return childrens; + } + + public void setChildrens(ArrayList childrens) { + this.childrens = childrens; + } + +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/skills/SkillCategory.java b/src/main/java/io/github/Alathra/AlathraSkills/skills/SkillCategory.java new file mode 100644 index 0000000..e1e20a1 --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/skills/SkillCategory.java @@ -0,0 +1,51 @@ +package io.github.alathra.alathraskills.skills; + +import java.util.ArrayList; + +import org.bukkit.inventory.ItemStack; + +public abstract class SkillCategory { + private int id; + private String name; + private String description; + + private ItemStack icon; + private ArrayList skills; + + public SkillCategory(int id, String name, String description) { + this.id = id; + this.name = name; + this.description = description; + } + + public int getId() { + return id; + } + public void setId(int id) { + this.id = id; + } + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public String getDescription() { + return description; + } + public void setDescription(String description) { + this.description = description; + } + public ItemStack getIcon() { + return icon; + } + public void setIcon(ItemStack icon) { + this.icon = icon; + } + public ArrayList getSkills() { + return skills; + } + public void setSkills(ArrayList skills) { + this.skills = skills; + } +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/skills/SkillsPlayer.java b/src/main/java/io/github/Alathra/AlathraSkills/skills/SkillsPlayer.java new file mode 100644 index 0000000..d656a94 --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/skills/SkillsPlayer.java @@ -0,0 +1,73 @@ +package io.github.alathra.alathraskills.skills; + +import java.util.HashMap; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; + +import io.github.alathra.alathraskills.AlathraSkills; +import io.github.alathra.alathraskills.api.SkillsManager; + +public class SkillsPlayer { + + // Bukkit + private OfflinePlayer offlinePlayer; + + // Internal + private SkillsManager skillsManager; + private HashMap expMap = new HashMap<>(); + + public SkillsPlayer(UUID playerUUID) { + skillsManager = AlathraSkills.getSkillsManager(); + setOfflinePlayer(Bukkit.getOfflinePlayer(playerUUID)); + + // Defaulting skill category exp + for (SkillCategory category : skillsManager.skillCategories.values()) { + expMap.put(category.getId(), 0.0f); + } + } + + public void setExp(int categoryID, float amount) { + expMap.put(categoryID, amount); + } + + public float getExp(int categoryID) { + return expMap.get(categoryID); + } + + public void addExp(int categoryID, float amount) { + setExp(categoryID, getExp(categoryID) + amount); + } + + public void removeExp(int categoryID, float amount) { + setExp(categoryID, getExp(categoryID) - amount); + } + + public void clearExp(int categoryID) { + setExp(categoryID, 0.0f); + } + + public void clearAllExp() { + for (SkillCategory category : skillsManager.skillCategories.values()) { + expMap.put(category.getId(), 0.0f); + } + } + + public float getTotalExp() { + float total = 0; + for (float exp : expMap.values()) { + total += exp; + } + return total; + } + + public OfflinePlayer getOfflinePlayer() { + return offlinePlayer; + } + + public void setOfflinePlayer(OfflinePlayer offlinePlayer) { + this.offlinePlayer = offlinePlayer; + } + +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/skills/categories/FarmingSkillCategory.java b/src/main/java/io/github/Alathra/AlathraSkills/skills/categories/FarmingSkillCategory.java new file mode 100644 index 0000000..f0109df --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/skills/categories/FarmingSkillCategory.java @@ -0,0 +1,24 @@ +package io.github.alathra.alathraskills.skills.categories; + +import java.util.Collections; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import com.github.milkdrinkers.colorparser.ColorParser; + +import io.github.alathra.alathraskills.skills.SkillCategory; + +public class FarmingSkillCategory extends SkillCategory { + + public FarmingSkillCategory(int id) { + super(id, "Farming", "Out here in the fields!"); + ItemStack icon = new ItemStack(Material.WHEAT, 1); + ItemMeta meta = icon.getItemMeta(); + meta.displayName(ColorParser.of("" + super.getName() + "").build()); + meta.lore(Collections.singletonList(ColorParser.of("" + super.getDescription() + "").build())); + icon.setItemMeta(meta); + super.setIcon(icon); + } +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/skills/categories/MiningSkillCategory.java b/src/main/java/io/github/Alathra/AlathraSkills/skills/categories/MiningSkillCategory.java new file mode 100644 index 0000000..c57bc33 --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/skills/categories/MiningSkillCategory.java @@ -0,0 +1,25 @@ +package io.github.alathra.alathraskills.skills.categories; + +import java.util.Collections; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import com.github.milkdrinkers.colorparser.ColorParser; + +import io.github.alathra.alathraskills.skills.SkillCategory; + +public class MiningSkillCategory extends SkillCategory { + + public MiningSkillCategory(int id) { + super(id, "Mining", "You know we had this one!"); + ItemStack icon = new ItemStack(Material.DIAMOND_ORE, 1); + ItemMeta meta = icon.getItemMeta(); + meta.displayName(ColorParser.of("" + super.getName() + "").build()); + meta.lore(Collections.singletonList(ColorParser.of("" + super.getDescription() + "").build())); + icon.setItemMeta(meta); + super.setIcon(icon); + } + +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/skills/categories/WoodcuttingSkillCategory.java b/src/main/java/io/github/Alathra/AlathraSkills/skills/categories/WoodcuttingSkillCategory.java new file mode 100644 index 0000000..797d544 --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/skills/categories/WoodcuttingSkillCategory.java @@ -0,0 +1,25 @@ +package io.github.alathra.alathraskills.skills.categories; + +import java.util.Collections; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import com.github.milkdrinkers.colorparser.ColorParser; + +import io.github.alathra.alathraskills.skills.SkillCategory; + +public class WoodcuttingSkillCategory extends SkillCategory { + + public WoodcuttingSkillCategory(int id) { + super(id, "Woodcutting", "Unleash your inner lumberjack!"); + ItemStack icon = new ItemStack(Material.OAK_LOG, 1); + ItemMeta meta = icon.getItemMeta(); + meta.displayName(ColorParser.of("" + super.getName() + "").build()); + meta.lore(Collections.singletonList(ColorParser.of("" + super.getDescription() + "
").build())); + icon.setItemMeta(meta); + super.setIcon(icon); + } + +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/utility/Cfg.java b/src/main/java/io/github/Alathra/AlathraSkills/utility/Cfg.java new file mode 100644 index 0000000..8ae875a --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/utility/Cfg.java @@ -0,0 +1,19 @@ +package io.github.alathra.alathraskills.utility; + +import io.github.alathra.alathraskills.AlathraSkills; +import io.github.alathra.alathraskills.config.ConfigHandler; +import com.github.milkdrinkers.Crate.Config; +import org.jetbrains.annotations.NotNull; + +/** + * Convenience class for accessing {@link ConfigHandler#getConfig} + */ +public abstract class Cfg { + /** + * Convenience method for {@link ConfigHandler#getConfig} to getConnection {@link Config} + */ + @NotNull + public static Config get() { + return AlathraSkills.getInstance().getConfigHandler().getConfig(); + } +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/utility/DB.java b/src/main/java/io/github/Alathra/AlathraSkills/utility/DB.java new file mode 100644 index 0000000..1c2d572 --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/utility/DB.java @@ -0,0 +1,39 @@ +package io.github.alathra.alathraskills.utility; + +import io.github.alathra.alathraskills.AlathraSkills; +import io.github.alathra.alathraskills.db.DatabaseHandler; +import io.github.alathra.alathraskills.db.DatabaseType; +import io.github.alathra.alathraskills.db.jooq.JooqContext; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Convenience class for accessing methods in {@link DatabaseHandler#getConnection} + */ +public abstract class DB { + /** + * Convenience method for {@link DatabaseHandler#getConnection} to getConnection {@link Connection} + */ + @NotNull + public static Connection getConnection() throws SQLException { + return AlathraSkills.getInstance().getDataHandler().getConnection(); + } + + /** + * Convenience method for {@link JooqContext#createContext(Connection)} to getConnection {@link DSLContext} + */ + @NotNull + public static DSLContext getContext(Connection con) { + return AlathraSkills.getInstance().getDataHandler().getJooqContext().createContext(con); + } + + /** + * Convenience method for {@link DatabaseHandler#getDB()} to getConnection {@link DatabaseType} + */ + public static DatabaseType getDB() { + return AlathraSkills.getInstance().getDataHandler().getDB(); + } +} diff --git a/src/main/java/io/github/Alathra/AlathraSkills/utility/Logger.java b/src/main/java/io/github/Alathra/AlathraSkills/utility/Logger.java new file mode 100644 index 0000000..813aaec --- /dev/null +++ b/src/main/java/io/github/Alathra/AlathraSkills/utility/Logger.java @@ -0,0 +1,21 @@ +package io.github.alathra.alathraskills.utility; + + +import io.github.alathra.alathraskills.AlathraSkills; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; +import org.jetbrains.annotations.NotNull; + +/** + * A class that provides shorthand access to {@link AlathraSkills#getComponentLogger}. + */ +public class Logger { + /** + * Get component logger. Shorthand for: + * + * @return the component logger {@link AlathraSkills#getComponentLogger}. + */ + @NotNull + public static ComponentLogger get() { + return AlathraSkills.getInstance().getComponentLogger(); + } +}