From dbac21aa4490756ddbccdddb7b323722a1466937 Mon Sep 17 00:00:00 2001 From: cmastudios Date: Fri, 9 Aug 2013 23:30:46 -0500 Subject: [PATCH] Add MySQL kill/death logging support. Closes gh-658 War now logs kills and deaths to a MySQL database. Records are created for every player at the end of each round with the current date and amount of times the player killed and was killed for the entire round. It has built in support for automatic & configurable log clearing past a certain date (default is 1 week). --- war/src/main/java/com/tommytony/war/War.java | 26 ++++- .../main/java/com/tommytony/war/Warzone.java | 23 ++++ .../com/tommytony/war/config/MySQLConfig.java | 102 ++++++++++++++++++ .../war/event/WarEntityListener.java | 2 + .../tommytony/war/job/LogKillsDeathsJob.java | 95 ++++++++++++++++ .../tommytony/war/job/ScoreCapReachedJob.java | 6 ++ .../tommytony/war/mapper/WarYmlMapper.java | 13 ++- 7 files changed, 262 insertions(+), 5 deletions(-) create mode 100644 war/src/main/java/com/tommytony/war/config/MySQLConfig.java create mode 100644 war/src/main/java/com/tommytony/war/job/LogKillsDeathsJob.java diff --git a/war/src/main/java/com/tommytony/war/War.java b/war/src/main/java/com/tommytony/war/War.java index 06b260b..7b3bd8c 100644 --- a/war/src/main/java/com/tommytony/war/War.java +++ b/war/src/main/java/com/tommytony/war/War.java @@ -30,6 +30,7 @@ import com.tommytony.war.config.FlagReturn; import com.tommytony.war.config.InventoryBag; import com.tommytony.war.config.ScoreboardType; import com.tommytony.war.config.KillstreakReward; +import com.tommytony.war.config.MySQLConfig; import com.tommytony.war.config.TeamConfig; import com.tommytony.war.config.TeamConfigBag; import com.tommytony.war.config.TeamKind; @@ -98,6 +99,7 @@ public class War extends JavaPlugin { private final InventoryBag defaultInventories = new InventoryBag(); private KillstreakReward killstreakReward; + private MySQLConfig mysqlConfig; private final WarConfigBag warConfig = new WarConfigBag(); private final WarzoneConfigBag warzoneDefaultConfig = new WarzoneConfigBag(); @@ -240,6 +242,7 @@ public class War extends JavaPlugin { this.getCommandWhitelist().add("who"); this.getZoneMakerNames().add("tommytony"); this.setKillstreakReward(new KillstreakReward()); + this.setMysqlConfig(new MySQLConfig()); // Add constants this.getDeadlyAdjectives().clear(); @@ -273,6 +276,14 @@ public class War extends JavaPlugin { SpoutFadeOutMessageJob fadeOutMessagesTask = new SpoutFadeOutMessageJob(); this.getServer().getScheduler().scheduleSyncRepeatingTask(this, fadeOutMessagesTask, 100, 100); } + if (this.mysqlConfig.isEnabled()) { + try { + Class.forName("com.mysql.jdbc.Driver").newInstance(); + } catch (Exception ex) { + this.log("MySQL driver not found!", Level.SEVERE); + this.getServer().getPluginManager().disablePlugin(this); + } + } // Get own log file try { @@ -286,6 +297,7 @@ public class War extends JavaPlugin { Formatter formatter = new WarLogFormatter(); handler.setFormatter(formatter); this.warLogger.addHandler(handler); + this.getLogger().addHandler(handler); } catch (IOException e) { this.getLogger().log(Level.WARNING, "Failed to create War log file"); } @@ -986,9 +998,9 @@ public class War extends JavaPlugin { // Log to Bukkit console this.getLogger().log(lvl, str); - if (this.warLogger != null) { - this.warLogger.log(lvl, str); - } +// if (this.warLogger != null) { +// this.warLogger.log(lvl, str); +// } } // the only way to find a zone that has only one corner @@ -1245,4 +1257,12 @@ public class War extends JavaPlugin { public void setKillstreakReward(KillstreakReward killstreakReward) { this.killstreakReward = killstreakReward; } + + public MySQLConfig getMysqlConfig() { + return mysqlConfig; + } + + public void setMysqlConfig(MySQLConfig mysqlConfig) { + this.mysqlConfig = mysqlConfig; + } } diff --git a/war/src/main/java/com/tommytony/war/Warzone.java b/war/src/main/java/com/tommytony/war/Warzone.java index 43903b6..eb898d7 100644 --- a/war/src/main/java/com/tommytony/war/Warzone.java +++ b/war/src/main/java/com/tommytony/war/Warzone.java @@ -32,6 +32,8 @@ import com.tommytony.war.config.WarzoneConfig; import com.tommytony.war.config.WarzoneConfigBag; import com.tommytony.war.job.InitZoneJob; import com.tommytony.war.job.LoadoutResetJob; +import com.tommytony.war.job.LogKillsDeathsJob; +import com.tommytony.war.job.LogKillsDeathsJob.KillsDeathsRecord; import com.tommytony.war.job.ScoreCapReachedJob; import com.tommytony.war.mapper.LoadoutYmlMapper; import com.tommytony.war.spout.SpoutDisplayer; @@ -56,6 +58,8 @@ import org.bukkit.OfflinePlayer; import org.bukkit.scoreboard.DisplaySlot; import org.bukkit.scoreboard.Objective; import org.bukkit.scoreboard.Scoreboard; +import java.util.Map; +import org.bukkit.OfflinePlayer; import org.bukkit.inventory.meta.LeatherArmorMeta; /** @@ -88,6 +92,8 @@ public class Warzone { private HashMap killCount = new HashMap(); private final List respawn = new ArrayList(); private final List reallyDeadFighters = new ArrayList(); + + private List killsDeathsTracker = new ArrayList(); private final WarzoneConfigBag warzoneConfig; private final TeamConfigBag teamDefaultConfig; @@ -1592,4 +1598,21 @@ public class Warzone { } killCount.put(player, killCount.get(player) + amount); } + + public void addKillDeathRecord(OfflinePlayer player, int kills, int deaths) { + for (Iterator it = this.killsDeathsTracker.iterator(); it.hasNext();) { + LogKillsDeathsJob.KillsDeathsRecord kdr = it.next(); + if (kdr.getPlayer().equals(player)) { + kills += kdr.getKills(); + deaths += kdr.getDeaths(); + it.remove(); + } + } + LogKillsDeathsJob.KillsDeathsRecord kdr = new LogKillsDeathsJob.KillsDeathsRecord(player, kills, deaths); + this.killsDeathsTracker.add(kdr); + } + + public List getKillsDeathsTracker() { + return killsDeathsTracker; + } } diff --git a/war/src/main/java/com/tommytony/war/config/MySQLConfig.java b/war/src/main/java/com/tommytony/war/config/MySQLConfig.java new file mode 100644 index 0000000..a1bfb6c --- /dev/null +++ b/war/src/main/java/com/tommytony/war/config/MySQLConfig.java @@ -0,0 +1,102 @@ +package com.tommytony.war.config; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Map; +import org.apache.commons.lang.Validate; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.MemoryConfiguration; + +/** + * Storage class for MySQL configuration settings. + * + * @author cmastudios + */ +public class MySQLConfig { + + private ConfigurationSection section; + + /** + * Load the values from the specified section into the MySQL config. + * + * @param section Section to load MySQL settings from. + */ + public MySQLConfig(ConfigurationSection section) { + this.section = section; + } + + /** + * Create a new MySQL configuration section with default values. + */ + public MySQLConfig() { + this(new MemoryConfiguration()); + section.set("enabled", false); + section.set("host", "localhost"); + section.set("port", 3306); + section.set("database", "war"); + section.set("username", "root"); + section.set("password", "meow"); + section.set("logging.enabled", false); + section.set("logging.autoclear", + "WHERE `date` < NOW() - INTERVAL 7 DAY"); + } + + /** + * Check if MySQL support is enabled. + * + * @return true if MySQL support is enabled, false otherwise. + */ + public boolean isEnabled() { + return section.getBoolean("enabled"); + } + + /** + * Check if kill-death logging is enabled. + * + * @return true if kill-death logging is enabled, false otherwise. + */ + public boolean isLoggingEnabled() { + return section.getBoolean("logging.enabled"); + } + + /** + * Get WHERE clause for automatic deletion from database table. + * + * @return deletion WHERE clause or empty string. + */ + public String getLoggingDeleteClause() { + return section.getString("logging.autoclear"); + } + + private String getJDBCUrl() { + return String.format("jdbc:mysql://%s:%d/%s?user=%s&password=%s", + section.getString("host"), section.getInt("port"), + section.getString("database"), section.getString("username"), + section.getString("password")); + } + + /** + * Get a connection to the MySQL database represented by this configuration. + * + * @return connection to MySQL database. + * @throws SQLException Error occured connecting to database. + * @throws IllegalArgumentException MySQL support is not enabled. + */ + public Connection getConnection() throws SQLException { + Validate.isTrue(this.isEnabled(), "MySQL support is not enabled"); + return DriverManager.getConnection(this.getJDBCUrl()); + } + + /** + * Copy represented configuration into another configuration section. + * + * @param section Mutable section to write values in. + */ + public void saveTo(ConfigurationSection section) { + Map values = this.section.getValues(true); + for (Map.Entry entry : values.entrySet()) { + section.set(entry.getKey(), entry.getValue()); + } + } +} diff --git a/war/src/main/java/com/tommytony/war/event/WarEntityListener.java b/war/src/main/java/com/tommytony/war/event/WarEntityListener.java index 47d88a2..d2d4400 100644 --- a/war/src/main/java/com/tommytony/war/event/WarEntityListener.java +++ b/war/src/main/java/com/tommytony/war/event/WarEntityListener.java @@ -182,6 +182,8 @@ public class WarEntityListener implements Listener { defenderWarzone.handleDeath(d); if (attacker.getEntityId() != defender.getEntityId()) { defenderWarzone.addKillCount(a.getName(), 1); + defenderWarzone.addKillDeathRecord(a, 1, 0); + defenderWarzone.addKillDeathRecord(d, 0, 1); if (attackerTeam.getTeamConfig().resolveBoolean(TeamConfig.XPKILLMETER)) { a.setLevel(defenderWarzone.getKillCount(a.getName())); } diff --git a/war/src/main/java/com/tommytony/war/job/LogKillsDeathsJob.java b/war/src/main/java/com/tommytony/war/job/LogKillsDeathsJob.java new file mode 100644 index 0000000..0e9da61 --- /dev/null +++ b/war/src/main/java/com/tommytony/war/job/LogKillsDeathsJob.java @@ -0,0 +1,95 @@ +package com.tommytony.war.job; + +import com.google.common.collect.ImmutableList; +import com.tommytony.war.War; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.logging.Level; +import org.bukkit.OfflinePlayer; +import org.bukkit.scheduler.BukkitRunnable; + +/** + * Job to insert kills and deaths information to MySQL database. + * + * @author cmastudios + */ +public class LogKillsDeathsJob extends BukkitRunnable { + + private final ImmutableList records; + + public LogKillsDeathsJob(final ImmutableList records) { + this.records = records; + } + + @Override + /** + * Adds all #records to database at #databaseURL. Will attempt to open a + * connection to the database at #databaseURL. This method is thread safe. + */ + public void run() { + Connection conn = null; + try { + conn = War.war.getMysqlConfig().getConnection(); + Statement createStmt = conn.createStatement(); + createStmt.executeUpdate("CREATE TABLE IF NOT EXISTS `war_kills` (`date` datetime NOT NULL, `player` varchar(16) NOT NULL, `kills` int(11) NOT NULL, `deaths` int(11) NOT NULL, KEY `date` (`date`)) ENGINE=InnoDB DEFAULT CHARSET=latin1"); + createStmt.close(); + PreparedStatement stmt = conn.prepareStatement("INSERT INTO war_kills (date, player, kills, deaths) VALUES (NOW(), ?, ?, ?)"); + conn.setAutoCommit(false); + for (KillsDeathsRecord kdr : records) { + stmt.setString(1, kdr.getPlayer().getName()); + stmt.setInt(2, kdr.getKills()); + stmt.setInt(3, kdr.getDeaths()); + stmt.addBatch(); + } + stmt.executeBatch(); + conn.commit(); + stmt.close(); + final String deleteClause = + War.war.getMysqlConfig().getLoggingDeleteClause(); + if (!deleteClause.isEmpty()) { + Statement deleteStmt = conn.createStatement(); + deleteStmt.executeUpdate( + "DELETE FROM war_kills " + deleteClause); + deleteStmt.close(); + conn.commit(); + } + } catch (SQLException ex) { + War.war.getLogger().log(Level.SEVERE, + "Inserting kill-death logs into database", ex); + } finally { + if (conn != null) { + try { + conn.close(); + } catch (SQLException ex) { + } + } + } + } + + public static final class KillsDeathsRecord { + + private final OfflinePlayer player; + private final int kills; + private final int deaths; + + public KillsDeathsRecord(OfflinePlayer player, int kills, int deaths) { + this.player = player; + this.kills = kills; + this.deaths = deaths; + } + + public OfflinePlayer getPlayer() { + return player; + } + + public int getKills() { + return kills; + } + + public int getDeaths() { + return deaths; + } + } +} diff --git a/war/src/main/java/com/tommytony/war/job/ScoreCapReachedJob.java b/war/src/main/java/com/tommytony/war/job/ScoreCapReachedJob.java index 508c4e3..53e5f11 100644 --- a/war/src/main/java/com/tommytony/war/job/ScoreCapReachedJob.java +++ b/war/src/main/java/com/tommytony/war/job/ScoreCapReachedJob.java @@ -1,5 +1,6 @@ package com.tommytony.war.job; +import com.google.common.collect.ImmutableList; import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.entity.Player; @@ -63,5 +64,10 @@ public class ScoreCapReachedJob implements Runnable { t.resetPoints(); t.getPlayers().clear(); // empty the team } + if (War.war.getMysqlConfig().isEnabled() && War.war.getMysqlConfig().isLoggingEnabled()) { + LogKillsDeathsJob logKillsDeathsJob = new LogKillsDeathsJob(ImmutableList.copyOf(zone.getKillsDeathsTracker())); + War.war.getServer().getScheduler().runTaskAsynchronously(War.war, logKillsDeathsJob); + } + zone.getKillsDeathsTracker().clear(); } } diff --git a/war/src/main/java/com/tommytony/war/mapper/WarYmlMapper.java b/war/src/main/java/com/tommytony/war/mapper/WarYmlMapper.java index a5a0432..06b34a4 100644 --- a/war/src/main/java/com/tommytony/war/mapper/WarYmlMapper.java +++ b/war/src/main/java/com/tommytony/war/mapper/WarYmlMapper.java @@ -15,6 +15,7 @@ import org.bukkit.inventory.ItemStack; import com.tommytony.war.War; import com.tommytony.war.Warzone; import com.tommytony.war.config.KillstreakReward; +import com.tommytony.war.config.MySQLConfig; import com.tommytony.war.job.RestoreYmlWarhubJob; import com.tommytony.war.job.RestoreYmlWarzonesJob; import com.tommytony.war.structure.WarHub; @@ -101,8 +102,13 @@ public class WarYmlMapper { } // Killstreak config - ConfigurationSection killstreakSection = warRootSection.getConfigurationSection("war.killstreak"); - War.war.setKillstreakReward(new KillstreakReward(killstreakSection)); + if (warRootSection.isConfigurationSection("war.killstreak")) { + War.war.setKillstreakReward(new KillstreakReward(warRootSection.getConfigurationSection("war.killstreak"))); + } + + if (warRootSection.isConfigurationSection("war.mysql")) { + War.war.setMysqlConfig(new MySQLConfig(warRootSection.getConfigurationSection("war.mysql"))); + } } public static void save() { @@ -197,6 +203,9 @@ public class WarYmlMapper { ConfigurationSection killstreakSection = warRootSection.createSection("war.killstreak"); War.war.getKillstreakReward().saveTo(killstreakSection); + ConfigurationSection mysqlSection = warRootSection.createSection("war.mysql"); + War.war.getMysqlConfig().saveTo(mysqlSection); + // Save to disk File warConfigFile = new File(War.war.getDataFolder().getPath() + "/war.yml"); try {