Replay overhaul

- added a section to the replay containing certain metadata about the match
- added versioning to replays
- added timestamps to replay actions
- overhauled the game_history table
- converted most date operations touching game_history to use java.time
- altered where replays are stored so that it will be easy to grab by date in the future
This commit is contained in:
Christian 'ketura' McCarty
2023-03-13 02:38:44 -05:00
parent c7d33a4f79
commit c6095126a6
16 changed files with 515 additions and 276 deletions

View File

@@ -0,0 +1,63 @@
SELECT *
FROM game_history gh
ORDER BY ID DESC
ALTER TABLE gemp_db.game_history
ADD COLUMN start_time DATETIME NOT NULL DEFAULT now() AFTER start_date;
ALTER TABLE gemp_db.game_history
ADD COLUMN end_time DATETIME NOT NULL DEFAULT now() AFTER end_date;
UPDATE game_history
SET start_time = from_unixtime(floor(start_date/1000)), end_time = from_unixtime(floor(end_date/1000));
ALTER TABLE gemp_db.game_history
DROP COLUMN start_date;
ALTER TABLE gemp_db.game_history
DROP COLUMN end_date;
ALTER TABLE gemp_db.game_history
RENAME COLUMN start_time TO start_date;
ALTER TABLE gemp_db.game_history
RENAME COLUMN end_time TO end_date;
ALTER TABLE gemp_db.game_history ADD INDEX game_history_win_id_index (win_recording_id);
ALTER TABLE gemp_db.game_history ADD INDEX game_history_lose_id_index (lose_recording_id);
ALTER TABLE gemp_db.game_history
ADD COLUMN winnerId INT(11) NOT NULL DEFAULT 0 AFTER winner;
ALTER TABLE gemp_db.game_history
ADD COLUMN loserId INT(11) NOT NULL DEFAULT 0 AFTER loser;
UPDATE game_history
INNER JOIN player P1
ON P1.name = game_history.winner
INNER JOIN player P2
ON P2.name = game_history.loser
SET winnerId = P1.id, loserId = P2.id;
ALTER TABLE gemp_db.game_history
ADD CONSTRAINT fk_winnerId FOREIGN KEY (winnerId) REFERENCES player(id);
ALTER TABLE gemp_db.game_history
ADD CONSTRAINT fk_loserId FOREIGN KEY (loserId) REFERENCES player(id);

View File

@@ -15,6 +15,7 @@ import org.w3c.dom.Element;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.lang.reflect.Type;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
@@ -60,6 +61,8 @@ public class GameHistoryRequestHandler extends LotroServerRequestHandler impleme
gameHistory.setAttribute("count", String.valueOf(recordCount));
gameHistory.setAttribute("playerId", resourceOwner.getName());
var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (DBDefs.GameHistory game : playerGameHistory) {
Element historyEntry = doc.createElement("historyEntry");
historyEntry.setAttribute("winner", game.winner);
@@ -81,8 +84,8 @@ public class GameHistoryRequestHandler extends LotroServerRequestHandler impleme
historyEntry.setAttribute("deckName", game.loser_deck_name);
}
historyEntry.setAttribute("startTime", game.start_date.toLocalDate().toString());
historyEntry.setAttribute("endTime", game.end_date.toLocalDate().toString());
historyEntry.setAttribute("startTime", game.start_date.format(formatter));
historyEntry.setAttribute("endTime", game.end_date.format(formatter));
gameHistory.appendChild(historyEntry);
}

View File

@@ -1,5 +1,6 @@
package com.gempukku.lotro.async.handler;
import com.alibaba.fastjson.JSON;
import com.gempukku.lotro.async.HttpProcessingException;
import com.gempukku.lotro.async.ResponseWriter;
import com.gempukku.lotro.common.DBDefs;
@@ -105,7 +106,7 @@ public class PlaytestRequestHandler extends LotroServerRequestHandler implements
final List<DBDefs.GameHistory> gameHistory = _gameHistoryService.getGameHistoryForFormat(format, count);
responseWriter.writeJsonResponse(JsonConvert.toJson(gameHistory));
responseWriter.writeJsonResponse(JSON.toJSONString(gameHistory));
} finally {
postDecoder.destroy();

View File

@@ -12,9 +12,10 @@ import io.netty.handler.codec.http.QueryStringDecoder;
import org.apache.log4j.Logger;
import java.lang.reflect.Type;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.TimeZone;
@@ -42,25 +43,29 @@ public class ServerStatsRequestHandler extends LotroServerRequestHandler impleme
try {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
format.setTimeZone(TimeZone.getTimeZone("GMT"));
long from = format.parse(startDay).getTime();
Date to = format.parse(startDay);
//This convoluted conversion is actually necessary, for it to be flexible enough to take
//human-level dates such as 2023-2-13 (note the lack of zero padding)
var from = ZonedDateTime.ofInstant(format.parse(startDay).toInstant(), ZoneOffset.UTC);
ZonedDateTime to = from;
switch (length) {
case "month" -> to.setMonth(to.getMonth() + 1);
case "week" -> to.setDate(to.getDate() + 7);
case "day" -> to.setDate(to.getDate() + 1);
case "month" -> to = from.plusMonths(1);
case "week" -> to = from.plusDays(7);
case "day" -> to = from.plusDays(1);
default -> throw new HttpProcessingException(400);
}
long duration = to.getTime() - from;
var stats = new JSONDefs.PlayHistoryStats();
stats.ActivePlayers = _gameHistoryService.getActivePlayersCount(from, duration);
stats.GamesCount = _gameHistoryService.getGamesPlayedCount(from, duration);
stats.StartDate = format.format(new Date(from));
stats.EndDate = format.format(new Date(from + duration - 1));
stats.Stats = _gameHistoryService.getGameHistoryStatistics(from, duration);
stats.ActivePlayers = _gameHistoryService.getActivePlayersCount(from, to);
stats.GamesCount = _gameHistoryService.getGamesPlayedCount(from, to);
stats.StartDate = from.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
stats.EndDate = to.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
stats.Stats = _gameHistoryService.getGameHistoryStatistics(from, to);
responseWriter.writeJsonResponse(JSON.toJSONString(stats));
} catch (ParseException exp) {
} catch (Exception exp) {
logHttpError(_log, 400, request.uri(), exp);
throw new HttpProcessingException(400);
}

View File

@@ -10,6 +10,8 @@ import io.netty.handler.codec.http.HttpRequest;
import org.apache.log4j.Logger;
import java.lang.reflect.Type;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Map;
public class StatusRequestHandler extends LotroServerRequestHandler implements UriRequestHandler {
@@ -30,11 +32,13 @@ public class StatusRequestHandler extends LotroServerRequestHandler implements U
public void handleRequest(String uri, HttpRequest request, Map<Type, Object> context, ResponseWriter responseWriter, String remoteIp) throws Exception {
if (uri.equals("") && request.method() == HttpMethod.GET) {
int day = 1000 * 60 * 60 * 24;
int week = 1000 * 60 * 60 * 24 * 7;
var today = ZonedDateTime.now(ZoneOffset.UTC);
var yesterday = today.minusDays(1);
var lastWeek = today.minusDays(7);
String sb = "Tables count: " + _hallServer.getTablesCount() + ", players in hall: " + _chatServer.getChatRoom("Game Hall").getUsersInRoom(false).size() +
", games played in last 24 hours: " + _gameHistoryService.getGamesPlayedCount(System.currentTimeMillis() - day, day) +
", active players in last week: " + _gameHistoryService.getActivePlayersCount(System.currentTimeMillis() - week, week);
", games played in last 24 hours: " + _gameHistoryService.getGamesPlayedCount(yesterday, today) +
", active players in last week: " + _gameHistoryService.getActivePlayersCount(lastWeek, today);
responseWriter.writeHtmlResponse(sb);
} else {

View File

@@ -1,6 +1,7 @@
package com.gempukku.lotro.common;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.Date;
public class DBDefs {
@@ -10,7 +11,9 @@ public class DBDefs {
public int id;
public String winner;
public int winnerId;
public String loser;
public int loserId;
public String win_reason;
public String lose_reason;
@@ -18,8 +21,8 @@ public class DBDefs {
public String win_recording_id;
public String lose_recording_id;
public long start_date;
public long end_date;
public ZonedDateTime start_date;
public ZonedDateTime end_date;
public String format_name;
@@ -28,18 +31,7 @@ public class DBDefs {
public String tournament;
public Date GetStartDate()
{
return new Date(start_date);
}
public Date GetEndDate()
{
return new Date(end_date);
}
public int replay_version = -1;
}
public static class Collection {

View File

@@ -8,6 +8,7 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
@@ -15,7 +16,10 @@ public class EventSerializer {
public Node serializeEvent(Document doc, GameEvent gameEvent) {
Element eventElem = doc.createElement("ge");
eventElem.setAttribute("type", gameEvent.getType().getCode());
eventElem.setAttribute("timestamp", gameEvent.getTimestamp().toString());
//TODO:
// - figure out a better date/time format for the log
// - import the json replay info into gemp
eventElem.setAttribute("timestamp", gameEvent.getTimestamp().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
if (gameEvent.getBlueprintId() != null)
eventElem.setAttribute("blueprintId", gameEvent.getBlueprintId());
@@ -43,6 +47,8 @@ public class EventSerializer {
eventElem.setAttribute("otherCardIds", arrayToCommaSeparated(gameEvent.getOtherCardIds()));
if (gameEvent.getMessage() != null)
eventElem.setAttribute("message", gameEvent.getMessage());
if (gameEvent.getVersion() != null)
eventElem.setAttribute("version", gameEvent.getVersion().toString());
if (gameEvent.getGameStats() != null)
serializeGameStats(doc, eventElem, gameEvent.getGameStats());
if (gameEvent.getAwaitingDecision() != null)

View File

@@ -5,7 +5,6 @@ import com.gempukku.lotro.communication.GameStateListener;
import com.gempukku.lotro.game.PhysicalCard;
import com.gempukku.lotro.logic.decisions.AwaitingDecision;
import com.gempukku.lotro.logic.timing.GameStats;
import com.gempukku.lotro.logic.vo.LotroDeck;
import com.gempukku.polling.LongPollableResource;
import com.gempukku.polling.WaitingRequest;
@@ -188,10 +187,6 @@ public class GameCommunicationChannel implements GameStateListener, LongPollable
appendEvent(new GameEvent(DECISION).awaitingDecision(decision).participantId(playerId));
}
public void deckReadout(Map<String, LotroDeck> decks) {
appendEvent(new GameEvent(DECK_READOUT).decks(decks));
}
@Override
public void sendWarning(String playerId, String warning) {
if (playerId.equals(_self))

View File

@@ -8,8 +8,8 @@ import com.gempukku.lotro.logic.decisions.AwaitingDecision;
import com.gempukku.lotro.logic.timing.GameStats;
import com.gempukku.lotro.logic.vo.LotroDeck;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
@@ -23,7 +23,7 @@ public class GameEvent {
ADD_TOKENS("AT"), REMOVE_TOKENS("RT"),
SEND_MESSAGE("M"), SEND_WARNING("W"),
GAME_STATS("GS"),
CHAT_MESSAGE("CM"), DECK_READOUT("DR"),
CHAT_MESSAGE("CM"),
CARD_AFFECTED_BY_CARD("CAC"), SHOW_CARD_ON_SCREEN("EP"), FLASH_CARD_IN_PLAY("CA"), DECISION("D");
private final String code;
@@ -55,14 +55,20 @@ public class GameEvent {
private Map<String, LotroDeck> _decks;
private GameStats _gameStats;
private AwaitingDecision _awaitingDecision;
private LocalDateTime _timestamp;
private ZonedDateTime _timestamp;
private Integer _version;
public GameEvent(Type type) {
_type = type;
_timestamp = LocalDateTime.now(ZoneOffset.UTC);
_timestamp = ZonedDateTime.now(ZoneOffset.UTC);
}
public LocalDateTime getTimestamp() { return _timestamp; }
public ZonedDateTime getTimestamp() { return _timestamp; }
public Integer getVersion() { return _version; }
public GameEvent version(int version) {
_version = version;
return this;
}
public Integer getIndex() {
return _index;

View File

@@ -48,7 +48,8 @@ public class ServerBuilder {
extract(objectMap, GameHistoryDAO.class)));
objectMap.put(GameRecorder.class,
new GameRecorder(
extract(objectMap, GameHistoryService.class)));
extract(objectMap, GameHistoryService.class),
extract(objectMap, PlayerDAO.class)));
objectMap.put(CollectionsManager.class,
new CollectionsManager(

View File

@@ -2,43 +2,62 @@ package com.gempukku.lotro.db;
import com.gempukku.lotro.common.DBDefs;
import com.gempukku.lotro.game.Player;
import org.sql2o.Query;
import org.sql2o.Sql2o;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
public class DbGameHistoryDAO implements GameHistoryDAO {
private final DbAccess _dbAccess;
private final DateTimeFormatter _dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public DbGameHistoryDAO(DbAccess dbAccess) {
_dbAccess = dbAccess;
}
public void addGameHistory(String winner, String loser, String winReason, String loseReason, String winRecordingId, String loseRecordingId, String formatName, String tournament, String winnerDeckName, String loserDeckName, Date startDate, Date endDate) {
public void addGameHistory(String winner, int winnerId, String loser, int loserId, String winReason, String loseReason, String winRecordingId, String loseRecordingId,
String formatName, String tournament, String winnerDeckName, String loserDeckName, ZonedDateTime startDate, ZonedDateTime endDate, int replayVersion) {
try {
try (Connection connection = _dbAccess.getDataSource().getConnection()) {
try (PreparedStatement statement = connection.prepareStatement("insert into game_history (winner, loser, win_reason, lose_reason, win_recording_id, lose_recording_id, format_name, tournament, winner_deck_name, loser_deck_name, start_date, end_date) values (?,?,?,?,?,?,?,?,?,?,?,?)")) {
statement.setString(1, winner);
statement.setString(2, loser);
statement.setString(3, winReason);
statement.setString(4, loseReason);
statement.setString(5, winRecordingId);
statement.setString(6, loseRecordingId);
statement.setString(7, formatName);
statement.setString(8, tournament);
statement.setString(9, winnerDeckName);
statement.setString(10, loserDeckName);
statement.setLong(11, startDate.getTime());
statement.setLong(12, endDate.getTime());
Sql2o db = new Sql2o(_dbAccess.getDataSource());
statement.execute();
}
String sql = """
INSERT INTO game_history (winner, winnerId, loser, loserId, win_reason, lose_reason, win_recording_id, lose_recording_id,
format_name, tournament, winner_deck_name, loser_deck_name, start_date, end_date, replay_version)
VALUES (:winner, :winnerId, :loser, :loserId, :win_reason, :lose_reason, :win_recording_id, :lose_recording_id,
:format_name, :tournament, :winner_deck_name, :loser_deck_name, :start_date, :end_date, :version)
""";
try (org.sql2o.Connection conn = db.beginTransaction()) {
Query query = conn.createQuery(sql, true);
query.addParameter("winner", winner)
.addParameter("winnerId", winnerId)
.addParameter("loser", loser)
.addParameter("loserId", loserId)
.addParameter("win_reason", winReason)
.addParameter("lose_reason", loseReason)
.addParameter("win_recording_id", winRecordingId)
.addParameter("lose_recording_id", loseRecordingId)
.addParameter("format_name", formatName)
.addParameter("tournament", tournament)
.addParameter("winner_deck_name", winnerDeckName)
.addParameter("loser_deck_name", loserDeckName)
.addParameter("start_date", startDate.format(DateTimeFormatter.ISO_DATE_TIME))
.addParameter("end_date", endDate.format(DateTimeFormatter.ISO_DATE_TIME))
.addParameter("version", replayVersion);
query.executeUpdate();
conn.commit();
}
} catch (SQLException exp) {
throw new RuntimeException("Unable to get count of player games", exp);
} catch (Exception ex) {
throw new RuntimeException("Unable to insert game history", ex);
}
}
@@ -49,7 +68,16 @@ public class DbGameHistoryDAO implements GameHistoryDAO {
Sql2o db = new Sql2o(_dbAccess.getDataSource());
try (org.sql2o.Connection conn = db.open()) {
String sql = "select winner, loser, win_reason, lose_reason, win_recording_id, lose_recording_id, format_name, tournament, winner_deck_name, loser_deck_name, start_date, end_date from game_history where winner=:playerName or loser=:playerName order by end_date desc limit :start, :count";
String sql = """
SELECT winner, winnerId, loser, loserId, win_reason, lose_reason,
win_recording_id, lose_recording_id, format_name, tournament,
winner_deck_name, loser_deck_name, start_date, end_date
FROM game_history
WHERE winner = :playerName
OR loser = :playerName
ORDER BY end_date DESC
LIMIT :start, :count;
""";
List<DBDefs.GameHistory> result = conn.createQuery(sql)
.addParameter("playerName", player.getName())
.addParameter("start", start)
@@ -64,6 +92,55 @@ public class DbGameHistoryDAO implements GameHistoryDAO {
}
public DBDefs.GameHistory getGameHistory(String recordID) {
try {
Sql2o db = new Sql2o(_dbAccess.getDataSource());
try (org.sql2o.Connection conn = db.open()) {
String sql = """
SELECT winner, winnerId, loser, loserId, win_reason, lose_reason,
win_recording_id, lose_recording_id, format_name, tournament,
winner_deck_name, loser_deck_name, start_date, end_date
FROM game_history
WHERE win_recording_id = :recordID
OR lose_recording_id = :recordID;
""";
List<DBDefs.GameHistory> result = conn.createQuery(sql)
.addParameter("recordID", recordID)
.executeAndFetch(DBDefs.GameHistory.class);
return result.stream().findFirst().orElse(null);
}
} catch (Exception ex) {
throw new RuntimeException("Unable to retrieve game history for player", ex);
}
}
public boolean doesReplayIDExist(String id) {
try {
Sql2o db = new Sql2o(_dbAccess.getDataSource());
try (org.sql2o.Connection conn = db.open()) {
String sql = """
SELECT COUNT(*)
FROM game_history
WHERE win_recording_id = :id
OR lose_recording_id = :id
""";
Integer result = conn.createQuery(sql)
.addParameter("id", id)
.executeScalar(Integer.class);
return result >= 1;
}
} catch (Exception ex) {
throw new RuntimeException("Unable to retrieve existence of replay ID", ex);
}
}
@Override
public List<DBDefs.GameHistory> getGameHistoryForFormat(String format, int count) {
try {
@@ -73,7 +150,7 @@ public class DbGameHistoryDAO implements GameHistoryDAO {
try (org.sql2o.Connection conn = db.open()) {
String sql = """
SELECT
winner, loser, win_reason, lose_reason,
winner, winnerId, loser, loserId, win_reason, lose_reason,
win_recording_id, lose_recording_id, format_name,
tournament, winner_deck_name, loser_deck_name,
start_date, end_date
@@ -82,12 +159,11 @@ public class DbGameHistoryDAO implements GameHistoryDAO {
ORDER BY end_date DESC
LIMIT :count
""";
List<DBDefs.GameHistory> result = conn.createQuery(sql)
return conn.createQuery(sql)
.addParameter("format", "%" + format + "%")
.addParameter("count", count)
.executeAndFetch(DBDefs.GameHistory.class);
return result;
}
} catch (Exception ex) {
throw new RuntimeException("Unable to retrieve game history by format", ex);
@@ -96,85 +172,82 @@ public class DbGameHistoryDAO implements GameHistoryDAO {
public int getGameHistoryForPlayerCount(Player player) {
try {
try (Connection connection = _dbAccess.getDataSource().getConnection()) {
try (PreparedStatement statement = connection.prepareStatement("select count(*) from game_history where winner=? or loser=?")) {
statement.setString(1, player.getName());
statement.setString(2, player.getName());
try (ResultSet rs = statement.executeQuery()) {
if (rs.next())
return rs.getInt(1);
else
return -1;
}
}
Sql2o db = new Sql2o(_dbAccess.getDataSource());
try (org.sql2o.Connection conn = db.open()) {
String sql = """
SELECT COUNT(*)
FROM game_history
WHERE winner = :player
OR loser = :player
""";
Integer result = conn.createQuery(sql)
.addParameter("player", player.getName())
.executeScalar(Integer.class);
return Objects.requireNonNullElse(result, -1);
}
} catch (SQLException exp) {
throw new RuntimeException("Unable to get count of player games", exp);
} catch (Exception ex) {
throw new RuntimeException("Unable to retrieve count of player games", ex);
}
}
public int getActivePlayersCount(long from, long duration) {
public int getActivePlayersCount(ZonedDateTime from, ZonedDateTime to) {
try {
try (Connection connection = _dbAccess.getDataSource().getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(
"select count(*) from (SELECT winner FROM game_history where end_date>=? and end_date<? union select loser from game_history where end_date>=? and end_date<?) as u")) {
statement.setLong(1, from);
statement.setLong(2, from + duration);
statement.setLong(3, from);
statement.setLong(4, from + duration);
try (ResultSet rs = statement.executeQuery()) {
if (rs.next())
return rs.getInt(1);
else
return -1;
}
}
Sql2o db = new Sql2o(_dbAccess.getDataSource());
try (org.sql2o.Connection conn = db.open()) {
String sql = """
SELECT COUNT(*)
FROM
(
SELECT winner
FROM game_history
WHERE end_date BETWEEN :from AND :to
UNION
SELECT loser
FROM game_history
WHERE end_date BETWEEN :from AND :to
) AS U
""";
Integer result = conn.createQuery(sql)
.addParameter("from", from.format(_dateFormat))
.addParameter("to", to.format(_dateFormat))
.executeScalar(Integer.class);
return Objects.requireNonNullElse(result, -1);
}
} catch (SQLException exp) {
throw new RuntimeException("Unable to get count of active players", exp);
} catch (Exception ex) {
throw new RuntimeException("Unable to retrieve count of active players", ex);
}
}
public int getGamesPlayedCount(long from, long duration) {
public int getGamesPlayedCount(ZonedDateTime from, ZonedDateTime to) {
try {
try (Connection connection = _dbAccess.getDataSource().getConnection()) {
try (PreparedStatement statement = connection.prepareStatement("select count(*) from game_history where end_date>=? and end_date<?")) {
statement.setLong(1, from);
statement.setLong(2, from + duration);
try (ResultSet rs = statement.executeQuery()) {
if (rs.next())
return rs.getInt(1);
else
return -1;
}
}
Sql2o db = new Sql2o(_dbAccess.getDataSource());
try (org.sql2o.Connection conn = db.open()) {
String sql = """
SELECT COUNT(*)
FROM game_history
WHERE end_date BETWEEN :from AND :to;
""";
Integer result = conn.createQuery(sql)
.addParameter("from", from.format(_dateFormat))
.addParameter("to", to.format(_dateFormat))
.executeScalar(Integer.class);
return Objects.requireNonNullElse(result, -1);
}
} catch (SQLException exp) {
throw new RuntimeException("Unable to get count of games played", exp);
} catch (Exception ex) {
throw new RuntimeException("Unable to retrieve count of games played", ex);
}
}
public Map<String, Integer> getCasualGamesPlayedPerFormat(long from, long duration) {
try {
try (Connection connection = _dbAccess.getDataSource().getConnection()) {
try (PreparedStatement statement = connection.prepareStatement("select count(*), format_name from game_history where (tournament is null or tournament like 'Casual %') and end_date>=? and end_date<? group by format_name")) {
statement.setLong(1, from);
statement.setLong(2, from + duration);
Map<String, Integer> result = new HashMap<>();
try (ResultSet rs = statement.executeQuery()) {
while (rs.next()) {
result.put(rs.getString(2), rs.getInt(1));
}
}
return result;
}
}
} catch (SQLException exp) {
throw new RuntimeException("Unable to get count of games played", exp);
}
}
public List<DBDefs.FormatStats> GetAllGameFormatData(long from, long duration) {
public List<DBDefs.FormatStats> GetAllGameFormatData(ZonedDateTime from, ZonedDateTime to) {
try {
Sql2o db = new Sql2o(_dbAccess.getDataSource());
@@ -185,13 +258,12 @@ public class DbGameHistoryDAO implements GameHistoryDAO {
,format_name AS Format
,CASE WHEN tournament IS NULL OR tournament LIKE 'Casual %' THEN 1 ELSE 0 END AS Casual
FROM game_history
WHERE end_date >= :from
AND end_date < :to
WHERE end_date BETWEEN :from AND :to
GROUP BY format_name, CASE WHEN tournament IS NULL OR tournament LIKE 'Casual %' THEN 1 ELSE 0 END
""";
List<DBDefs.FormatStats> result = conn.createQuery(sql)
.addParameter("from", from)
.addParameter("to", from + duration)
.addParameter("from", from.format(_dateFormat))
.addParameter("to", to.format(_dateFormat))
.executeAndFetch(DBDefs.FormatStats.class);
return result;
@@ -233,9 +305,14 @@ public class DbGameHistoryDAO implements GameHistoryDAO {
Sql2o db = new Sql2o(_dbAccess.getDataSource());
try (org.sql2o.Connection conn = db.open()) {
String sql = "select winner, loser, win_reason, lose_reason, win_recording_id, lose_recording_id, format_name, " +
"tournament, winner_deck_name, loser_deck_name, start_date, end_date from game_history " +
"where format_name=:formatName order by end_date desc limit :count";
String sql = """
SELECT winner, winnerId, loser, loserId, win_reason, lose_reason, win_recording_id, lose_recording_id, format_name,
tournament, winner_deck_name, loser_deck_name, start_date, end_date
FROM game_history
WHERE format_name = :formatName
ORDER BY end_date DESC
LIMIT :count
""";
List<DBDefs.GameHistory> result = conn.createQuery(sql)
.addParameter("formatName", requestedFormatName)
.addParameter("count", count)

View File

@@ -3,26 +3,23 @@ package com.gempukku.lotro.db;
import com.gempukku.lotro.common.DBDefs;
import com.gempukku.lotro.game.Player;
import java.util.Date;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
public interface GameHistoryDAO {
public void addGameHistory(String winner, String loser, String winReason, String loseReason, String winRecordingId, String loseRecordingId, String formatName, String tournament, String winnerDeckName, String loserDeckName, Date startDate, Date endDate);
public void addGameHistory(String winner, int winnerId, String loser, int loserId, String winReason, String loseReason, String winRecordingId, String loseRecordingId, String formatName, String tournament, String winnerDeckName, String loserDeckName, ZonedDateTime startDate, ZonedDateTime endDate, int version);
public DBDefs.GameHistory getGameHistory(String recordID);
public boolean doesReplayIDExist(String id);
public List<DBDefs.GameHistory> getGameHistoryForPlayer(Player player, int start, int count);
public int getGameHistoryForPlayerCount(Player player);
public List<DBDefs.GameHistory> getGameHistoryForFormat(String format, int count);
public int getActivePlayersCount(long from, long duration);
public int getActivePlayersCount(ZonedDateTime from, ZonedDateTime to);
public int getGamesPlayedCount(long from, long duration);
public int getGamesPlayedCount(ZonedDateTime from, ZonedDateTime to);
public Map<String, Integer> getCasualGamesPlayedPerFormat(long from, long duration);
public List<DBDefs.FormatStats> GetAllGameFormatData(long from, long duration);
public List<DBDefs.FormatStats> GetAllGameFormatData(ZonedDateTime from, ZonedDateTime to);
public List<PlayerStatistic> getCasualPlayerStatistics(Player player);

View File

@@ -4,7 +4,7 @@ import com.gempukku.lotro.common.DBDefs;
import com.gempukku.lotro.db.GameHistoryDAO;
import com.gempukku.lotro.db.PlayerStatistic;
import java.util.Date;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -17,8 +17,13 @@ public class GameHistoryService {
_gameHistoryDAO = gameHistoryDAO;
}
public void addGameHistory(String winner, String loser, String winReason, String loseReason, String winRecordingId, String loseRecordingId, String formatName, String tournament, String winnerDeckName, String loserDeckName, Date startDate, Date endDate) {
_gameHistoryDAO.addGameHistory(winner, loser, winReason, loseReason, winRecordingId, loseRecordingId, formatName, tournament, winnerDeckName, loserDeckName, startDate, endDate);
public void addGameHistory(DBDefs.GameHistory gh) {
addGameHistory(gh.winner, gh.winnerId, gh.loser, gh.loserId, gh.win_reason, gh.lose_reason, gh.win_recording_id, gh.lose_recording_id,
gh.format_name, gh.tournament, gh.winner_deck_name, gh.loser_deck_name, gh.start_date, gh.end_date, gh.replay_version);
}
public void addGameHistory(String winner, int winnerId, String loser, int loserId, String winReason, String loseReason, String winRecordingId, String loseRecordingId, String formatName, String tournament, String winnerDeckName, String loserDeckName, ZonedDateTime startDate, ZonedDateTime endDate, int version) {
_gameHistoryDAO.addGameHistory(winner, winnerId, loser, loserId, winReason, loseReason, winRecordingId, loseRecordingId, formatName, tournament, winnerDeckName, loserDeckName, startDate, endDate, version);
Integer winnerCount = _playerGameCount.get(winner);
Integer loserCount = _playerGameCount.get(loser);
if (winnerCount != null)
@@ -27,6 +32,14 @@ public class GameHistoryService {
_playerGameCount.put(loser, loserCount + 1);
}
public boolean doesReplayIDExist(String id) {
return _gameHistoryDAO.doesReplayIDExist(id);
}
public DBDefs.GameHistory getGameHistory(String recordID) {
return _gameHistoryDAO.getGameHistory(recordID);
}
public int getGameHistoryForPlayerCount(Player player) {
Integer result = _playerGameCount.get(player.getName());
if (result != null)
@@ -44,20 +57,16 @@ public class GameHistoryService {
return _gameHistoryDAO.getGameHistoryForFormat(format, count);
}
public List<DBDefs.GameHistory> getTrackableGames(int count) {
return _gameHistoryDAO.getLastGames("Second Edition", count);
}
public int getActivePlayersCount(long from, long duration) {
public int getActivePlayersCount(ZonedDateTime from, ZonedDateTime duration) {
return _gameHistoryDAO.getActivePlayersCount(from, duration);
}
public int getGamesPlayedCount(long from, long duration) {
public int getGamesPlayedCount(ZonedDateTime from, ZonedDateTime duration) {
return _gameHistoryDAO.getGamesPlayedCount(from, duration);
}
public List<DBDefs.FormatStats> getGameHistoryStatistics(long from, long duration) {
return _gameHistoryDAO.GetAllGameFormatData(from, duration);
public List<DBDefs.FormatStats> getGameHistoryStatistics(ZonedDateTime from, ZonedDateTime to) {
return _gameHistoryDAO.GetAllGameFormatData(from, to);
}
public List<PlayerStatistic> getCasualPlayerStatistics(Player player) {

View File

@@ -1,6 +1,8 @@
package com.gempukku.lotro.game;
import com.gempukku.lotro.common.AppConfig;
import com.gempukku.lotro.common.DBDefs;
import com.gempukku.lotro.db.PlayerDAO;
import com.gempukku.lotro.game.state.EventSerializer;
import com.gempukku.lotro.game.state.GameCommunicationChannel;
import com.gempukku.lotro.game.state.GameEvent;
@@ -17,7 +19,12 @@ import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.*;
import java.util.*;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
@@ -28,9 +35,164 @@ public class GameRecorder {
private static final int _charsCount = _possibleChars.length();
private final GameHistoryService _gameHistoryService;
private final PlayerDAO _playerDAO;
public GameRecorder(GameHistoryService gameHistoryService) {
public GameRecorder(GameHistoryService gameHistoryService, PlayerDAO playerDAO) {
_gameHistoryService = gameHistoryService;
_playerDAO = playerDAO;
}
public GameRecordingInProgress recordGame(LotroGameMediator lotroGame, LotroFormat format, final String tournamentName, final Map<String, LotroDeck> decks) {
final ZonedDateTime startDate = ZonedDateTime.now(ZoneOffset.UTC);
final Map<String, GameCommunicationChannel> recordingChannels = new HashMap<>();
for (String playerId : lotroGame.getPlayersPlaying()) {
var recordChannel = new GameCommunicationChannel(playerId, 0);
lotroGame.addGameStateListener(playerId, recordChannel);
recordingChannels.put(playerId, recordChannel);
}
return (winnerName, winReason, loserName, loseReason) -> {
final ZonedDateTime endDate = ZonedDateTime.now(ZoneOffset.UTC);
var gameInfo = new DBDefs.GameHistory() {{
winner = winnerName;
winnerId = _playerDAO.getPlayer(winnerName).getId();
loser = loserName;
loserId = _playerDAO.getPlayer(loserName).getId();
win_reason = winReason;
lose_reason = loseReason;
win_recording_id = getNewRecordingID();
lose_recording_id = getNewRecordingID();
start_date = startDate;
end_date = endDate;
format_name = format.getName();
winner_deck_name = decks.get(winner).getDeckName();
loser_deck_name = decks.get(loser).getDeckName();
tournament = tournamentName;
//Update this version as needed
replay_version = 1;
}};
var playerRecordingId = saveRecordedChannels(recordingChannels, gameInfo, decks);
_gameHistoryService.addGameHistory(gameInfo);
if(format.isPlaytest())
{
String url = "https://docs.google.com/forms/d/e/1FAIpQLSdKJrCmjoyUqDTusDcpNoWAmvkGdzQqTxWGpdNIFX9biCee-A/viewform?usp=pp_url&entry.1592109986=";
String winnerURL = "https://play.lotrtcgpc.net/gemp-lotr/game.html%3FreplayId%3D" + winnerName + "$" + playerRecordingId.get(winnerName);
String loserURL = "https://play.lotrtcgpc.net/gemp-lotr/game.html%3FreplayId%3D" + loserName + "$" + playerRecordingId.get(loserName);
url += winnerURL + "%20" + loserURL;
lotroGame.sendMessageToPlayers("Thank you for playtesting! If you have any feedback, bugs, or other issues to report about this match, <a href= '" + url + "'>please do so using this form.</a>");
}
};
}
public interface GameRecordingInProgress {
void finishRecording(String winner, String winReason, String loser, String loseReason);
}
private File getRecordingFileVersion0(String playerId, String gameId) {
File gameReplayFolder = new File(AppConfig.getProperty("application.root"), "replay");
File playerReplayFolder = new File(gameReplayFolder, playerId);
return new File(playerReplayFolder, gameId + ".xml.gz");
}
private File getRecordingFileVersion1(String playerId, String gameId, ZonedDateTime startDate) {
var gameReplayFolder = new File(AppConfig.getProperty("application.root"), "replay");
var yearFolder = new File(gameReplayFolder, String.format("%04d", startDate.getYear()));
var monthFolder = new File(yearFolder, String.format("%02d", startDate.getMonthValue()));
var playerReplayFolder = new File(monthFolder, playerId);
return new File(playerReplayFolder, gameId + ".xml.gz");
}
private OutputStream getRecordingWriteStream(String playerId, String gameId, ZonedDateTime startDate) throws IOException {
File recordingFile = getRecordingFileVersion1(playerId, gameId, startDate);
recordingFile.getParentFile().mkdirs();
Deflater deflater = new Deflater(9);
return new DeflaterOutputStream(new FileOutputStream(recordingFile), deflater);
}
private Map<String, String> saveRecordedChannels(Map<String, GameCommunicationChannel> gameProgress, DBDefs.GameHistory gameInfo, Map<String, LotroDeck> decks) {
Map<String, String> result = new HashMap<>();
for (Map.Entry<String, GameCommunicationChannel> playerRecordings : gameProgress.entrySet()) {
String playerId = playerRecordings.getKey();
String recordingId = "";
if(playerId.equals(gameInfo.winner)) {
recordingId = gameInfo.win_recording_id;
}
else {
recordingId = gameInfo.lose_recording_id;
}
final List<GameEvent> gameEvents = playerRecordings.getValue().consumeGameEvents();
try {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document doc = documentBuilder.newDocument();
Element gameReplay = doc.createElement("gameReplay");
EventSerializer serializer = new EventSerializer();
var info = doc.createElement("info");
info.setAttribute("replay_version", String.valueOf(gameInfo.replay_version));
info.setAttribute("winner", gameInfo.winner);
info.setAttribute("loser", gameInfo.loser);
info.setAttribute("win_reason", gameInfo.win_reason);
info.setAttribute("lose_reason", gameInfo.lose_reason);
info.setAttribute("start_date", gameInfo.start_date.toString());
info.setAttribute("end_date", gameInfo.end_date.toString());
info.setAttribute("format", gameInfo.format_name);
info.setAttribute("tournament", gameInfo.tournament);
for(var pair : decks.entrySet()) {
String player = pair.getKey();
var deck = pair.getValue();
var deckElement = doc.createElement("deckReadout");
deckElement.setAttribute("playerId", player);
deckElement.setAttribute("name", deck.getDeckName());
deckElement.setAttribute("rb", deck.getRingBearer());
deckElement.setAttribute("ring", deck.getRing());
deckElement.setAttribute("sites", String.join(",", deck.getSites()));
deckElement.setAttribute("deck", String.join(",", deck.getAdventureCards()));
info.appendChild(deckElement);
}
gameReplay.appendChild(info);
for (GameEvent gameEvent : gameEvents) {
gameReplay.appendChild(serializer.serializeEvent(doc, gameEvent));
}
doc.appendChild(gameReplay);
try (OutputStream replayStream = getRecordingWriteStream(playerId, recordingId, gameInfo.start_date)) {
// Prepare the DOM document for writing
Source source = new DOMSource(doc);
// Prepare the output file
Result streamResult = new StreamResult(replayStream);
// Write the DOM document to the file
Transformer xformer = TransformerFactory.newInstance().newTransformer();
xformer.transform(source, streamResult);
}
} catch (Exception exp) {
}
result.put(playerId, recordingId);
}
return result;
}
private String randomUid() {
@@ -43,108 +205,32 @@ public class GameRecorder {
return new String(chars);
}
public InputStream getRecordedGame(String playerId, String gameId) throws IOException {
final File file = getRecordingFile(playerId, gameId);
if (!file.exists() || !file.isFile())
return null;
return new InflaterInputStream(new FileInputStream(file));
}
public GameRecordingInProgress recordGame(LotroGameMediator lotroGame, LotroFormat format, final String tournament, final Map<String, LotroDeck> decks) {
final Date startData = new Date();
final Map<String, GameCommunicationChannel> recordingChannels = new HashMap<>();
for (String playerId : lotroGame.getPlayersPlaying()) {
var recordChannel = new GameCommunicationChannel(playerId, 0);
lotroGame.addGameStateListener(playerId, recordChannel);
recordingChannels.put(playerId, recordChannel);
}
return (winner, winReason, loser, loseReason) -> {
for(var comm : recordingChannels.values()) {
comm.deckReadout(decks);
}
var playerRecordingId = saveRecordedChannels(recordingChannels);
_gameHistoryService.addGameHistory(winner, loser, winReason, loseReason,
playerRecordingId.get(winner), playerRecordingId.get(loser), format.getName(),
tournament, decks.get(winner).getDeckName(), decks.get(loser).getDeckName(), startData, new Date());
if(format.isPlaytest())
{
String url = "https://docs.google.com/forms/d/e/1FAIpQLSdKJrCmjoyUqDTusDcpNoWAmvkGdzQqTxWGpdNIFX9biCee-A/viewform?usp=pp_url&entry.1592109986=";
String winnerURL = "https://play.lotrtcgpc.net/gemp-lotr/game.html%3FreplayId%3D" + winner + "$" + playerRecordingId.get(winner);
String loserURL = "https://play.lotrtcgpc.net/gemp-lotr/game.html%3FreplayId%3D" + loser + "$" + playerRecordingId.get(loser);
url += winnerURL + "%20" + loserURL;
lotroGame.sendMessageToPlayers("Thank you for playtesting! If you have any feedback, bugs, or other issues to report about this match, <a href= '" + url + "'>please do so using this form.</a>");
}
};
}
public interface GameRecordingInProgress {
void finishRecording(String winner, String winReason, String loser, String loseReason);
}
private File getRecordingFile(String playerId, String gameId) {
File gameReplayFolder = new File(AppConfig.getProperty("application.root"), "replay");
File playerReplayFolder = new File(gameReplayFolder, playerId);
return new File(playerReplayFolder, gameId + ".xml.gz");
}
private OutputStream getRecordingWriteStream(String playerId, String gameId) throws IOException {
File recordingFile = getRecordingFile(playerId, gameId);
recordingFile.getParentFile().mkdirs();
Deflater deflater = new Deflater(9);
return new DeflaterOutputStream(new FileOutputStream(recordingFile), deflater);
}
private Map<String, String> saveRecordedChannels(Map<String, GameCommunicationChannel> gameProgress) {
Map<String, String> result = new HashMap<>();
for (Map.Entry<String, GameCommunicationChannel> playerRecordings : gameProgress.entrySet()) {
String playerId = playerRecordings.getKey();
String gameRecordingId = getRecordingId(playerId);
final List<GameEvent> gameEvents = playerRecordings.getValue().consumeGameEvents();
try {
try (OutputStream replayStream = getRecordingWriteStream(playerId, gameRecordingId)) {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document doc = documentBuilder.newDocument();
Element gameReplay = doc.createElement("gameReplay");
EventSerializer serializer = new EventSerializer();
for (GameEvent gameEvent : gameEvents) {
gameReplay.appendChild(serializer.serializeEvent(doc, gameEvent));
}
doc.appendChild(gameReplay);
// Prepare the DOM document for writing
Source source = new DOMSource(doc);
// Prepare the output file
Result streamResult = new StreamResult(replayStream);
// Write the DOM document to the file
Transformer xformer = TransformerFactory.newInstance().newTransformer();
xformer.transform(source, streamResult);
}
} catch (Exception exp) {
}
result.put(playerId, gameRecordingId);
}
return result;
}
private String getRecordingId(String playerId) {
String result;
File recordingFile;
private String getNewRecordingID() {
String id;
do {
result = randomUid();
recordingFile = getRecordingFile(playerId, result);
} while (recordingFile.exists());
return result;
id = randomUid();
} while (_gameHistoryService.doesReplayIDExist(id));
return id;
}
public InputStream getRecordedGame(String playerId, String recordId) throws IOException {
var history = _gameHistoryService.getGameHistory(recordId);
if(history == null)
return null;
File recordingFile = null;
if(history.replay_version == 0) {
recordingFile = getRecordingFileVersion0(playerId, recordId);
}
else if(history.replay_version == 1) {
recordingFile = getRecordingFileVersion1(playerId, recordId, history.start_date);
}
if (recordingFile == null || !recordingFile.exists() || !recordingFile.isFile())
return null;
return new InflaterInputStream(new FileInputStream(recordingFile));
}
}

View File

@@ -353,12 +353,6 @@ public class LotroGameMediator {
}
}
public void readoutParticipantDecks() {
for(var comms : _communicationChannels.values()) {
comms.deckReadout(_playerDecks);
}
}
public GameCommunicationChannel getCommunicationChannel(Player player, int channelNumber) throws PrivateInformationException, SubscriptionConflictException, SubscriptionExpiredException {
String playerName = player.getName();
if (!player.hasType(Player.Type.ADMIN) && !_allowSpectators && !_playersPlaying.contains(playerName))

View File

@@ -126,7 +126,7 @@ public class LotroServer extends AbstractServer {
decks.put(participant.getPlayerId(), participant.getDeck());
}
lotroGameMediator.sendMessageToPlayers("Players in the game are: " + players.toString());
lotroGameMediator.sendMessageToPlayers("Players in the game are: " + players);
final var gameRecordingInProgress = _gameRecorder.recordGame(lotroGameMediator, gameSettings.getLotroFormat(), tournamentName, decks);
lotroGameMediator.addGameResultListener(