package de.oppermann.bastian.spleef.util; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.UUID; import java.util.concurrent.Callable; import java.util.logging.Level; import org.bukkit.Bukkit; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.ListenableFuture; import de.oppermann.bastian.spleef.SpleefMain; import de.oppermann.bastian.spleef.arena.SpleefArena; import de.oppermann.bastian.spleef.storage.StorageManager; /** * Class that represents the stats of a player. * * @author Bastian Oppermann */ public class SpleefPlayer { static { // write the changes every 2 minutes into database Bukkit.getScheduler().runTaskTimer(SpleefMain.getInstance(), new Runnable() { @Override public void run() { for (SpleefPlayer stats : STATS.values()) { stats.writeChangesToDatabase(); } } }, 20*60*2, 20*60*2); // TODO configurable interval } private static final HashMap<UUID, SpleefPlayer> STATS = new HashMap<>(); private final UUID PLAYER; private boolean existsInMainTable; private ArrayList<String> existsInTable = new ArrayList<>(); // if the list contains the arena name it exists in the table for the arena private final HashMap<String, Integer> WINS = new HashMap<>(); private final HashMap<String, Integer> LOSSES = new HashMap<>(); private final HashMap<String, Integer> POINTS = new HashMap<>(); private final HashMap<String, Integer> JUMPS = new HashMap<>(); private final HashMap<String, Integer> DESTROYED_BLOCKS = new HashMap<>(); private final ArrayList<String> NOT_IN_SYNC_WITH_DATABASE = new ArrayList<String>(); // every arena name in the list is different from the database private boolean notInSyncWithMainTable = false; private boolean notInSyncWithShopTable = false; private int totalPoints; private ArrayList<Integer> boughtEffects = new ArrayList<Integer>(); private SpleefPlayer(UUID player, boolean existsInMainTable, int totalPoints) { Validator.validateNotNull(player, "player"); this.PLAYER = player; this.existsInMainTable = existsInMainTable; this.totalPoints = totalPoints; STATS.put(PLAYER, this); } /** * Adds wins to the stats of the player. * * @param arenaName The name of the arena. * @param wins The wins to add. Could also be negative to remove wins. */ public void addWins(String arenaName, int wins) { Validator.validateNotNull(arenaName, "arenaName"); Integer oldWins = WINS.get(arenaName); int newWins = oldWins == null ? wins : (oldWins + wins); WINS.put(arenaName, newWins); if (wins != 0 && !NOT_IN_SYNC_WITH_DATABASE.contains(arenaName)){ NOT_IN_SYNC_WITH_DATABASE.add(arenaName); } // TODO option to write data immediately into database } /** * Adds jumps to the stats of the player. * * @param arenaName The name of the arena. * @param jumps The jumps to add. Could also be negative to remove jumps. */ public void addJumps(String arenaName, int jumps) { Validator.validateNotNull(arenaName, "arenaName"); Integer oldJumps = JUMPS.get(arenaName); int newJumps = oldJumps == null ? jumps : (oldJumps + jumps); JUMPS.put(arenaName, newJumps); if (jumps != 0 && !NOT_IN_SYNC_WITH_DATABASE.contains(arenaName)){ NOT_IN_SYNC_WITH_DATABASE.add(arenaName); } // TODO option to write data immediately into database } /** * Adds destroyed blocks to the stats of the player. * * @param arenaName The name of the arena. * @param destroyedBlocks The amount of destroyed blocks to add. Could also be negative to decrease the amount. */ public void addDestroyedBlocks(String arenaName, int destroyedBlocks) { Validator.validateNotNull(arenaName, "arenaName"); Integer oldDestroyedBlocks = DESTROYED_BLOCKS.get(arenaName); int newDestroyedBlocks = oldDestroyedBlocks == null ? destroyedBlocks : (oldDestroyedBlocks + destroyedBlocks); DESTROYED_BLOCKS.put(arenaName, newDestroyedBlocks); if (destroyedBlocks != 0 && !NOT_IN_SYNC_WITH_DATABASE.contains(arenaName)){ NOT_IN_SYNC_WITH_DATABASE.add(arenaName); } // TODO option to write data immediately into database } /** * Adds losses to the stats of the player. * * @param arenaName The name of the arena. * @param losses The losses to add. Could also be negative to remove losses. */ public void addLosses(String arenaName, int losses) { Validator.validateNotNull(arenaName, "arenaName"); Integer oldLosses = LOSSES.get(arenaName); int newLosses = oldLosses == null ? losses : (oldLosses + losses); LOSSES.put(arenaName, newLosses); if (losses != 0 && !NOT_IN_SYNC_WITH_DATABASE.contains(arenaName)){ NOT_IN_SYNC_WITH_DATABASE.add(arenaName); } // TODO option to write data immediately into database } /** * Adds points for the player. * * @param arenaName The name of the arena. * @param losses The points to add. Could also be negative to remove points. */ public void addPoints(String arenaName, int points) { Validator.validateNotNull(arenaName, "arenaName"); Integer oldPoints = POINTS.get(arenaName); int newPoints = oldPoints == null ? points : (oldPoints + points); POINTS.put(arenaName, newPoints); if (points != 0 && !NOT_IN_SYNC_WITH_DATABASE.contains(arenaName)){ NOT_IN_SYNC_WITH_DATABASE.add(arenaName); } // special for points totalPoints += points; if (points != 0) { notInSyncWithMainTable = true; } // TODO option to write data immediately into database } /** * Gets the wins of the player. * * @param arenaName The name of the arena. */ public int getWins(String arenaName) { Validator.validateNotNull(arenaName, "arenaName"); Integer wins = WINS.get(arenaName); return wins == null ? 0 : wins; } /** * Gets the amount of wins earned in all arenas together. */ public int getTotalWins() { int wins = 0; for (Integer win : WINS.values()) { wins += win; } return wins; } /** * Gets the losses of the player. * * @param arenaName The name of the arena. */ public int getLosses(String arenaName) { Validator.validateNotNull(arenaName, "arenaName"); Integer losses = LOSSES.get(arenaName); return losses == null ? 0 : losses; } /** * Gets the amount of losses earned in all arenas together. */ public int getTotalLosses() { int losses = 0; for (Integer loss : LOSSES.values()) { losses += loss; } return losses; } /** * Gets the jumps of the player. * * @param arenaName The name of the arena. */ public int getJumps(String arenaName) { Validator.validateNotNull(arenaName, "arenaName"); Integer jumps = JUMPS.get(arenaName); return jumps == null ? 0 : jumps; } /** * Gets the amount of jumps in all arenas together. */ public int getTotalJumps() { int jumps = 0; for (Integer jump : JUMPS.values()) { jumps += jump; } return jumps; } /** * Gets the amount of destroyed blocks of the player. * * @param arenaName The name of the arena. */ public int getDestroyedBlocks(String arenaName) { Validator.validateNotNull(arenaName, "arenaName"); Integer destroyedBlocks = DESTROYED_BLOCKS.get(arenaName); return destroyedBlocks == null ? 0 : destroyedBlocks; } /** * Gets the amount of destroyed blocks in all arenas together. */ public int getTotalDestroyedBlocks() { int destroyedBlocks = 0; for (Integer destroyedBlock : DESTROYED_BLOCKS.values()) { destroyedBlocks += destroyedBlock; } return destroyedBlocks; } /** * Gets the points of the player. * * @param arenaName The name of the arena. */ public int getPoints(String arenaName) { Validator.validateNotNull(arenaName, "arenaName"); Integer points = POINTS.get(arenaName); return points == null ? 0 : points; } /** * Gets the amount of wins earned in all arenas together. */ public int getTotalEarnedPoints() { int points = 0; for (Integer point : POINTS.values()) { points += point; } return points; } /** * Gets the total amount of points of the player. */ public int getTotalPoints() { return totalPoints; } /** * Sets the total points of the player. */ public void setTotalPoints(int totalPoints) { this.totalPoints = totalPoints; notInSyncWithMainTable = true; } /** * Checks if the player has bought an effect. */ public boolean hasBought(Particle particleEffect) { return boughtEffects.contains(particleEffect.getId()); } /** * Sets an effect as bought. */ public void addEffect(Particle particleEffect) { if (!hasBought(particleEffect)) { boughtEffects.add(particleEffect.getId()); notInSyncWithShopTable = true; } } /** * Writes the changes to the database. This has no effect if there aren't any changes. */ public void writeChangesToDatabase() { if (!notInSyncWithMainTable && NOT_IN_SYNC_WITH_DATABASE.isEmpty() && !notInSyncWithShopTable) { return; // nothing to write in the database } final HashMap<String, Integer> WINS_CLONE = new HashMap<>(this.WINS); final HashMap<String, Integer> LOSSES_CLONE = new HashMap<>(this.LOSSES); final HashMap<String, Integer> POINTS_CLONE = new HashMap<>(this.POINTS); final HashMap<String, Integer> JUMPS_CLONE = new HashMap<>(this.JUMPS); final HashMap<String, Integer> DESTROYED_BLOCKS_CLONE = new HashMap<>(this.DESTROYED_BLOCKS); final ArrayList<String> NOT_IN_SYNC_WITH_DATABASE_CLONE = new ArrayList<String>(NOT_IN_SYNC_WITH_DATABASE); final ArrayList<String> EXISTS_IN_TABLE_CLONE = new ArrayList<String>(existsInTable); for (String arena : NOT_IN_SYNC_WITH_DATABASE) { if (!existsInTable.contains(arena)) { existsInTable.add(arena); // adds all arenas where values has been changed } } NOT_IN_SYNC_WITH_DATABASE.clear(); final boolean NOT_IN_SYNC_WITH_MAIN_TABLE_CLONE = notInSyncWithMainTable; final boolean NOT_IN_SYNC_WITH_SHOP_TABLE_CLONE = notInSyncWithShopTable; final int TOTAL_POINTS_CLONE = totalPoints; final boolean EXISTS_IN_MAIN_TABLE_CLONE = existsInMainTable; final ArrayList<Integer> BOUGHT_EFFECTS_CLONE = new ArrayList<Integer>(boughtEffects); existsInMainTable = notInSyncWithMainTable ? true : existsInMainTable; notInSyncWithMainTable = false; StorageManager.getInstance().submit(new Runnable() { @Override public void run() { for (String arena : NOT_IN_SYNC_WITH_DATABASE_CLONE) { final String QUERY; final int INDEX_WINS; final int INDEX_LOSSES; final int INDEX_POINTS; final int INDEX_JUMPS; final int INDEX_DESTROYED_BLOCKS; final int INDEX_PLAYERUUID; if (!EXISTS_IN_TABLE_CLONE.contains(arena)) { QUERY = "INSERT INTO `epicspleef_stats_" + arena + "` (`uuid`, `wins`, `losses`, `points`, `jumps`, `destroyedblocks`) VALUES (?, ?, ?, ?, ?, ?);"; INDEX_WINS = 2; INDEX_LOSSES = 3; INDEX_POINTS = 4; INDEX_JUMPS = 5; INDEX_DESTROYED_BLOCKS = 6; INDEX_PLAYERUUID = 1; } else { QUERY = "UPDATE `epicspleef_stats_" + arena + "` SET `wins` = ? ,`losses` = ? ,`points` = ?,`jumps` = ?,`destroyedblocks` = ? WHERE `uuid` = ?"; INDEX_WINS = 1; INDEX_LOSSES = 2; INDEX_POINTS = 3; INDEX_JUMPS = 4; INDEX_DESTROYED_BLOCKS = 5; INDEX_PLAYERUUID = 6; } try { int wins = WINS_CLONE.get(arena) == null ? 0 : WINS_CLONE.get(arena); int losses = LOSSES_CLONE.get(arena) == null ? 0 : LOSSES_CLONE.get(arena); int points = POINTS_CLONE.get(arena) == null ? 0 : POINTS_CLONE.get(arena); int jumps = JUMPS_CLONE.get(arena) == null ? 0 : JUMPS_CLONE.get(arena); int destroyedBlocks = DESTROYED_BLOCKS_CLONE.get(arena) == null ? 0 : DESTROYED_BLOCKS_CLONE.get(arena); PreparedStatement stmt = StorageManager.getInstance().getSqlConnector().prepareStatement(QUERY); try { stmt.setInt(INDEX_WINS, wins); stmt.setInt(INDEX_LOSSES, losses); stmt.setInt(INDEX_POINTS, points); stmt.setInt(INDEX_JUMPS, jumps); stmt.setInt(INDEX_DESTROYED_BLOCKS, destroyedBlocks); stmt.setString(INDEX_PLAYERUUID, PLAYER.toString()); } catch (Exception e) { e.printStackTrace(); } // execute if (!EXISTS_IN_TABLE_CLONE.contains(arena)) { if (StorageManager.getInstance().needWriteLock()) { StorageManager.getInstance().getLock().lock(); } try { stmt.execute(); } finally { if (StorageManager.getInstance().needWriteLock()) { StorageManager.getInstance().getLock().unlock(); } } } else { if (StorageManager.getInstance().needWriteLock()) { StorageManager.getInstance().getLock().lock(); } try { stmt.executeUpdate(); } finally { if (StorageManager.getInstance().needWriteLock()) { StorageManager.getInstance().getLock().unlock(); } } } } catch (SQLException e) { // should not happen ... SpleefMain.getInstance().log(Level.SEVERE, "Failed to write playerstats into database!"); e.printStackTrace(); return; } } if (NOT_IN_SYNC_WITH_MAIN_TABLE_CLONE) { final String QUERY; final int INDEX_POINTS; final int INDEX_PLAYERUUID; if (!EXISTS_IN_MAIN_TABLE_CLONE) { QUERY = "INSERT INTO `epicspleef_stats` (`uuid`, `points`) VALUES (?, ?);"; INDEX_POINTS = 2; INDEX_PLAYERUUID = 1; } else { QUERY = "UPDATE `epicspleef_stats` SET `points` = ? WHERE `uuid` = ?"; INDEX_POINTS = 1; INDEX_PLAYERUUID = 2; } try { PreparedStatement stmt = StorageManager.getInstance().getSqlConnector().prepareStatement(QUERY); stmt.setInt(INDEX_POINTS, TOTAL_POINTS_CLONE); stmt.setString(INDEX_PLAYERUUID, PLAYER.toString()); // execute update if (!EXISTS_IN_MAIN_TABLE_CLONE) { if (StorageManager.getInstance().needWriteLock()) { StorageManager.getInstance().getLock().lock(); } try { stmt.execute(); } finally { if (StorageManager.getInstance().needWriteLock()) { StorageManager.getInstance().getLock().unlock(); } } } else { if (StorageManager.getInstance().needWriteLock()) { StorageManager.getInstance().getLock().lock(); } try { stmt.executeUpdate(); } finally { if (StorageManager.getInstance().needWriteLock()) { StorageManager.getInstance().getLock().unlock(); } } } } catch (SQLException e) { // should not happen ... SpleefMain.getInstance().log(Level.SEVERE, "Failed to write playerstats into database!"); e.printStackTrace(); return; } } if (NOT_IN_SYNC_WITH_SHOP_TABLE_CLONE) { for (int particleId : BOUGHT_EFFECTS_CLONE) { final String QUERY = StorageManager.getInstance().isMySQL() ? "INSERT INTO `epicspleef_shop` (`uuid`, `particleId`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `uuid` = `uuid`;" : "INSERT OR REPLACE INTO `epicspleef_shop` (`uuid`, `particleId`) VALUES (?, ?);"; final int INDEX_ITEM_ID = 2; final int INDEX_PLAYERUUID = 1; try { PreparedStatement stmt = StorageManager.getInstance().getSqlConnector().prepareStatement(QUERY); stmt.setInt(INDEX_ITEM_ID, particleId); stmt.setString(INDEX_PLAYERUUID, PLAYER.toString()); // execute update if (StorageManager.getInstance().needWriteLock()) { StorageManager.getInstance().getLock().lock(); } try { stmt.execute(); } finally { if (StorageManager.getInstance().needWriteLock()) { StorageManager.getInstance().getLock().unlock(); } } } catch (SQLException e) { // should not happen ... SpleefMain.getInstance().log(Level.SEVERE, "Failed to write playerstats into database!"); e.printStackTrace(); return; } } } } }); } /** * Gets the statistics for a player with this uuid. * * @param player The uuid of the player. * @param callback The callback. * @return The statistics for the player with the given uuid. */ public static void getPlayer(final UUID player, final FutureCallback<SpleefPlayer> callback) { Validator.validateNotNull(callback, "callback"); Validator.validateNotNull(player, "player"); if (!STATS.containsKey(player)) { ListenableFuture<SpleefPlayer> future = StorageManager.getInstance().getListeningExecutorService().submit(new Callable<SpleefPlayer>() { @Override public SpleefPlayer call() throws Exception { boolean existsInMainTable = false; int totalPoints = 0; final String QUERY_TOTAL_POINTS = "SELECT `points` FROM `epicspleef_stats` WHERE `uuid` = ?"; int indexPlayerUUID = 1; try { // load stats from database PreparedStatement stmt = StorageManager.getInstance().getSqlConnector().prepareStatement(QUERY_TOTAL_POINTS); stmt.setString(indexPlayerUUID, player.toString()); // sets the uuid stmt.execute(); ResultSet result = stmt.getResultSet(); while (result.next()) { existsInMainTable = true; totalPoints = result.getInt("points"); } } catch (SQLException e) { // should not happen ... SpleefMain.getInstance().log(Level.SEVERE, "Failed to load playerstats from database!"); throw e; } final SpleefPlayer stats = new SpleefPlayer(player, existsInMainTable, totalPoints); for (String arena : SpleefArena.getArenaNames()) { boolean existsInTable = false; int wins = 0; int losses = 0; int points = 0; int jumps = 0; int destroyedBlocks = 0; final String QUERY_LOAD_STATS = "SELECT * FROM `epicspleef_stats_" + arena + "` WHERE `uuid` = ?"; indexPlayerUUID = 1; try { // load stats from database PreparedStatement stmt = StorageManager.getInstance().getSqlConnector().prepareStatement(QUERY_LOAD_STATS); stmt.setString(indexPlayerUUID, player.toString()); // sets the uuid stmt.execute(); ResultSet result = stmt.getResultSet(); while (result.next()) { existsInTable = true; wins = result.getInt("wins"); losses = result.getInt("losses"); points = result.getInt("points"); jumps = result.getInt("jumps"); destroyedBlocks = result.getInt("destroyedblocks"); } stats.WINS.put(arena, wins); stats.LOSSES.put(arena, losses); stats.POINTS.put(arena, points); stats.JUMPS.put(arena, jumps); stats.DESTROYED_BLOCKS.put(arena, destroyedBlocks); if (existsInTable) { stats.existsInTable.add(arena); } } catch (SQLException e) { // should not happen ... SpleefMain.getInstance().log(Level.SEVERE, "Failed to load playerstats from database!"); throw e; } } final String QUERY_BOUGHT_EFFECTS = "SELECT `particleId` FROM `epicspleef_shop` WHERE `uuid` = ?"; indexPlayerUUID = 1; try { // load from database PreparedStatement stmt = StorageManager.getInstance().getSqlConnector().prepareStatement(QUERY_BOUGHT_EFFECTS); stmt.setString(indexPlayerUUID, player.toString()); // sets the uuid stmt.execute(); ResultSet result = stmt.getResultSet(); while (result.next()) { int particleId = result.getInt("particleId"); stats.boughtEffects.add(particleId); } } catch (SQLException e) { // should not happen ... SpleefMain.getInstance().log(Level.SEVERE, "Failed to load playerstats from database!"); throw e; } return stats; } }); MoreFutures.addBukkitSyncCallback(future, callback); } else { // if the hashmap contains the uuid callback.onSuccess(STATS.get(player)); } } }