From 4f44c24ef14fb0f3763c16f42b3834aac7c823f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bro=C4=8Dko?= Date: Sat, 6 Oct 2018 03:03:07 +0200 Subject: [PATCH] Stats: playtime and PvP statistics --- Stats/resources/activity_update.sql | 8 + Stats/resources/create.sql | 18 +- Stats/resources/players_update.sql | 7 + Stats/src/cz/marwland/mc/features/Stats.java | 161 ++++++++++++++++-- .../mc/features/stats/PlayerCounter.java | 87 ++++++++++ 5 files changed, 265 insertions(+), 16 deletions(-) create mode 100644 Stats/resources/activity_update.sql create mode 100644 Stats/resources/players_update.sql create mode 100644 Stats/src/cz/marwland/mc/features/stats/PlayerCounter.java diff --git a/Stats/resources/activity_update.sql b/Stats/resources/activity_update.sql new file mode 100644 index 0000000..80a5763 --- /dev/null +++ b/Stats/resources/activity_update.sql @@ -0,0 +1,8 @@ +INSERT INTO `{prefix}activity` + (`uuid`, `date`, `kills`, `deaths`, `playtime`) VALUES + (?, ?, ?, ?, ?) + ON DUPLICATE KEY + UPDATE + `kills` = `kills` + VALUES(`kills`), + `deaths` = `deaths` + VALUES(`deaths`), + `playtime` = `playtime` + VALUES(`playtime`); \ No newline at end of file diff --git a/Stats/resources/create.sql b/Stats/resources/create.sql index 4b1f2e4..f1ac1e2 100644 --- a/Stats/resources/create.sql +++ b/Stats/resources/create.sql @@ -1,9 +1,19 @@ CREATE TABLE IF NOT EXISTS `{prefix}players` ( - `id` int UNSIGNED NOT NULL AUTO_INCREMENT, + `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, `uuid` binary(16) NOT NULL, `serverNick` varchar(32) NOT NULL, + `voteCount` int UNSIGNED DEFAULT 0, + PRIMARY KEY `id` (`id`), + UNIQUE KEY `uuid` (`uuid`) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `{prefix}activity` ( + `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, + `uuid` binary(16) NOT NULL, + `date` DATE NOT NULL, `kills` int DEFAULT 0, `deaths` int UNSIGNED DEFAULT 0, - `voteCount` int UNSIGNED DEFAULT 0, - PRIMARY KEY `id` (`id`) -) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; \ No newline at end of file + `playtime` mediumint UNSIGNED DEFAULT 0, + PRIMARY KEY `id` (`id`), + UNIQUE KEY `date_player` (`uuid`, `date`) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/Stats/resources/players_update.sql b/Stats/resources/players_update.sql new file mode 100644 index 0000000..99a7b7b --- /dev/null +++ b/Stats/resources/players_update.sql @@ -0,0 +1,7 @@ +INSERT INTO `{prefix}players` + (`uuid`, `serverNick`) VALUES + (?, ?) + ON DUPLICATE KEY + UPDATE + `serverNick` = VALUES(`serverNick`), + `voteCount` = `voteCount` + VALUES(`voteCount`); \ No newline at end of file diff --git a/Stats/src/cz/marwland/mc/features/Stats.java b/Stats/src/cz/marwland/mc/features/Stats.java index 6be2a57..a9f004a 100644 --- a/Stats/src/cz/marwland/mc/features/Stats.java +++ b/Stats/src/cz/marwland/mc/features/Stats.java @@ -1,54 +1,191 @@ package cz.marwland.mc.features; import java.io.IOException; +import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Arrays; +import java.util.HashMap; +import java.util.logging.Level; import org.bukkit.command.CommandSender; import org.bukkit.command.defaults.BukkitCommand; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; import cz.marwland.mc.core.MarwCore; import cz.marwland.mc.core.features.Feature; import cz.marwland.mc.core.storage.SQLStorage; +import cz.marwland.mc.core.util.FileUtil; +import cz.marwland.mc.core.util.UuidUtils; +import cz.marwland.mc.features.stats.PlayerCounter; public class Stats extends Feature { - - private final Class parentClass = this.getClass(); + private final SQLStorage database = MarwCore.getInstance().getStorage(); - + public HashMap counters = new HashMap<>(); + private final long WRITE_PERIOD_TICKS = 20 * 15; + private String playersUpdateQuery; + private BukkitTask recordTask; + public Stats() { super(); - this.addCommand(new BukkitCommand( - "mwstats", - "Manages player's statistics.", - "/mwstats", - Arrays.asList() ) { + this.addCommand(new BukkitCommand("mwstats", "Manages player's statistics.", "/mwstats", Arrays.asList("mws")) { @Override public boolean execute(CommandSender sender, String commandLabel, String[] args) { + if (!permissionMissingCheck(sender, this.getPermission())) return true; - + return true; } - + @Override public String getPermission() { return getPermissionPath(); } }); } - + @Override public void onEnable() { + super.onEnable(); createTables(); + loadSQLQueries(); + PlayerCounter.init(this.database); + + new BukkitRunnable() { + @Override + public void run() { + for (Player p : MarwCore.getInstance().getServer().getOnlinePlayers()) { + recordPlayer(p); + } + } + }.runTaskAsynchronously(MarwCore.getInstance()); + for (Player p : MarwCore.getInstance().getServer().getOnlinePlayers()) { + this.startCountingPlayer(p); + } + + recordTask = new BukkitRunnable() { + @Override + public void run() { + pushActivityToSql(); + } + }.runTaskTimerAsynchronously(MarwCore.getInstance(), 5, WRITE_PERIOD_TICKS); } + @Override + public void onDisable() { + super.onDisable(); + recordTask.cancel(); + for (Player p : MarwCore.getInstance().getServer().getOnlinePlayers()) { + this.stopCountingPlayer(p); + } + } + public void createTables() { try { - database.executeRaw(parentClass.getResourceAsStream("/resources/create.sql")); + database.executeRaw(this.getClass().getResourceAsStream("/resources/create.sql")); } catch (SQLException | IOException e) { e.printStackTrace(); } } + public void loadSQLQueries() { + if (playersUpdateQuery == null) { + try { + playersUpdateQuery = FileUtil.readFileFromJAR(PlayerCounter.class, "/resources/players_update.sql"); + playersUpdateQuery = this.database.getStatementProcessor().apply(playersUpdateQuery); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public void recordPlayer(Player p) { + try(Connection conn = this.database.getConnectionFactory().getConnection()) { + PreparedStatement prepst = conn.prepareStatement(this.playersUpdateQuery); + prepst.setBytes(1, UuidUtils.asBytes(p.getUniqueId())); + prepst.setString(2, p.getName()); + prepst.execute(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + Player p = event.getPlayer(); + new BukkitRunnable() { + @Override + public void run() { + recordPlayer(p); + } + }.runTaskAsynchronously(MarwCore.getInstance()); + this.startCountingPlayer(event.getPlayer()); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + this.stopCountingPlayer(event.getPlayer()); + } + + @EventHandler + public void onPlayerDeath(PlayerDeathEvent event) { + PlayerCounter targetPC = this.counters.get(event.getEntity()); + if (targetPC != null) { + targetPC.addDeath(); + } + Entity killerEntity = event.getEntity().getLastDamageCause().getEntity(); + if (killerEntity instanceof Player) { + Player killerPlayer = (Player) killerEntity; + PlayerCounter killerPC = this.counters.get(killerPlayer); + if (killerPC != null) { + killerPC.addKill(); + } // else should never happen in both cases - players are always tracked + } + } + + public void startCountingPlayer(Player p) { + if (this.counters.containsKey(p)) + this.counters.get(p).start(); + else { + PlayerCounter pc = new PlayerCounter(p); + this.counters.put(p, pc); + pc.start(); + } + } + + public void stopCountingPlayer(Player p) { + if (!this.counters.containsKey(p)) + return; + PlayerCounter pc = this.counters.get(p); + pc.stop(); + this.counters.remove(p); + } + + public void pushActivityToSql() { + try(Connection conn = this.database.getConnectionFactory().getConnection();) { + PreparedStatement prepst = conn.prepareStatement(PlayerCounter.getRawQuery()); + int i = 0; + for (PlayerCounter pc : this.counters.values()) { + if (!pc.shouldRecord()) + continue; + pc.addToSql(prepst); + pc.start(); + i++; + } + if (i > 0) + prepst.execute(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } diff --git a/Stats/src/cz/marwland/mc/features/stats/PlayerCounter.java b/Stats/src/cz/marwland/mc/features/stats/PlayerCounter.java new file mode 100644 index 0000000..6a3db98 --- /dev/null +++ b/Stats/src/cz/marwland/mc/features/stats/PlayerCounter.java @@ -0,0 +1,87 @@ +package cz.marwland.mc.features.stats; + +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.bukkit.entity.Player; + +import cz.marwland.mc.core.storage.SQLStorage; +import cz.marwland.mc.core.util.FileUtil; +import cz.marwland.mc.core.util.UuidUtils; + +public class PlayerCounter { + + private static String updateQuery; + + private Player player; + + private long lastRecorded = 0; + private boolean doRecord = false; + private int killsSinceRecord = 0; + private int deathsSinceRecord = 0; + + + public PlayerCounter(Player player) { + this.player = player; + } + + public static void init(SQLStorage db) { + if (updateQuery == null) { + try { + updateQuery = FileUtil.readFileFromJAR(PlayerCounter.class, "/resources/activity_update.sql"); + updateQuery = db.getStatementProcessor().apply(updateQuery); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public void start() { + this.lastRecorded = System.currentTimeMillis(); + this.killsSinceRecord = 0; + this.deathsSinceRecord = 0; + this.doRecord = true; + } + + public void stop() { + this.doRecord = false; + } + + public Player getPlayer() { + return this.player; + } + + public void addToSql(PreparedStatement prepst) { + long playtime = (System.currentTimeMillis() - this.lastRecorded) / 1000; + try { + prepst.setBytes(1, UuidUtils.asBytes(player.getUniqueId())); + prepst.setDate(2, new java.sql.Date(System.currentTimeMillis())); + prepst.setInt(3, this.killsSinceRecord); + prepst.setInt(4, this.deathsSinceRecord); + prepst.setLong(5, playtime); + prepst.addBatch(); + } catch (SQLException e) { + // TODO: Notify admins about errors. + e.printStackTrace(); + } + + } + + public static String getRawQuery() { + return updateQuery; + } + + public void addKill() { + this.killsSinceRecord++; + } + + public void addDeath() { + this.deathsSinceRecord++; + } + + public boolean shouldRecord() { + return this.doRecord; + } + +}