diff --git a/pom.xml b/pom.xml index f8868fc..0b757e7 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,10 @@ placeholderapi http://repo.extendedclip.com/content/repositories/placeholderapi/ + + papermc + https://papermc.io/repo/repository/maven-public/ + @@ -63,6 +67,12 @@ 2.9.2 provided + + io.papermc + paperlib + 1.0.4 + compile + \ No newline at end of file diff --git a/src/main/java/com/gmail/mezymc/stats/StatsManager.java b/src/main/java/com/gmail/mezymc/stats/StatsManager.java index a00def1..7128520 100644 --- a/src/main/java/com/gmail/mezymc/stats/StatsManager.java +++ b/src/main/java/com/gmail/mezymc/stats/StatsManager.java @@ -1,9 +1,6 @@ package com.gmail.mezymc.stats; -import com.gmail.mezymc.stats.database.DatabaseColumn; -import com.gmail.mezymc.stats.database.DatabaseConnector; -import com.gmail.mezymc.stats.database.MySqlConnector; -import com.gmail.mezymc.stats.database.Position; +import com.gmail.mezymc.stats.database.*; import com.gmail.mezymc.stats.listeners.ConnectionListener; import com.gmail.mezymc.stats.listeners.GuiListener; import com.gmail.mezymc.stats.listeners.StatCommandListener; @@ -43,6 +40,7 @@ public class StatsManager{ private boolean isUhcServer; private boolean onlineMode; private GameMode serverGameMode; + private int leaderBoardsUpdateInterval; public StatsManager(){ statsManager = this; @@ -180,19 +178,28 @@ boolean loadConfig(){ return false; } - // SQL Details not yet set, disable plugin - if (cfg.getString("sql.password", "password123").equals("password123")){ - Bukkit.getLogger().warning("[UhcStats] SQL details not set! Disabling plugin!"); + boolean useMySql = cfg.getBoolean ("use-mysql-database"); + + // If need to use MySQL but database connection details are not yet set, disable plugin + if (useMySql && cfg.getString("sql.password", "password123").equals("password123")){ + Bukkit.getLogger().severe("[UhcStats] MySQL database details not set! Disabling plugin since stats cannot be saved!"); return false; } - databaseConnector = new MySqlConnector( - cfg.getString("sql.ip", "localhost"), - cfg.getString("sql.username", "localhost"), - cfg.getString("sql.password", "password123"), - cfg.getString("sql.database", "minecraft"), - cfg.getInt("sql.port", 3306) - ); + // Connect to MySQL database if configured to do so and details are set + if(useMySql) { + databaseConnector = new MySqlConnector( + cfg.getString("sql.ip", "localhost"), + cfg.getString("sql.username", "localhost"), + cfg.getString("sql.password", "password123"), + cfg.getString("sql.database", "minecraft"), + cfg.getInt("sql.port", 3306) + ); + } + // Connect to local SQLite database if configured to do so + else { + databaseConnector = new SQLiteConnector(); + } onlineMode = cfg.getBoolean("online-mode", true); @@ -239,9 +246,14 @@ boolean loadConfig(){ Bukkit.getLogger().info("[UhcStats] Loaded " + gameModes.size() + " GameModes!"); - Bukkit.getScheduler().runTaskLater(UhcStats.getPlugin(), () -> loadLeaderBoards(cfg), 10); - } + // If this is not a UHC server (but a lobby server with this plugin installed on it), + // load the leader-boards right away since we do not need to wait for the UHC game world(s) to pre-generate + if(!isUhcServer) { + Bukkit.getScheduler().runTaskLater(UhcStats.getPlugin(), () -> loadLeaderBoards(), 10); + } + // Otherwise, they will be loaded when the game is ready + } // Load server GameMode if (isUhcServer){ @@ -254,6 +266,8 @@ boolean loadConfig(){ Bukkit.getLogger().info("[UhcStats] Server GameMode is: " + serverGameMode.getKey()); } + leaderBoardsUpdateInterval = cfg.getInt("leaderboards-update-interval", 60); + return true; } @@ -380,7 +394,7 @@ private void pushGameModeStats(StatsPlayer statsPlayer, GameMode gameMode){ databaseConnector.pushStats(statsPlayer.getId(), gameMode, statsPlayer.getGameModeStats(gameMode)); } - private void loadLeaderBoards(YamlConfiguration cfg){ + public void loadLeaderBoards(){ ConfigurationSection cfgSection = cfg.getConfigurationSection("leaderboards"); leaderBoards = new HashSet<>(); @@ -413,4 +427,8 @@ public Set getLeaderBoards(){ return leaderBoards; } + public int getLeaderBoardsUpdateInterval() { return leaderBoardsUpdateInterval; } + + public boolean getIsUhcServer() { return isUhcServer; } + } \ No newline at end of file diff --git a/src/main/java/com/gmail/mezymc/stats/UhcStats.java b/src/main/java/com/gmail/mezymc/stats/UhcStats.java index 01ddf47..a147d56 100644 --- a/src/main/java/com/gmail/mezymc/stats/UhcStats.java +++ b/src/main/java/com/gmail/mezymc/stats/UhcStats.java @@ -35,8 +35,12 @@ public void onEnable(){ @Override public void onDisable() { - for (LeaderBoard leaderBoard : StatsManager.getStatsManager().getLeaderBoards()){ - leaderBoard.unload(); + if(StatsManager.getStatsManager().getLeaderBoards() != null) { + for (LeaderBoard leaderBoard : StatsManager.getStatsManager().getLeaderBoards()) { + if(leaderBoard != null) { + leaderBoard.unload(); + } + } } } diff --git a/src/main/java/com/gmail/mezymc/stats/database/SQLiteConnector.java b/src/main/java/com/gmail/mezymc/stats/database/SQLiteConnector.java new file mode 100644 index 0000000..a467a80 --- /dev/null +++ b/src/main/java/com/gmail/mezymc/stats/database/SQLiteConnector.java @@ -0,0 +1,224 @@ +package com.gmail.mezymc.stats.database; + +import com.gmail.mezymc.stats.GameMode; +import com.gmail.mezymc.stats.StatType; +import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; + +import java.io.File; +import java.io.IOException; +import java.sql.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; + +import static com.gmail.mezymc.stats.UhcStats.getPlugin; + +public class SQLiteConnector implements DatabaseConnector { + + private Connection connection; + + public SQLiteConnector() { + connection = null; + } + + @Override + public List getTop10(StatType statType, GameMode gameMode) { + try { + Connection connection = getSqlConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT `id`, `" + statType.getColumnName() + "` FROM `" + gameMode.getTableName() + "` ORDER BY `" + statType.getColumnName() + "` DESC LIMIT 10"); + List positions = new ArrayList<>(); + + int pos = 1; + while (resultSet.next()) { + Position position = new Position( + pos, + resultSet.getString("id"), + resultSet.getInt(statType.getColumnName()) + ); + + positions.add(position); + pos++; + } + + resultSet.close(); + statement.close(); + + return positions; + } catch (SQLException ex) { + ex.printStackTrace(); + return new ArrayList<>(); + } + } + + @Override + public boolean doesTableExists(String tableName) { + Connection connection; + Statement statement; + try { + connection = getSqlConnection(); + statement = connection.createStatement(); + } catch (SQLException ex) { + ex.printStackTrace(); + throw new RuntimeException("Failed to create statement!"); + } + + try { + statement.executeQuery("SELECT 1 FROM `" + tableName + "` LIMIT 1;").close(); + statement.close(); + return true; + } catch (SQLException ex) { + return false; + } + } + + @Override + public void createTable(String name, DatabaseColumn... databaseColumns) { + StringBuilder sb = new StringBuilder("CREATE TABLE `" + name + "` ("); + boolean first = true; + + for (DatabaseColumn databaseColumn : databaseColumns) { + if (first) { + first = false; + } else { + sb.append(", "); + } + + sb.append(databaseColumn.toString()); + } + sb.append(");"); + + try { + Connection connection = getSqlConnection(); + Statement statement = connection.createStatement(); + statement.execute(sb.toString()); + statement.close(); + } catch (SQLException ex) { + Bukkit.getLogger().warning("[UhcStats] Failed to create table!"); + ex.printStackTrace(); + } + } + + @Override + public void pushStats(String playerId, GameMode gameMode, Map stats) { + try { + Connection connection = getSqlConnection(); + Statement statement = connection.createStatement(); + for (StatType statType : stats.keySet()) { + statement.executeUpdate( + "UPDATE `" + gameMode.getTableName() + "` SET `" + statType.getColumnName() + "`=" + stats.get(statType) + " WHERE `id`='" + playerId + "'" + ); + } + statement.close(); + } catch (SQLException ex) { + Bukkit.getLogger().warning("[UhcStats] Failed to push stats for: " + playerId); + ex.printStackTrace(); + } + } + + @Override + public Map loadStats(String playerId, GameMode gameMode) { + Map stats = getEmptyStatMap(); + + try { + Connection connection = getSqlConnection(); + Statement statement = connection.createStatement(); + ResultSet result = statement.executeQuery("SELECT * FROM `" + gameMode.getTableName() + "` WHERE `id`='" + playerId + "'"); + + if (result.next()) { + // collect stats + for (StatType statType : StatType.values()) { + stats.put(statType, result.getInt(statType.getColumnName())); + } + } else { + // Player not found, insert player to table. + insertPlayerToTable(connection, playerId, gameMode); + } + + result.close(); + statement.close(); + } catch (SQLException ex) { + ex.printStackTrace(); + } + + return stats; + } + + @Override + public boolean checkConnection() { + try { + getSqlConnection(); + return true; + } catch (SQLException ex) { + ex.printStackTrace(); + return false; + } + } + + private Connection getSqlConnection() throws SQLException { + Validate.isTrue(!Bukkit.isPrimaryThread(), "You may only open an connection to the database on a asynchronous thread!"); + + // Open connection to local SQLite database "database.db" + File dataFile = new File(getPlugin().getDataFolder(), "database.db"); + if (!dataFile.exists()) { + try { + dataFile.createNewFile(); + } catch (IOException e) { + Bukkit.getLogger().log(Level.SEVERE, "File write error: database.db"); + } + } + try { + if (connection != null && !connection.isClosed()) { + return connection; + } + Class.forName("org.sqlite.JDBC"); + connection = DriverManager.getConnection("jdbc:sqlite:" + dataFile); + return connection; + } catch (SQLException ex) { + Bukkit.getLogger().log(Level.SEVERE, "SQLite exception on initialize", ex); + } catch (ClassNotFoundException ex) { + Bukkit.getLogger().log(Level.SEVERE, "You need the SQLite JBDC library."); + } + + return null; + } + + private void insertPlayerToTable(Connection connection, String playerId, GameMode gameMode) { + try { + StringBuilder sb = new StringBuilder("INSERT INTO `" + gameMode.getTableName() + "` (`id`"); + for (StatType statType : StatType.values()) { + sb.append(", `" + statType.getColumnName() + "`"); + } + + sb.append(") VALUES ('" + playerId + "'"); + + for (int i = 0; i < StatType.values().length; i++) { + sb.append(", 0"); + } + + sb.append(")"); + + Statement statement = connection.createStatement(); + statement.execute(sb.toString()); + + statement.close(); + } catch (SQLException ex) { + Bukkit.getLogger().warning("[UhcStats] Failed to update stats for: " + playerId); + ex.printStackTrace(); + } + } + + private Map getEmptyStatMap() { + Map stats = new HashMap<>(); + + for (StatType statType : StatType.values()) { + stats.put(statType, 0); + } + + return stats; + } + +} \ No newline at end of file diff --git a/src/main/java/com/gmail/mezymc/stats/listeners/UhcStatListener.java b/src/main/java/com/gmail/mezymc/stats/listeners/UhcStatListener.java index 0d047dc..bbc06a5 100644 --- a/src/main/java/com/gmail/mezymc/stats/listeners/UhcStatListener.java +++ b/src/main/java/com/gmail/mezymc/stats/listeners/UhcStatListener.java @@ -1,7 +1,9 @@ package com.gmail.mezymc.stats.listeners; import com.gmail.mezymc.stats.*; +import com.gmail.val59000mc.events.UhcGameStateChangedEvent; import com.gmail.val59000mc.events.UhcWinEvent; +import com.gmail.val59000mc.game.GameState; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -46,4 +48,17 @@ public void onGameWin(UhcWinEvent e){ Bukkit.getScheduler().runTaskAsynchronously(UhcStats.getPlugin(), () -> statsManager.pushAllStats()); } + @EventHandler + public void onGameStateChangedEvent(UhcGameStateChangedEvent e){ + // If this is a UHC server (and not a lobby server with this plugin installed on it), + // load the leader-boards only after the the UHC game world(s) are pre-generated + if(StatsManager.getStatsManager().getIsUhcServer()) { + // When the game is waiting for players (world(s) have finished being pre-generated), load the leaderboards + // This is so that the leaderboards can go in the correct world if the default lobby is used + if (e.getNewGameState() == GameState.WAITING) { + Bukkit.getScheduler().runTaskLater(UhcStats.getPlugin(), () -> StatsManager.getStatsManager().loadLeaderBoards(), 10); + } + } + } + } \ No newline at end of file diff --git a/src/main/java/com/gmail/mezymc/stats/scoreboards/BoardPosition.java b/src/main/java/com/gmail/mezymc/stats/scoreboards/BoardPosition.java index bfeb1ab..c84cffd 100644 --- a/src/main/java/com/gmail/mezymc/stats/scoreboards/BoardPosition.java +++ b/src/main/java/com/gmail/mezymc/stats/scoreboards/BoardPosition.java @@ -72,7 +72,10 @@ public void updateText() { } public void remove(){ - armorStand.remove(); + // Only remove armor stand if it was created in the first place + if(armorStand != null) { + armorStand.remove(); + } } public Location getLocation(){ @@ -98,11 +101,17 @@ public boolean ownsArmorStand(ArmorStand armorStand){ private void spawnArmorStand(){ Location location = getLocation(); - for (Entity entity : location.getWorld().getNearbyEntities(location,1,1,1)){ - if (entity.getType() == EntityType.ARMOR_STAND && entity.getLocation().equals(location)){ - armorStand = (ArmorStand) entity; + // getNearbyEntities() must be used synchronously + LeaderboardUpdateThread.runSync(new Runnable() { + @Override + public void run() { + for (Entity entity : location.getWorld().getNearbyEntities(location, 1, 1, 1)){ + if (entity.getType() == EntityType.ARMOR_STAND && entity.getLocation().equals(location)){ + armorStand = (ArmorStand) entity; + } + } } - } + }); if (armorStand == null){ LeaderboardUpdateThread.runSync(new Runnable() { diff --git a/src/main/java/com/gmail/mezymc/stats/scoreboards/LeaderBoard.java b/src/main/java/com/gmail/mezymc/stats/scoreboards/LeaderBoard.java index 8d1272d..e6cbbb4 100644 --- a/src/main/java/com/gmail/mezymc/stats/scoreboards/LeaderBoard.java +++ b/src/main/java/com/gmail/mezymc/stats/scoreboards/LeaderBoard.java @@ -3,9 +3,7 @@ import com.gmail.mezymc.stats.GameMode; import com.gmail.mezymc.stats.StatType; import com.gmail.mezymc.stats.StatsManager; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.Location; +import org.bukkit.*; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.ArmorStand; import org.bukkit.entity.Entity; @@ -41,16 +39,49 @@ public Location getLocation() { public void instantiate(ConfigurationSection cfg){ + String title = cfg.getString("title"); + format = cfg.getString("lines"); + + boolean didFindWorld = false; + World worldForLeaderboard = Bukkit.getWorlds().get(0); + String configWorldName = cfg.getString("location.world"); + + // If this leader-board specifies which world it should be created in, create it in that world + if(configWorldName != null) { + worldForLeaderboard = Bukkit.getWorld(configWorldName); + if(worldForLeaderboard == null) { + Bukkit.getLogger().warning("[UhcStats] World \"" + configWorldName + "\" for leaderboard titled \"" + title + "\" is invalid"); + Bukkit.getLogger().warning("[UhcStats] Will attempt to place it in default lobby world..."); + } else { + didFindWorld = true; + } + } + // Otherwise, search for the world containing the default lobby to place the leader-board into + if(!didFindWorld) { + for (World world : Bukkit.getWorlds()) { + // The world must be in the overworld + if (world.getEnvironment() == World.Environment.NORMAL) { + // The correct one is the one with the glass box (the default lobby) + if (world.getBlockAt(0, 199, 0).getType() == Material.GLASS) { + worldForLeaderboard = world; + didFindWorld = true; + } + } + } + } + // If the correct world could not be found for leader-board, issue a warning + if(!didFindWorld) { + Bukkit.getLogger().warning("[UhcStats] Could not find the default lobby world for leaderboard titled \"" + title + "\""); + Bukkit.getLogger().warning("[UhcStats] Using the first available one instead"); + } + location = new Location( - Bukkit.getWorld(cfg.getString("location.world")), + worldForLeaderboard, cfg.getDouble("location.x"), cfg.getDouble("location.y"), cfg.getDouble("location.z") ); - String title = cfg.getString("title"); - format = cfg.getString("lines"); - title = ChatColor.translateAlternateColorCodes('&', title); format = ChatColor.translateAlternateColorCodes('&', format); @@ -59,6 +90,7 @@ public void instantiate(ConfigurationSection cfg){ title ); armorStand2 = null; + } public String getFormat(){ @@ -88,7 +120,9 @@ private ArmorStand spawnArmorStand(Location location, String text){ } public void unload(){ - armorStand1.remove(); + if (armorStand1 != null) { + armorStand1.remove(); + } if (armorStand2 != null) { armorStand2.remove(); } diff --git a/src/main/java/com/gmail/mezymc/stats/scoreboards/LeaderboardUpdateThread.java b/src/main/java/com/gmail/mezymc/stats/scoreboards/LeaderboardUpdateThread.java index 49db5af..386ea0a 100644 --- a/src/main/java/com/gmail/mezymc/stats/scoreboards/LeaderboardUpdateThread.java +++ b/src/main/java/com/gmail/mezymc/stats/scoreboards/LeaderboardUpdateThread.java @@ -22,8 +22,11 @@ public void run() { } } - // re-run - Bukkit.getScheduler().runTaskLaterAsynchronously(UhcStats.getPlugin(), this, 20*60); + // Re-run if need be + int leaderboardsUpdateInterval = statsManager.getLeaderBoardsUpdateInterval(); + if(leaderboardsUpdateInterval > 0) { + Bukkit.getScheduler().runTaskLaterAsynchronously(UhcStats.getPlugin(), this, 20 * leaderboardsUpdateInterval); + } } public static void runSync(Runnable runnable){ diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index b0ae826..0ba453b 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,3 +1,25 @@ +# Recommended config options + +# For a hub / lobby server in a bungeecord server network: +# use-mysql-database: true +# `gamemodes` section must be populated with corresponding gamemode(s) from your actual UHC game server(s) + +# For a UHC server in a bungeecord server network: +# use-mysql-database: true +# Set `server-gamemode` to type of game being hosted on this UHC server + +# For a standalone UHC server: +# use-mysql-database: false +# Either remove `gamemodes` section or set `server-gamemode` to `uhc` +# Set each leaderboard's `gamemode` to `uhc` +# Remove each leaderboard's `world` section if not using a custom lobby + + +# Database type to use. +# true: use external MySQL database (primarily for server networks) +# false: use local SQLite database file "plugins/UhcStats/database.db" (primarily for standalone servers) +use-mysql-database: true + # MySql details, in this database the stats will be stored. sql: ip: 'localhost' @@ -15,7 +37,8 @@ stats-command: '/stats' gui-title: '&6&lUHC Stats' -# If you have multiple UHC GameModes on your server you can configure them here. If you only have one you can delete this section. +# If you are using a server network (e.g. bungeecord) with multiple UHC servers, you can configure them here. +# If you only have one standalone UHC server you should delete this section. gamemodes: # The current server GameMode. If this is a UHC server under what GameMode should the statistics be saved? server-gamemode: 'cutclean' @@ -30,19 +53,31 @@ gamemodes: name: '&aUhc Run' display-item: 'DIAMOND_PICKAXE' +# How often to update the leader-boards, in seconds +# Set to 0 to disable +leaderboards-update-interval: 60 + +# Set this according to the example configuration section below +# (You can delete this line and adapt the example section to your needs) leaderboards: {} leaderboards-sample: board-1: # Choose from: KILL, DEATH and WIN stat-type: KILL + # If gamemodes section above is not used, set the gamemode below to "uhc". gamemode: cutclean + # Title for this leader-board (can use formatting codes). title: '&aCutClean top 10 kills' - # Layout of the leader-board lines. + # Layout of the leader-board lines (can use formatting codes). lines: '&a%number%. %player%: %count%' # Location where the leader-board should spawn. location: + # Remove the "world: world" line to place this leader-board in the default lobby + # Otherwise, set the world this leader-board should be created in + # If used on a standalone server with the default lobby, this line should be removed world: world + # Coordinates in world to create the leader-board at ([0x 202y 0z] is the center of the default glass box lobby) x: 0 - y: 50 + y: 202 z: 0 \ No newline at end of file