Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ dependencies {
compileOnly("me.clip:placeholderapi:2.11.6")

implementation("com.github.cryptomorin:XSeries:13.7.0")
implementation("com.zaxxer:HikariCP:4.0.3")
implementation("com.mysql:mysql-connector-j:8.0.33")
implementation("net.kyori:adventure-text-minimessage:4.17.0")
implementation("net.kyori:adventure-text-serializer-legacy:4.17.0")

Expand All @@ -51,6 +53,7 @@ tasks {

withType<ShadowJar> {
relocate("com.cryptomorin.xseries", "me.clip.deluxetags.libs.xseries")
relocate("com.zaxxer.hikari", "me.clip.deluxetags.libs.hikari")
relocate("net.kyori", "me.clip.deluxetags.libs.kyori")
archiveFileName.set("DeluxeTags-${project.version}.jar")
}
Expand Down
149 changes: 125 additions & 24 deletions src/main/java/me/clip/deluxetags/DeluxeTags.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package me.clip.deluxetags;

import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

import me.clip.deluxetags.commands.TagCommand;
import me.clip.deluxetags.config.ConfigWrapper;
Expand All @@ -15,6 +21,10 @@
import me.clip.deluxetags.listeners.JoinListener;
import me.clip.deluxetags.listeners.PlayerListener;
import me.clip.deluxetags.placeholders.TagPlaceholders;
import me.clip.deluxetags.storage.MysqlPlayerDataStorage;
import me.clip.deluxetags.storage.PlayerData;
import me.clip.deluxetags.storage.PlayerDataStorage;
import me.clip.deluxetags.storage.YamlPlayerDataStorage;
import me.clip.deluxetags.tags.DeluxeTag;
import me.clip.deluxetags.tags.DeluxeTagCategory;
import me.clip.deluxetags.tags.DeluxeTagsHandler;
Expand Down Expand Up @@ -42,6 +52,9 @@ public class DeluxeTags extends JavaPlugin {
private TagConfig cfg;
private ConfigWrapper messages;
private ConfigWrapper playerFile;
private PlayerDataStorage playerDataStorage;
private final Map<UUID, PlayerData> playerDataCache = new ConcurrentHashMap<>();
private ExecutorService storageExecutor;
private GUIHandler guiHandler;
private GUIOptions guiOptions;
private BukkitTask cleanupTask = null;
Expand All @@ -68,6 +81,25 @@ public void onEnable() {
playerFile = new ConfigWrapper(this, "userdata", "player_tags.yml");

playerFile.createNewFile(null, "DeluxeTags player_tags.yml\nDo not edit this file!");

try {
playerDataStorage = createPlayerDataStorage();
playerDataStorage.initialize();
getLogger().info("Player data storage initialized: " + (cfg.mysqlStorageEnabled() ? "MYSQL" : "YAML"));
} catch (Exception exception) {
getLogger().log(Level.SEVERE, "Could not initialize MySQL player data storage. Falling back to YAML", exception);
if (playerDataStorage != null) {
playerDataStorage.close();
}
playerDataStorage = new YamlPlayerDataStorage(playerFile);
try {
playerDataStorage.initialize();
} catch (Exception fallbackException) {
getLogger().log(Level.SEVERE, "Could not initialize fallback YAML player data storage", fallbackException);
Bukkit.getPluginManager().disablePlugin(this);
return;
}
}

cleanupTask = Bukkit.getScheduler().runTaskTimerAsynchronously(this, new CleanupTask(this), 20L*300, 20L*300);

Expand All @@ -82,9 +114,7 @@ public void onEnable() {

Bukkit.getPluginManager().registerEvents(new PlayerListener(this), this);

if (cfg.loadTagOnJoin()) {
Bukkit.getPluginManager().registerEvents(new JoinListener(this), this);
}
Bukkit.getPluginManager().registerEvents(new JoinListener(this), this);

messages = new ConfigWrapper(this, null, "messages.yml");

Expand Down Expand Up @@ -125,6 +155,11 @@ public void onDisable() {
}

this.getTagsHandler().unloadData();
shutdownStorageExecutor();
if (playerDataStorage != null) {
playerDataStorage.close();
}
playerDataCache.clear();
TagGUI.unload();

guiOptions = null;
Expand Down Expand Up @@ -163,45 +198,111 @@ public static boolean papi() {
}

public String getSavedTagIdentifier(String uuid) {
FileConfiguration c = playerFile.getConfig();
if (c.contains(uuid) && c.isString(uuid) && c.getString(uuid) != null) {
return c.getString(uuid);
}
return null;
PlayerData playerData = playerDataCache.get(UUID.fromString(uuid));
return playerData == null ? null : playerData.getTagIdentifier();
}

public void saveTagIdentifier(String uuid, String tagIdentifier) {
FileConfiguration c = playerFile.getConfig();
c.set(uuid, tagIdentifier);
playerFile.saveConfig();
UUID playerUuid = UUID.fromString(uuid);
PlayerData oldData = playerDataCache.get(playerUuid);
PlayerData playerData = new PlayerData(tagIdentifier, oldData == null ? null : oldData.getData());
playerDataCache.put(playerUuid, playerData);
runStorageTask(() -> playerDataStorage.save(playerUuid, playerData));
}

public void removeSavedTag(String uuid) {
FileConfiguration c = playerFile.getConfig();
if (c.contains(uuid)) {
c.set(uuid, null);
playerFile.saveConfig();
}
UUID playerUuid = UUID.fromString(uuid);
playerDataCache.remove(playerUuid);
runStorageTask(() -> playerDataStorage.delete(playerUuid));
}

public void removeSavedTags(List<UUID> uuids) {
boolean requiresSave = false;
FileConfiguration config = playerFile.getConfig();

if (uuids == null || uuids.isEmpty()) {
return;
}

for (UUID uuid : uuids) {
if (config.contains(uuid.toString())) {
config.set(uuid.toString(), null);
requiresSave = true;
playerDataCache.remove(uuid);
runStorageTask(() -> playerDataStorage.delete(uuid));
}
}

public void loadPlayerData(Player player) {
UUID uuid = player.getUniqueId();
runStorageTask(() -> {
PlayerData playerData = playerDataStorage.load(uuid);
if (playerData == null) {
playerDataCache.remove(uuid);
} else {
playerDataCache.put(uuid, playerData);
}
if (!cfg.loadTagOnJoin()) {
return;
}
Bukkit.getScheduler().runTask(this, () -> {
Player onlinePlayer = Bukkit.getPlayer(uuid);
if (onlinePlayer != null) {
getTagsHandler().updateTagForPlayer(onlinePlayer);
}
});
});
}

public String getPlayerData(UUID uuid) {
PlayerData playerData = playerDataCache.get(uuid);
return playerData == null ? null : playerData.getData();
}

public void savePlayerData(UUID uuid, String data) {
PlayerData oldData = playerDataCache.get(uuid);
PlayerData playerData = new PlayerData(oldData == null ? null : oldData.getTagIdentifier(), data);
playerDataCache.put(uuid, playerData);
runStorageTask(() -> playerDataStorage.save(uuid, playerData));
}

private PlayerDataStorage createPlayerDataStorage() {
if (cfg.mysqlStorageEnabled()) {
return new MysqlPlayerDataStorage(getConfig().getConfigurationSection("storage.mysql"));
}
return new YamlPlayerDataStorage(playerFile);
}

private void runStorageTask(StorageTask task) {
if (storageExecutor == null) {
storageExecutor = Executors.newSingleThreadExecutor(runnable -> {
Thread thread = new Thread(runnable, "DeluxeTags-Storage");
thread.setDaemon(true);
return thread;
});
}
storageExecutor.execute(() -> {
try {
task.run();
} catch (Exception exception) {
getLogger().log(Level.SEVERE, "Player data storage operation failed", exception);
}
});
}

if (requiresSave) {
playerFile.saveConfig();
private void shutdownStorageExecutor() {
if (storageExecutor == null) {
return;
}
storageExecutor.shutdown();
try {
if (!storageExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
getLogger().warning("Timed out while waiting for player data to save");
storageExecutor.shutdownNow();
}
} catch (InterruptedException exception) {
storageExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
storageExecutor = null;
}

private interface StorageTask {
void run() throws Exception;
}

public void loadMessages() {
Expand Down
27 changes: 23 additions & 4 deletions src/main/java/me/clip/deluxetags/config/TagConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class TagConfig {
"papi_chat",
"format_chat",
"load_tag_on_join",
"storage",
"gui",
"categories",
"deluxetags"
Expand Down Expand Up @@ -141,6 +142,15 @@ public void loadDefConfig() {
config.set("force_tag_on_join", null);
}
addDefault("load_tag_on_join", true);
addDefault("storage.mysql.enabled", false);
addDefault("storage.mysql.host", "localhost");
addDefault("storage.mysql.port", 3306);
addDefault("storage.mysql.database", "deluxetags");
addDefault("storage.mysql.username", "root");
addDefault("storage.mysql.password", "password");
addDefault("storage.mysql.table_prefix", "deluxetags_");
addDefault("storage.mysql.use_ssl", false);
addDefault("storage.mysql.pool_size", 10);

migrateTagAvailabilityPlaceholder(config);

Expand Down Expand Up @@ -283,10 +293,15 @@ private void addDefault(String path, Object value) {
}

static void applySectionComments(FileConfiguration config) {
config.setComments("use_minimessage", Collections.singletonList("Main Options"));
config.setComments("gui", Arrays.asList(null, "GUI layout and buttons"));
config.setComments("categories", Arrays.asList(null, "Tag category menus"));
config.setComments("deluxetags", Arrays.asList(null, "Tags"));
try {
java.lang.reflect.Method setComments = config.getClass().getMethod("setComments", String.class, List.class);
setComments.invoke(config, "use_minimessage", Collections.singletonList("Main Options"));
setComments.invoke(config, "gui", Arrays.asList(null, "GUI layout and buttons"));
setComments.invoke(config, "categories", Arrays.asList(null, "Tag category menus"));
setComments.invoke(config, "deluxetags", Arrays.asList(null, "Tags"));
} catch (ReflectiveOperationException ignored) {
// Configuration comments are unavailable on legacy Bukkit versions.
}
}

static void migrateTagAvailabilityPlaceholder(FileConfiguration config) {
Expand Down Expand Up @@ -448,6 +463,10 @@ public boolean loadTagOnJoin() {
return config.getBoolean("load_tag_on_join");
}

public boolean mysqlStorageEnabled() {
return config.getBoolean("storage.mysql.enabled", false);
}

public boolean forceTags() {
return config.getBoolean("force_tags");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ public JoinListener(DeluxeTags instance) {

@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onChat(final PlayerJoinEvent event) {
plugin.getTagsHandler().updateTagForPlayer(event.getPlayer());
plugin.loadPlayerData(event.getPlayer());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package me.clip.deluxetags.storage;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.bukkit.configuration.ConfigurationSection;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.UUID;

public class MysqlPlayerDataStorage implements PlayerDataStorage {

private final ConfigurationSection config;
private final String tableName;
private HikariDataSource dataSource;

public MysqlPlayerDataStorage(ConfigurationSection config) {
this.config = config;
this.tableName = validateTableName(config.getString("table_prefix", "deluxetags_") + "player_data");
}

@Override
public void initialize() throws Exception {
HikariConfig hikariConfig = new HikariConfig();
String host = config.getString("host", "localhost");
int port = config.getInt("port", 3306);
String database = config.getString("database", "deluxetags");
boolean useSsl = config.getBoolean("use_ssl", false);
hikariConfig.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + database
+ "?useSSL=" + useSsl + "&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf8");
hikariConfig.setUsername(config.getString("username", "root"));
hikariConfig.setPassword(config.getString("password", "password"));
hikariConfig.setMaximumPoolSize(Math.max(1, config.getInt("pool_size", 10)));
hikariConfig.setMinimumIdle(1);
hikariConfig.setPoolName("DeluxeTags-MySQL");
hikariConfig.setConnectionTimeout(10000L);
dataSource = new HikariDataSource(hikariConfig);

try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) {
statement.executeUpdate("CREATE TABLE IF NOT EXISTS `" + tableName + "` ("
+ "`player_uuid` CHAR(36) NOT NULL,"
+ "`tag_identifier` VARCHAR(255) NULL,"
+ "`data` JSON NULL,"
+ "`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,"
+ "PRIMARY KEY (`player_uuid`)"
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
}
}

@Override
public PlayerData load(UUID uuid) throws Exception {
String sql = "SELECT `tag_identifier`, `data` FROM `" + tableName + "` WHERE `player_uuid` = ?";
try (Connection connection = dataSource.getConnection(); PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, uuid.toString());
try (ResultSet result = statement.executeQuery()) {
return result.next() ? new PlayerData(result.getString("tag_identifier"), result.getString("data")) : null;
}
}
}

@Override
public void save(UUID uuid, PlayerData playerData) throws Exception {
String sql = "INSERT INTO `" + tableName + "` (`player_uuid`, `tag_identifier`, `data`) VALUES (?, ?, ?) "
+ "ON DUPLICATE KEY UPDATE `tag_identifier` = VALUES(`tag_identifier`), `data` = VALUES(`data`)";
try (Connection connection = dataSource.getConnection(); PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, uuid.toString());
statement.setString(2, playerData.getTagIdentifier());
statement.setString(3, playerData.getData());
statement.executeUpdate();
}
}

@Override
public void delete(UUID uuid) throws Exception {
String sql = "DELETE FROM `" + tableName + "` WHERE `player_uuid` = ?";
try (Connection connection = dataSource.getConnection(); PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, uuid.toString());
statement.executeUpdate();
}
}

@Override
public void close() {
if (dataSource != null) {
dataSource.close();
}
}

private static String validateTableName(String tableName) {
if (!tableName.matches("[A-Za-z0-9_]+")) {
throw new IllegalArgumentException("MySQL table prefix may only contain letters, numbers, and underscores");
}
return tableName;
}
}
Loading