package mage.server.record; import com.j256.ormlite.dao.Dao; import com.j256.ormlite.dao.DaoManager; import com.j256.ormlite.jdbc.JdbcConnectionSource; import com.j256.ormlite.stmt.QueryBuilder; import com.j256.ormlite.support.ConnectionSource; import com.j256.ormlite.support.DatabaseConnection; import com.j256.ormlite.table.TableUtils; import mage.cards.repository.RepositoryUtil; import mage.game.result.ResultProtos; import mage.server.rating.GlickoRating; import mage.server.rating.GlickoRatingSystem; import org.apache.log4j.Logger; import java.io.File; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; public enum UserStatsRepository { instance; private static final String JDBC_URL = "jdbc:sqlite:./db/user_stats.db"; private static final String VERSION_ENTITY_NAME = "user_stats"; // raise this if db structure was changed private static final long DB_VERSION = 0; private Dao<UserStats, Object> dao; UserStatsRepository() { File file = new File("db"); if (!file.exists()) { file.mkdirs(); } try { ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL); boolean obsolete = RepositoryUtil.isDatabaseObsolete(connectionSource, VERSION_ENTITY_NAME, DB_VERSION); if (obsolete) { TableUtils.dropTable(connectionSource, UserStats.class, true); } TableUtils.createTableIfNotExists(connectionSource, UserStats.class); dao = DaoManager.createDao(connectionSource, UserStats.class); } catch (SQLException ex) { Logger.getLogger(UserStatsRepository.class).error("Error creating user_stats repository - ", ex); } } public void add(UserStats userStats) { try { dao.create(userStats); } catch (SQLException ex) { Logger.getLogger(UserStatsRepository.class).error("Error adding a user_stats to DB - ", ex); } } public void update(UserStats userStats) { try { dao.update(userStats); } catch (SQLException ex) { Logger.getLogger(UserStatsRepository.class).error("Error updating a user_stats in DB - ", ex); } } public UserStats getUser(String userName) { try { QueryBuilder<UserStats, Object> qb = dao.queryBuilder(); qb.limit(1L).where().eq("userName", userName); List<UserStats> users = dao.query(qb.prepare()); if (!users.isEmpty()) { return users.get(0); } } catch (SQLException ex) { Logger.getLogger(UserStatsRepository.class).error("Error getting a user from DB - ", ex); } return null; } public List<UserStats> getAllUsers() { try { QueryBuilder<UserStats, Object> qb = dao.queryBuilder(); return dao.query(qb.prepare()); } catch (SQLException ex) { Logger.getLogger(UserStatsRepository.class).error("Error getting all users from DB - ", ex); } return null; } public long getLatestEndTimeMs() { try { QueryBuilder<UserStats, Object> qb = dao.queryBuilder(); qb.orderBy("endTimeMs", false).limit(1L); List<UserStats> users = dao.query(qb.prepare()); if (!users.isEmpty()) { return users.get(0).getEndTimeMs(); } } catch (SQLException ex) { Logger.getLogger(UserStatsRepository.class).error("Error getting the latest end time from DB - ", ex); } return 0; } // updateUserStats reads tables finished after the last DB update and reflects it to the DB. // It returns the list of user names that are upated. public List<String> updateUserStats() { HashSet<String> updatedUsers = new HashSet<>(); // Lock the DB so that no other updateUserStats runs at the same time. synchronized(this) { long latestEndTimeMs = this.getLatestEndTimeMs(); List<TableRecord> records = TableRecordRepository.instance.getAfter(latestEndTimeMs); for (TableRecord record : records) { ResultProtos.TableProto table = record.getProto(); if (table.getControllerName().equals("System")) { // This is a sub table within a tournament, so it's already handled by the main // tournament table. continue; } if (table.hasMatch()) { ResultProtos.MatchProto match = table.getMatch(); for (ResultProtos.MatchPlayerProto player : match.getPlayersList()) { UserStats userStats = this.getUser(player.getName()); ResultProtos.UserStatsProto proto = userStats != null ? userStats.getProto() : ResultProtos.UserStatsProto.newBuilder().setName(player.getName()).build(); ResultProtos.UserStatsProto.Builder builder = ResultProtos.UserStatsProto.newBuilder(proto) .setMatches(proto.getMatches() + 1); switch (player.getQuit()) { case IDLE_TIMEOUT: builder.setMatchesIdleTimeout(proto.getMatchesIdleTimeout() + 1); break; case TIMER_TIMEOUT: builder.setMatchesTimerTimeout(proto.getMatchesTimerTimeout() + 1); break; case QUIT: builder.setMatchesQuit(proto.getMatchesQuit() + 1); break; } if (userStats == null) { this.add(new UserStats(builder.build(), table.getEndTimeMs())); } else { this.update(new UserStats(builder.build(), table.getEndTimeMs())); } updatedUsers.add(player.getName()); } updateRating(match, table.getEndTimeMs()); } else if (table.hasTourney()) { ResultProtos.TourneyProto tourney = table.getTourney(); for (ResultProtos.TourneyPlayerProto player : tourney.getPlayersList()) { UserStats userStats = this.getUser(player.getName()); ResultProtos.UserStatsProto proto = userStats != null ? userStats.getProto() : ResultProtos.UserStatsProto.newBuilder().setName(player.getName()).build(); ResultProtos.UserStatsProto.Builder builder = ResultProtos.UserStatsProto.newBuilder(proto) .setTourneys(proto.getTourneys() + 1); switch (player.getQuit()) { case DURING_ROUND: builder.setTourneysQuitDuringRound(proto.getTourneysQuitDuringRound() + 1); break; case DURING_DRAFTING: builder.setTourneysQuitDuringDrafting(proto.getTourneysQuitDuringDrafting() + 1); break; case DURING_CONSTRUCTION: builder.setTourneysQuitDuringConstruction(proto.getTourneysQuitDuringConstruction() + 1); break; } if (userStats == null) { this.add(new UserStats(builder.build(), table.getEndTimeMs())); } else { this.update(new UserStats(builder.build(), table.getEndTimeMs())); } updatedUsers.add(player.getName()); } for (ResultProtos.TourneyRoundProto round : tourney.getRoundsList()) { for (ResultProtos.MatchProto match : round.getMatchesList()) { updateRating(match, table.getEndTimeMs()); } } } } } return new ArrayList<>(updatedUsers); } private void updateRating(ResultProtos.MatchProto match, long tableEndTimeMs) { long matchEndTimeMs; if (match.hasEndTimeMs()) { matchEndTimeMs = match.getEndTimeMs(); } else { matchEndTimeMs = tableEndTimeMs; } // process only match with options if (!match.hasMatchOptions()) { return; } ResultProtos.MatchOptionsProto matchOptions = match.getMatchOptions(); // process only rated matches if (!matchOptions.getRated()) { return; } // rating only for duels if (match.getPlayersCount() != 2) { return; } ResultProtos.MatchPlayerProto player1 = match.getPlayers(0); ResultProtos.MatchPlayerProto player2 = match.getPlayers(1); // rate only games between human players if (!player1.getHuman() || !player2.getHuman()) { return; } double outcome; if ((player1.getQuit() == ResultProtos.MatchQuitStatus.NO_MATCH_QUIT && player1.getWins() > player2.getWins()) || player2.getQuit() != ResultProtos.MatchQuitStatus.NO_MATCH_QUIT) { // player1 won outcome = 1; } else if ((player2.getQuit() == ResultProtos.MatchQuitStatus.NO_MATCH_QUIT && player1.getWins() < player2.getWins()) || player1.getQuit() != ResultProtos.MatchQuitStatus.NO_MATCH_QUIT) { // player2 won outcome = 0; } else { // draw outcome = 0.5; } // get players stats UserStats player1Stats = getOrCreateUserStats(player1.getName(), tableEndTimeMs); ResultProtos.UserStatsProto player1StatsProto = player1Stats.getProto(); UserStats player2Stats = getOrCreateUserStats(player2.getName(), tableEndTimeMs); ResultProtos.UserStatsProto player2StatsProto = player2Stats.getProto(); ResultProtos.UserStatsProto.Builder player1StatsBuilder = ResultProtos.UserStatsProto.newBuilder(player1StatsProto); ResultProtos.UserStatsProto.Builder player2StatsBuilder = ResultProtos.UserStatsProto.newBuilder(player2StatsProto); // update general rating ResultProtos.GlickoRatingProto player1GeneralRatingProto = null; if (player1StatsProto.hasGeneralGlickoRating()) { player1GeneralRatingProto = player1StatsProto.getGeneralGlickoRating(); } ResultProtos.GlickoRatingProto player2GeneralRatingProto = null; if (player2StatsProto.hasGeneralGlickoRating()) { player2GeneralRatingProto = player2StatsProto.getGeneralGlickoRating(); } ResultProtos.GlickoRatingProto.Builder player1GeneralGlickoRatingBuilder = player1StatsBuilder.getGeneralGlickoRatingBuilder(); ResultProtos.GlickoRatingProto.Builder player2GeneralGlickoRatingBuilder = player2StatsBuilder.getGeneralGlickoRatingBuilder(); updateRating(player1GeneralRatingProto, player2GeneralRatingProto, outcome, matchEndTimeMs, player1GeneralGlickoRatingBuilder, player2GeneralGlickoRatingBuilder); if (matchOptions.hasLimited()) { if (matchOptions.getLimited()) { // update limited rating ResultProtos.GlickoRatingProto player1LimitedRatingProto = null; if (player1StatsProto.hasLimitedGlickoRating()) { player1LimitedRatingProto = player1StatsProto.getLimitedGlickoRating(); } ResultProtos.GlickoRatingProto player2LimitedRatingProto = null; if (player2StatsProto.hasLimitedGlickoRating()) { player2LimitedRatingProto = player2StatsProto.getLimitedGlickoRating(); } ResultProtos.GlickoRatingProto.Builder player1LimitedGlickoRatingBuilder = player1StatsBuilder.getLimitedGlickoRatingBuilder(); ResultProtos.GlickoRatingProto.Builder player2LimitedGlickoRatingBuilder = player2StatsBuilder.getLimitedGlickoRatingBuilder(); updateRating(player1LimitedRatingProto, player2LimitedRatingProto, outcome, matchEndTimeMs, player1LimitedGlickoRatingBuilder, player2LimitedGlickoRatingBuilder); } else { // update constructed rating ResultProtos.GlickoRatingProto player1ConstructedRatingProto = null; if (player1StatsProto.hasConstructedGlickoRating()) { player1ConstructedRatingProto = player1StatsProto.getConstructedGlickoRating(); } ResultProtos.GlickoRatingProto player2ConstructedRatingProto = null; if (player2StatsProto.hasConstructedGlickoRating()) { player2ConstructedRatingProto = player2StatsProto.getConstructedGlickoRating(); } ResultProtos.GlickoRatingProto.Builder player1ConstructedGlickoRatingBuilder = player1StatsBuilder.getConstructedGlickoRatingBuilder(); ResultProtos.GlickoRatingProto.Builder player2ConstructedGlickoRatingBuilder = player2StatsBuilder.getConstructedGlickoRatingBuilder(); updateRating(player1ConstructedRatingProto, player2ConstructedRatingProto, outcome, matchEndTimeMs, player1ConstructedGlickoRatingBuilder, player2ConstructedGlickoRatingBuilder); } } this.update(new UserStats(player1StatsBuilder.build(), player1Stats.getEndTimeMs())); this.update(new UserStats(player2StatsBuilder.build(), player2Stats.getEndTimeMs())); } private void updateRating( ResultProtos.GlickoRatingProto player1RatingProto, ResultProtos.GlickoRatingProto player2RatingProto, double outcome, long tableEndTimeMs, ResultProtos.GlickoRatingProto.Builder player1GlickoRatingBuilder, ResultProtos.GlickoRatingProto.Builder player2GlickoRatingBuilder) { GlickoRating player1GlickoRating; if (player1RatingProto != null) { player1GlickoRating = new GlickoRating( player1RatingProto.getRating(), player1RatingProto.getRatingDeviation(), player1RatingProto.getLastGameTimeMs()); } else { player1GlickoRating = GlickoRatingSystem.getInitialRating(); } GlickoRating player2GlickoRating; if (player2RatingProto != null) { player2GlickoRating = new GlickoRating( player2RatingProto.getRating(), player2RatingProto.getRatingDeviation(), player2RatingProto.getLastGameTimeMs()); } else { player2GlickoRating = GlickoRatingSystem.getInitialRating(); } GlickoRatingSystem glickoRatingSystem = new GlickoRatingSystem(); glickoRatingSystem.updateRating(player1GlickoRating, player2GlickoRating, outcome, tableEndTimeMs); player1GlickoRatingBuilder .setRating(player1GlickoRating.getRating()) .setRatingDeviation(player1GlickoRating.getRatingDeviation()) .setLastGameTimeMs(tableEndTimeMs); player2GlickoRatingBuilder .setRating(player2GlickoRating.getRating()) .setRatingDeviation(player2GlickoRating.getRatingDeviation()) .setLastGameTimeMs(tableEndTimeMs); } private UserStats getOrCreateUserStats(String playerName, long endTimeMs) { UserStats userStats = this.getUser(playerName); if (userStats == null) { ResultProtos.UserStatsProto userStatsProto = ResultProtos.UserStatsProto.newBuilder().setName(playerName).build(); userStats = new UserStats(userStatsProto, endTimeMs); this.add(userStats); } return userStats; } public void closeDB() { try { if (dao != null && dao.getConnectionSource() != null) { DatabaseConnection conn = dao.getConnectionSource().getReadWriteConnection(); conn.executeStatement("shutdown compact", 0); } } catch (SQLException ex) { Logger.getLogger(UserStatsRepository.class).error("Error closing user_stats repository - ", ex); } } }