Added data collectors, AI and testing tools improves:

- dev: added data collectors API to collect and process game data in real time;
- tests: added game logs output in all unit tests (enabled by default);
- tests: added games history storage (decks, game logs, chats - disabled by default);
This commit is contained in:
Oleg Agafonov 2025-07-05 20:54:33 +04:00
parent 9812e133e1
commit 52180d1393
30 changed files with 1007 additions and 80 deletions

View file

@ -192,11 +192,6 @@
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>

View file

@ -4,6 +4,8 @@ import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
import mage.constants.Constants;
import mage.game.Game;
import mage.game.Table;
import mage.game.tournament.Tournament;
import mage.server.game.GameController;
import mage.server.managers.ChatManager;
import mage.server.managers.ManagerFactory;
@ -40,11 +42,39 @@ public class ChatManagerImpl implements ChatManager {
this.managerFactory = managerFactory;
}
@Override
public UUID createChatSession(String info) {
public UUID createRoomChatSession(UUID roomId) {
return createChatSession("Room " + roomId)
.withRoom(roomId)
.getChatId();
}
@Override
public UUID createTourneyChatSession(Tournament tournament) {
return createChatSession("Tourney " + tournament.getId())
.withTourney(tournament)
.getChatId();
}
@Override
public UUID createTableChatSession(Table table) {
return createChatSession("Table " + table.getId())
.withTable(table)
.getChatId();
}
@Override
public UUID createGameChatSession(Game game) {
return createChatSession("Game " + game.getId())
.withGame(game)
.getChatId();
}
private ChatSession createChatSession(String info) {
ChatSession chatSession = new ChatSession(managerFactory, info);
chatSessions.put(chatSession.getChatId(), chatSession);
return chatSession.getChatId();
return chatSession;
}
@Override
@ -94,7 +124,7 @@ public class ChatManagerImpl implements ChatManager {
ChatSession chatSession = chatSessions.get(chatId);
Optional<User> user = managerFactory.userManager().getUserByName(userName);
if (chatSession != null) {
// special commads
// special commands
if (message.startsWith("\\") || message.startsWith("/")) {
if (user.isPresent()) {
if (!performUserCommand(user.get(), message, chatId, false)) {

View file

@ -1,6 +1,9 @@
package mage.server;
import mage.collectors.DataCollectorServices;
import mage.game.Game;
import mage.game.Table;
import mage.game.tournament.Tournament;
import mage.interfaces.callback.ClientCallback;
import mage.interfaces.callback.ClientCallbackMethod;
import mage.server.managers.ManagerFactory;
@ -27,6 +30,13 @@ public class ChatSession {
private final ManagerFactory managerFactory;
private final ReadWriteLock lock = new ReentrantReadWriteLock(); // TODO: no needs due ConcurrentHashMap usage?
// only 1 field must be filled per chat type
// TODO: rework chat sessions to share logic (one server room/lobby + one table/subtable + one games/match)
private UUID roomId = null;
private UUID tourneyId = null;
private UUID tableId = null;
private UUID gameId = null;
private final ConcurrentMap<UUID, String> users = new ConcurrentHashMap<>(); // active users
private final Set<UUID> usersHistory = new HashSet<>(); // all users that was here (need for system messages like connection problem)
private final UUID chatId;
@ -40,6 +50,26 @@ public class ChatSession {
this.info = info;
}
public ChatSession withRoom(UUID roomId) {
this.roomId = roomId;
return this;
}
public ChatSession withTourney(Tournament tournament) {
this.tourneyId = tournament.getId();
return this;
}
public ChatSession withTable(Table table) {
this.tableId = table.getId();
return this;
}
public ChatSession withGame(Game game) {
this.gameId = game.getId();
return this;
}
public void join(UUID userId) {
managerFactory.userManager().getUser(userId).ifPresent(user -> {
if (!users.containsKey(userId)) {
@ -112,9 +142,36 @@ public class ChatSession {
// TODO: is it freeze on someone's connection fail/freeze with play multiple games/chats/lobby?
// TODO: send messages in another thread?!
if (!message.isEmpty()) {
ChatMessage chatMessage = new ChatMessage(userName, message, (withTime ? new Date() : null), game, color, messageType, soundToPlay);
switch (messageType) {
case USER_INFO:
case STATUS:
case TALK:
if (this.roomId != null) {
DataCollectorServices.getInstance().onChatRoom(this.roomId, userName, message);
} else if (this.tourneyId != null) {
DataCollectorServices.getInstance().onChatTourney(this.tourneyId, userName, message);
} else if (this.tableId != null) {
DataCollectorServices.getInstance().onChatTable(this.tableId, userName, message);
} else if (this.gameId != null) {
DataCollectorServices.getInstance().onChatGame(this.gameId, userName, message);
}
break;
case GAME:
// game logs processing in other place
break;
case WHISPER_FROM:
case WHISPER_TO:
// ignore private messages
break;
default:
throw new IllegalStateException("Unsupported message type " + messageType);
}
// TODO: wtf, remove all that locks/tries and make it simpler
Set<UUID> clientsToRemove = new HashSet<>();
ClientCallback clientCallback = new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId,
new ChatMessage(userName, message, (withTime ? new Date() : null), game, color, messageType, soundToPlay));
ClientCallback clientCallback = new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId, chatMessage);
List<UUID> chatUserIds = new ArrayList<>();
final Lock r = lock.readLock();
r.lock();

View file

@ -5,6 +5,7 @@ import mage.cards.decks.DeckCardLists;
import mage.cards.decks.DeckValidatorFactory;
import mage.cards.repository.CardRepository;
import mage.cards.repository.ExpansionRepository;
import mage.collectors.DataCollectorServices;
import mage.constants.Constants;
import mage.constants.ManaType;
import mage.constants.PlayerAction;
@ -29,6 +30,7 @@ import mage.server.managers.ManagerFactory;
import mage.server.services.impl.FeedbackServiceImpl;
import mage.server.tournament.TournamentFactory;
import mage.server.util.ServerMessagesUtil;
import mage.util.DebugUtil;
import mage.utils.*;
import mage.view.*;
import mage.view.ChatMessage.MessageColor;
@ -68,6 +70,13 @@ public class MageServerImpl implements MageServer {
this.detailsMode = detailsMode;
this.callExecutor = managerFactory.threadExecutor().getCallExecutor();
ServerMessagesUtil.instance.getMessages();
// additional logs
DataCollectorServices.init(
DebugUtil.SERVER_DATA_COLLECTORS_ENABLE_PRINT_GAME_LOGS,
DebugUtil.SERVER_DATA_COLLECTORS_ENABLE_SAVE_GAME_HISTORY
);
DataCollectorServices.getInstance().onServerStart();
}
@Override

View file

@ -14,7 +14,7 @@ public abstract class RoomImpl implements Room {
public RoomImpl(ChatManager chatManager) {
roomId = UUID.randomUUID();
chatId = chatManager.createChatSession("Room " + roomId);
chatId = chatManager.createRoomChatSession(roomId);
}
/**

View file

@ -72,7 +72,7 @@ public class TableController {
}
this.table = new Table(roomId, options.getGameType(), options.getName(), controllerName, DeckValidatorFactory.instance.createDeckValidator(options.getDeckType()),
options.getPlayerTypes(), new TableRecorderImpl(managerFactory.userManager()), match, options.getBannedUsers(), options.isPlaneChase());
this.chatId = managerFactory.chatManager().createChatSession("Match Table " + table.getId());
this.chatId = managerFactory.chatManager().createTableChatSession(table);
init();
}
@ -94,7 +94,7 @@ public class TableController {
}
table = new Table(roomId, options.getTournamentType(), options.getName(), controllerName, DeckValidatorFactory.instance.createDeckValidator(options.getMatchOptions().getDeckType()),
options.getPlayerTypes(), new TableRecorderImpl(managerFactory.userManager()), tournament, options.getMatchOptions().getBannedUsers(), options.isPlaneChase());
chatId = managerFactory.chatManager().createChatSession("Tourney table " + table.getId());
chatId = managerFactory.chatManager().createTableChatSession(table);
}
private void init() {

View file

@ -85,11 +85,11 @@ public class GameController implements GameCallback {
public GameController(ManagerFactory managerFactory, Game game, ConcurrentMap<UUID, UUID> userPlayerMap, UUID tableId, UUID choosingPlayerId, GameOptions gameOptions) {
this.managerFactory = managerFactory;
gameExecutor = managerFactory.threadExecutor().getGameExecutor();
responseIdleTimeoutExecutor = managerFactory.threadExecutor().getTimeoutIdleExecutor();
gameSessionId = UUID.randomUUID();
this.gameExecutor = managerFactory.threadExecutor().getGameExecutor();
this.responseIdleTimeoutExecutor = managerFactory.threadExecutor().getTimeoutIdleExecutor();
this.gameSessionId = UUID.randomUUID();
this.userPlayerMap = userPlayerMap;
chatId = managerFactory.chatManager().createChatSession("Game " + game.getId());
this.chatId = managerFactory.chatManager().createGameChatSession(game);
this.userRequestingRollback = null;
this.game = game;
this.game.setSaveGame(managerFactory.configSettings().isSaveGameActivated());

View file

@ -1,6 +1,8 @@
package mage.server.managers;
import mage.game.Game;
import mage.game.Table;
import mage.game.tournament.Tournament;
import mage.server.ChatSession;
import mage.server.DisconnectReason;
import mage.view.ChatMessage;
@ -10,7 +12,13 @@ import java.util.UUID;
public interface ChatManager {
UUID createChatSession(String info);
UUID createRoomChatSession(UUID roomId);
UUID createTourneyChatSession(Tournament tournament);
UUID createTableChatSession(Table table);
UUID createGameChatSession(Game game);
void joinChat(UUID chatId, UUID userId);

View file

@ -54,7 +54,7 @@ public class TournamentController {
public TournamentController(ManagerFactory managerFactory, Tournament tournament, ConcurrentMap<UUID, UUID> userPlayerMap, UUID tableId) {
this.managerFactory = managerFactory;
this.userPlayerMap = userPlayerMap;
chatId = managerFactory.chatManager().createChatSession("Tournament " + tournament.getId());
this.chatId = managerFactory.chatManager().createTourneyChatSession(tournament);
this.tournament = tournament;
this.tableId = tableId;
init();
@ -261,9 +261,7 @@ public class TournamentController {
table.setState(TableState.STARTING);
tableManager.startTournamentSubMatch(null, table.getId());
tableManager.getMatch(table.getId()).ifPresent(match -> {
match.setTableId(tableId);
pair.setMatch(match);
pair.setTableId(table.getId());
pair.setMatchAndTable(match, table.getId());
player1.setState(TournamentPlayerState.DUELING);
player2.setState(TournamentPlayerState.DUELING);
});
@ -291,9 +289,7 @@ public class TournamentController {
table.setState(TableState.STARTING);
tableManager.startTournamentSubMatch(null, table.getId());
tableManager.getMatch(table.getId()).ifPresent(match -> {
match.setTableId(tableId);
round.setMatch(match);
round.setTableId(table.getId());
round.setMatchAndTable(match, table.getId());
for (TournamentPlayer player : round.getAllPlayers()) {
player.setState(TournamentPlayerState.DUELING);
}