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