Network upgrade and new reconnection mode (#11527)

Network upgrade and new reconnection mode:
* users can disconnect or close app without game progress loose now;
* disconnect dialog will show active tables stats and additional options;
* all active tables will be restored on reconnect (tables, tourneys, games, drafts, sideboarding, constructing);
* user must use same server and username on next connection;
* there are few minutes for reconnect until server kick off a disconnected player from all player's tables (concede/loose);
* now you can safety reconnect after IP change (after proxy/vpn/wifi/router restart);

Other improvements and fixes:
* gui: main menu - improved switch panel button, added stats about current tables/panels;
* gui: improved data sync and updates (fixes many use cases with empty battlefield, not started games/drafts/tourneys, not updatable drafts, etc);
* gui: improved stability on game updates (fixes some random errors related to wrong threads);
* server: fixed miss messages about player's disconnection problems for other players in the chat;
* refactor: simplified and improved connection and network related code, deleted outdated code, added docs;
* tests: improved load test to support lands only set for more stable performance/network testing (set TEST_AI_RANDOM_DECK_SETS = PELP and run test_TwoAIPlayGame_Multiple);
This commit is contained in:
Oleg Agafonov 2023-12-07 19:56:52 +03:00 committed by GitHub
parent 7f0558ff3c
commit 960e896903
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 1274 additions and 802 deletions

View file

@ -124,6 +124,9 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
private static int startPort = -1; private static int startPort = -1;
private static boolean debugMode = false; private static boolean debugMode = false;
private JToggleButton switchPanelsButton = null; // from main menu
private static String SWITCH_PANELS_BUTTON_NAME = "Switch panels";
private static final Map<UUID, ChatPanelBasic> CHATS = new HashMap<>(); private static final Map<UUID, ChatPanelBasic> CHATS = new HashMap<>();
private static final Map<UUID, GamePanel> GAMES = new HashMap<>(); private static final Map<UUID, GamePanel> GAMES = new HashMap<>();
private static final Map<UUID, DraftPanel> DRAFTS = new HashMap<>(); private static final Map<UUID, DraftPanel> DRAFTS = new HashMap<>();
@ -189,9 +192,6 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
} }
} }
/**
* Creates new form MageFrame
*/
public MageFrame() throws MageException { public MageFrame() throws MageException {
File cacertsFile = new File(System.getProperty("user.dir") + "/release/cacerts").getAbsoluteFile(); File cacertsFile = new File(System.getProperty("user.dir") + "/release/cacerts").getAbsoluteFile();
if (!cacertsFile.exists()) { // When running from the jar file the contents of the /release folder will have been expanded into the home folder as part of packaging if (!cacertsFile.exists()) { // When running from the jar file the contents of the /release folder will have been expanded into the home folder as part of packaging
@ -283,6 +283,23 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
initComponents(); initComponents();
// auto-update switch panels button with actual stats
desktopPane.addContainerListener(new ContainerAdapter() {
@Override
public void componentAdded(ContainerEvent e) {
if (desktopPane.getLayer(e.getComponent()) == JLayeredPane.DEFAULT_LAYER) {
updateSwitchPanelsButton();
}
}
@Override
public void componentRemoved(ContainerEvent e) {
if (desktopPane.getLayer(e.getComponent()) == JLayeredPane.DEFAULT_LAYER) {
updateSwitchPanelsButton();
}
}
});
desktopPane.setDesktopManager(new MageDesktopManager()); desktopPane.setDesktopManager(new MageDesktopManager());
setSize(1024, 768); setSize(1024, 768);
@ -303,8 +320,12 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
updateMemUsageTask = new UpdateMemUsageTask(jMemUsageLabel); updateMemUsageTask = new UpdateMemUsageTask(jMemUsageLabel);
// create default server lobby and hide it until connect
tablesPane = new TablesPane(); tablesPane = new TablesPane();
desktopPane.add(tablesPane, javax.swing.JLayeredPane.DEFAULT_LAYER); desktopPane.add(tablesPane, javax.swing.JLayeredPane.DEFAULT_LAYER);
SwingUtilities.invokeLater(() -> {
this.hideServerLobby();
});
addTooltipContainer(); addTooltipContainer();
setBackground(); setBackground();
@ -565,15 +586,27 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
} }
private AbstractButton createSwitchPanelsButton() { private AbstractButton createSwitchPanelsButton() {
final JToggleButton switchPanelsButton = new JToggleButton("Switch panels"); this.switchPanelsButton = new JToggleButton(SWITCH_PANELS_BUTTON_NAME);
switchPanelsButton.addItemListener(e -> { this.switchPanelsButton.addItemListener(e -> {
if (e.getStateChange() == ItemEvent.SELECTED) { if (e.getStateChange() == ItemEvent.SELECTED) {
createAndShowSwitchPanelsMenu((JComponent) e.getSource(), switchPanelsButton); createAndShowSwitchPanelsMenu((JComponent) e.getSource(), this.switchPanelsButton);
} }
}); });
switchPanelsButton.setFocusable(false); this.switchPanelsButton.setFocusable(false);
switchPanelsButton.setHorizontalTextPosition(SwingConstants.LEADING); this.switchPanelsButton.setHorizontalTextPosition(SwingConstants.LEADING);
return switchPanelsButton; return this.switchPanelsButton;
}
private void updateSwitchPanelsButton() {
if (this.switchPanelsButton != null) {
int totalCount = getPanelsCount(false);
int activeCount = getPanelsCount(true);
this.switchPanelsButton.setText(SWITCH_PANELS_BUTTON_NAME + String.format(" (%d)", totalCount));
this.switchPanelsButton.setToolTipText(String.format("Click to switch between panels (active panels: %d of %d)",
activeCount,
totalCount
));
}
} }
private void createAndShowSwitchPanelsMenu(final JComponent component, final AbstractButton windowButton) { private void createAndShowSwitchPanelsMenu(final JComponent component, final AbstractButton windowButton) {
@ -762,17 +795,27 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
} }
public void showTournament(UUID tournamentId) { public void showTournament(UUID tournamentId) {
// existing tourney
TournamentPane tournamentPane = null;
for (Component component : desktopPane.getComponents()) { for (Component component : desktopPane.getComponents()) {
if (component instanceof TournamentPane if (component instanceof TournamentPane
&& ((TournamentPane) component).getTournamentId().equals(tournamentId)) { && ((TournamentPane) component).getTournamentId().equals(tournamentId)) {
setActive((TournamentPane) component); tournamentPane = (TournamentPane) component;
return;
} }
} }
TournamentPane tournamentPane = new TournamentPane();
// new tourney
if (tournamentPane == null) {
tournamentPane = new TournamentPane();
desktopPane.add(tournamentPane, JLayeredPane.DEFAULT_LAYER); desktopPane.add(tournamentPane, JLayeredPane.DEFAULT_LAYER);
tournamentPane.setVisible(true); tournamentPane.setVisible(true);
tournamentPane.showTournament(tournamentId); tournamentPane.showTournament(tournamentId);
}
// if user connects on startup then there are possible multiple tables open, so keep only actual
// priority: game > constructing > draft > tourney
// TODO: activate panel by priority
setActive(tournamentPane); setActive(tournamentPane);
} }
@ -843,7 +886,7 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
LOGGER.debug("connecting (auto): " + currentConnection.getProxyType().toString() LOGGER.debug("connecting (auto): " + currentConnection.getProxyType().toString()
+ ' ' + currentConnection.getProxyHost() + ' ' + currentConnection.getProxyPort() + ' ' + currentConnection.getProxyUsername()); + ' ' + currentConnection.getProxyHost() + ' ' + currentConnection.getProxyPort() + ' ' + currentConnection.getProxyUsername());
if (MageFrame.connect(currentConnection)) { if (MageFrame.connect(currentConnection)) {
prepareAndShowTablesPane(); prepareAndShowServerLobby();
return true; return true;
} else { } else {
showMessage("Unable connect to server: " + SessionHandler.getLastConnectError()); showMessage("Unable connect to server: " + SessionHandler.getLastConnectError());
@ -1075,10 +1118,7 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
private void btnConnectActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnConnectActionPerformed private void btnConnectActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnConnectActionPerformed
if (SessionHandler.isConnected()) { if (SessionHandler.isConnected()) {
UserRequestMessage message = new UserRequestMessage("Confirm disconnect", "Are you sure you want to disconnect?"); tryDisconnectOrExit(false);
message.setButton1("No", null);
message.setButton2("Yes", PlayerAction.CLIENT_DISCONNECT);
showUserRequestDialog(message);
} else { } else {
connectDialog.showDialog(); connectDialog.showDialog();
setWindowTitle(); setWindowTitle();
@ -1148,15 +1188,34 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
} }
public void exitApp() { public void exitApp() {
tryDisconnectOrExit(true);
}
private void tryDisconnectOrExit(Boolean needExit) {
String actionName = needExit ? "exit" : "disconnect";
PlayerAction actionFull = needExit ? PlayerAction.CLIENT_EXIT_FULL : PlayerAction.CLIENT_DISCONNECT_FULL;
PlayerAction actionKeepTables = needExit ? PlayerAction.CLIENT_EXIT_KEEP_GAMES : PlayerAction.CLIENT_DISCONNECT_KEEP_GAMES;
double windowSizeRatio = 1.3;
if (SessionHandler.isConnected()) { if (SessionHandler.isConnected()) {
UserRequestMessage message = new UserRequestMessage("Confirm disconnect", "You are currently connected. Are you sure you want to disconnect?"); int activeTables = MageFrame.getInstance().getPanelsCount(true);
message.setButton1("No", null); UserRequestMessage message = new UserRequestMessage(
message.setButton2("Yes", PlayerAction.CLIENT_EXIT); "Confirm " + actionName,
"You are connected and has " + activeTables + " active table(s). You can quit from all your tables (concede) or ask server to wait a few minutes for reconnect. What to do?"
);
String totalInfo = (activeTables == 0 ? "" : String.format(" from %d table%s", activeTables, (activeTables > 1 ? "s" : "")));
message.setButton1("Cancel", null);
message.setButton2("Wait for me", actionKeepTables);
message.setButton3("Quit" + totalInfo, actionFull);
message.setWindowSizeRatio(windowSizeRatio);
MageFrame.getInstance().showUserRequestDialog(message); MageFrame.getInstance().showUserRequestDialog(message);
} else { } else {
UserRequestMessage message = new UserRequestMessage("Confirm exit", "Are you sure you want to exit?"); UserRequestMessage message = new UserRequestMessage(
message.setButton1("No", null); "Confirm " + actionName,
message.setButton2("Yes", PlayerAction.CLIENT_EXIT); "Are you sure you want to " + actionName + "?"
);
message.setButton1("Cancel", null);
message.setButton2("Yes", actionFull);
message.setWindowSizeRatio(windowSizeRatio);
MageFrame.getInstance().showUserRequestDialog(message); MageFrame.getInstance().showUserRequestDialog(message);
} }
} }
@ -1171,17 +1230,18 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
btnDeckEditor.setEnabled(true); btnDeckEditor.setEnabled(true);
} }
public void hideTables() { public void hideServerLobby() {
this.tablesPane.hideTables(); this.tablesPane.hideTables();
updateSwitchPanelsButton();
} }
public void setTableFilter() { public void setServerLobbyTablesFilter() {
if (this.tablesPane != null) { if (this.tablesPane != null) {
this.tablesPane.setTableFilter(); this.tablesPane.setTableFilter();
} }
} }
public void prepareAndShowTablesPane() { public void prepareAndShowServerLobby() {
// Update the tables pane with the new session // Update the tables pane with the new session
this.tablesPane.showTables(); this.tablesPane.showTables();
@ -1191,6 +1251,8 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
if (topPanebefore != null && topPanebefore != tablesPane) { if (topPanebefore != null && topPanebefore != tablesPane) {
setActive(topPanebefore); setActive(topPanebefore);
} }
updateSwitchPanelsButton();
} }
public void hideGames() { public void hideGames() {
@ -1253,14 +1315,18 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
} }
public void showUserRequestDialog(final UserRequestMessage userRequestMessage) { public void showUserRequestDialog(final UserRequestMessage userRequestMessage) {
final UserRequestDialog userRequestDialog = new UserRequestDialog(); if (SwingUtilities.isEventDispatchThread()) {
innerShowUserRequestDialog(userRequestMessage);
} else {
SwingUtilities.invokeLater(() -> innerShowUserRequestDialog(userRequestMessage));
}
}
private void innerShowUserRequestDialog(final UserRequestMessage userRequestMessage) {
UserRequestDialog userRequestDialog = new UserRequestDialog();
userRequestDialog.setLocation(100, 100); userRequestDialog.setLocation(100, 100);
desktopPane.add(userRequestDialog, JLayeredPane.MODAL_LAYER); desktopPane.add(userRequestDialog, JLayeredPane.MODAL_LAYER);
if (SwingUtilities.isEventDispatchThread()) {
userRequestDialog.showDialog(userRequestMessage); userRequestDialog.showDialog(userRequestMessage);
} else {
SwingUtilities.invokeLater(() -> userRequestDialog.showDialog(userRequestMessage));
}
} }
public void showErrorDialog(final String title, final String message) { public void showErrorDialog(final String title, final String message) {
@ -1478,50 +1544,55 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
DRAFTS.put(draftId, draftPanel); DRAFTS.put(draftId, draftPanel);
} }
public BalloonTip getBalloonTip() { /**
return balloonTip; * Return total number of panels/frames (game panel, deck editor panel, etc)
*
* @param onlyActive return only active panels (related to online like game panel, but not game viewer)
* @return
*/
public int getPanelsCount(boolean onlyActive) {
return (int) Arrays.stream(this.desktopPane.getComponentsInLayer(javax.swing.JLayeredPane.DEFAULT_LAYER))
.filter(Component::isVisible)
.filter(p -> p instanceof MagePane)
.map(p -> (MagePane) p)
.filter(p-> !onlyActive || p.isActiveTable())
.count();
} }
@Override @Override
public void connected(final String message) { public void connected(final String message) {
if (SwingUtilities.isEventDispatchThread()) {
setConnectButtonText(message);
enableButtons();
} else {
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
setConnectButtonText(message); setConnectButtonText(message);
enableButtons(); enableButtons();
}); });
} }
}
@Override @Override
public void disconnected(final boolean askToReconnect) { public void disconnected(final boolean askToReconnect) {
if (SwingUtilities.isEventDispatchThread()) { // Returns true if the current thread is an AWT event dispatching thread. if (SwingUtilities.isEventDispatchThread()) {
// REMOTE task, e.g. connecting // REMOTE task, e.g. connecting
LOGGER.info("Disconnected from remote task"); LOGGER.info("Disconnected from server side");
setConnectButtonText(NOT_CONNECTED_BUTTON);
disableButtons();
hideGames();
hideTables();
} else { } else {
// USER mode, e.g. user plays and got disconnect // USER mode, e.g. user plays and got disconnect
LOGGER.info("Disconnected from user mode"); LOGGER.info("Disconnected from client side");
}
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
SessionHandler.disconnect(false); // user already disconnected, can't do any online actions like quite chat // user already disconnected, can't do any online actions like quite chat
// but try to keep session
// TODO: why it ignore askToReconnect here, but use custom reconnect dialog later?! Need research
SessionHandler.disconnect(false, true);
setConnectButtonText(NOT_CONNECTED_BUTTON); setConnectButtonText(NOT_CONNECTED_BUTTON);
disableButtons(); disableButtons();
hideGames(); hideGames();
hideTables(); hideServerLobby();
if (askToReconnect) { if (askToReconnect) {
UserRequestMessage message = new UserRequestMessage("Connection lost", "The connection to server was lost. Reconnect to " + MagePreferences.getLastServerAddress() + "?"); UserRequestMessage message = new UserRequestMessage("Connection lost", "The connection to server was lost. Reconnect to " + MagePreferences.getLastServerAddress() + "?");
message.setButton1("No", null); message.setButton1("No", null);
message.setButton2("Yes", PlayerAction.CLIENT_RECONNECT); message.setButton2("Yes", PlayerAction.CLIENT_RECONNECT);
showUserRequestDialog(message); showUserRequestDialog(message);
} }
} });
);
}
} }
@Override @Override
@ -1539,8 +1610,13 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
} }
@Override @Override
public void processCallback(ClientCallback callback) { public void onCallback(ClientCallback callback) {
callbackClient.processCallback(callback); callbackClient.onCallback(callback);
}
@Override
public void onNewConnection() {
callbackClient.onNewConnection();
} }
public void sendUserReplay(PlayerAction playerAction, UserRequestMessage userRequestMessage) { public void sendUserReplay(PlayerAction playerAction, UserRequestMessage userRequestMessage) {
@ -1551,13 +1627,11 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
case CLIENT_DOWNLOAD_CARD_IMAGES: case CLIENT_DOWNLOAD_CARD_IMAGES:
DownloadPicturesService.startDownload(); DownloadPicturesService.startDownload();
break; break;
case CLIENT_DISCONNECT: case CLIENT_DISCONNECT_FULL:
if (SessionHandler.isConnected()) { doClientDisconnect(false, "You have disconnected");
SessionHandler.disconnect(false); break;
} case CLIENT_DISCONNECT_KEEP_GAMES:
tablesPane.clearChat(); doClientDisconnect(true, "You have disconnected and have few minutes to reconnect");
showMessage("You have disconnected");
setWindowTitle();
break; break;
case CLIENT_QUIT_TOURNAMENT: case CLIENT_QUIT_TOURNAMENT:
SessionHandler.quitTournament(userRequestMessage.getTournamentId()); SessionHandler.quitTournament(userRequestMessage.getTournamentId());
@ -1580,15 +1654,13 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
} }
removeGame(userRequestMessage.getGameId()); removeGame(userRequestMessage.getGameId());
break; break;
case CLIENT_EXIT: case CLIENT_EXIT_FULL:
if (SessionHandler.isConnected()) { doClientDisconnect(false, "");
SessionHandler.disconnect(false); doClientShutdownAndExit();
} break;
CardRepository.instance.closeDB(); case CLIENT_EXIT_KEEP_GAMES:
tablesPane.cleanUp(); doClientDisconnect(true, "");
Plugins.instance.shutdown(); doClientShutdownAndExit();
dispose();
System.exit(0);
break; break;
case CLIENT_REMOVE_TABLE: case CLIENT_REMOVE_TABLE:
SessionHandler.removeTable(userRequestMessage.getRoomId(), userRequestMessage.getTableId()); SessionHandler.removeTable(userRequestMessage.getRoomId(), userRequestMessage.getTableId());
@ -1609,6 +1681,26 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
} }
} }
private void doClientDisconnect(boolean keepMySessionActive, String afterMessage) {
if (SessionHandler.isConnected()) {
SessionHandler.disconnect(false, keepMySessionActive);
}
tablesPane.clearChat();
setWindowTitle();
if (!afterMessage.isEmpty()) {
showMessage(afterMessage);
}
}
private void doClientShutdownAndExit() {
tablesPane.cleanUp();
CardRepository.instance.closeDB();
Plugins.instance.shutdown();
dispose();
System.exit(0);
}
private void endTables() { private void endTables() {
for (UUID gameId : GAMES.keySet()) { for (UUID gameId : GAMES.keySet()) {
SessionHandler.quitMatch(gameId); SessionHandler.quitMatch(gameId);

View file

@ -3,21 +3,19 @@
import java.awt.*; import java.awt.*;
/** /**
* GUI: basic class for all full screen frames/tabs (example: game pane, deck editor pane, card viewer, etc)
*
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public abstract class MagePane extends javax.swing.JLayeredPane { public abstract class MagePane extends javax.swing.JLayeredPane {
private String title = "no title set"; private String title = "no title set";
/**
* Creates new form MagePane
*/
public MagePane() { public MagePane() {
initComponents(); initComponents();
} }
public void changeGUISize() { public void changeGUISize() {
} }
public void setTitle(String title) { public void setTitle(String title) {
@ -53,6 +51,12 @@
return this; return this;
} }
/**
* Active table: game pane, deck editor in sideboarding mode, etc
* Non active table: client side panes like card viewer, deck viewer, etc
*/
abstract public boolean isActiveTable();
/** /**
* This method is called from within the constructor to initialize the form. * This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always * WARNING: Do NOT modify this code. The content of this method is always

View file

@ -5,6 +5,7 @@ import static mage.cards.decks.DeckFormats.XMAGE;
import mage.client.chat.LocalCommands; import mage.client.chat.LocalCommands;
import mage.client.constants.Constants.DeckEditorMode; import mage.client.constants.Constants.DeckEditorMode;
import mage.client.dialog.PreferencesDialog; import mage.client.dialog.PreferencesDialog;
import mage.client.preference.MagePreferences;
import mage.constants.ManaType; import mage.constants.ManaType;
import mage.constants.PlayerAction; import mage.constants.PlayerAction;
import mage.game.match.MatchOptions; import mage.game.match.MatchOptions;
@ -41,7 +42,6 @@ public final class SessionHandler {
} }
public static void startSession(MageFrame mageFrame) { public static void startSession(MageFrame mageFrame) {
session = new SessionImpl(mageFrame); session = new SessionImpl(mageFrame);
session.setJsonLogActive("true".equals(PreferencesDialog.getCachedValue(PreferencesDialog.KEY_JSON_GAME_LOG_AUTO_SAVE, "true"))); session.setJsonLogActive("true".equals(PreferencesDialog.getCachedValue(PreferencesDialog.KEY_JSON_GAME_LOG_AUTO_SAVE, "true")));
} }
@ -64,10 +64,22 @@ public final class SessionHandler {
public static boolean connect(Connection connection) { public static boolean connect(Connection connection) {
lastConnectError = ""; lastConnectError = "";
// restore last used session
String restoreSessionId = MagePreferences.findRestoreSession(connection.getHost(), connection.getUsername());
if (!restoreSessionId.isEmpty()) {
logger.info("Connect: trying to restore old session for user " + connection.getUsername());
session.setRestoreSessionId(restoreSessionId);
}
// connect
if (session.connectStart(connection)) { if (session.connectStart(connection)) {
// save current session for restore
MagePreferences.saveRestoreSession(connection.getHost(), connection.getUsername(), getSessionId());
return true; return true;
} else { } else {
lastConnectError = session.getLastError(); lastConnectError = session.getLastError();
disconnect(false, true);
return false; return false;
} }
} }
@ -80,8 +92,16 @@ public final class SessionHandler {
return session.connectAbort(); return session.connectAbort();
} }
public static void disconnect(boolean showmessage) { public static void disconnect(boolean askForReconnect, boolean keepMySessionActive) {
session.connectStop(showmessage); if (!keepMySessionActive) {
String serverName = session.getServerHost();
String userName = session.getUserName();
if (!serverName.isEmpty() && !userName.isEmpty()) {
MagePreferences.saveRestoreSession(serverName, userName, "");
}
}
session.connectStop(askForReconnect, keepMySessionActive);
} }
public static void sendPlayerAction(PlayerAction playerAction, UUID gameId, Object relatedUserId) { public static void sendPlayerAction(PlayerAction playerAction, UUID gameId, Object relatedUserId) {

View file

@ -9,10 +9,9 @@ import mage.client.dialog.PreferencesDialog;
import mage.client.plugins.adapters.MageActionCallback; import mage.client.plugins.adapters.MageActionCallback;
import mage.client.plugins.impl.Plugins; import mage.client.plugins.impl.Plugins;
import mage.client.util.ClientDefaultSettings; import mage.client.util.ClientDefaultSettings;
import mage.utils.SystemUtil; import mage.util.ThreadUtils;
import mage.view.CardView; import mage.view.CardView;
import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.util.UUID; import java.util.UUID;
@ -107,7 +106,7 @@ public class VirtualCardInfo {
} }
public boolean prepared() { public boolean prepared() {
SystemUtil.ensureRunInGUISwingThread(); ThreadUtils.ensureRunInGUISwingThread();
return this.cardView != null return this.cardView != null
&& this.cardComponent != null && this.cardComponent != null

View file

@ -37,7 +37,7 @@ public final class LocalCommands {
return false; return false;
} }
final String serverAddress = SessionHandler.getSession().getServerHostname().orElse(""); final String serverAddress = SessionHandler.getSession().getServerHost();
Optional<String> response = Optional.empty(); Optional<String> response = Optional.empty();
String command = st.nextToken(); String command = st.nextToken();
@ -69,6 +69,6 @@ public final class LocalCommands {
final String text = new StringBuilder().append("<font color=yellow>").append(response).append("</font>").toString(); final String text = new StringBuilder().append("<font color=yellow>").append(response).append("</font>").toString();
ClientCallback chatMessage = new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId, ClientCallback chatMessage = new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId,
new ChatMessage("", text, new Date(), null, ChatMessage.MessageColor.BLUE)); new ChatMessage("", text, new Date(), null, ChatMessage.MessageColor.BLUE));
MageFrame.getInstance().processCallback(chatMessage); MageFrame.getInstance().onCallback(chatMessage);
} }
} }

View file

@ -11,7 +11,6 @@ import mage.client.game.GamePanel;
import mage.game.command.Plane; import mage.game.command.Plane;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.util.GameLog; import mage.util.GameLog;
import mage.utils.ThreadUtils;
import mage.view.CardView; import mage.view.CardView;
import mage.view.PlaneView; import mage.view.PlaneView;

View file

@ -1,6 +1,6 @@
package mage.client.components; package mage.client.components;
import mage.utils.ThreadUtils; import mage.util.ThreadUtils;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.awt.Component; import java.awt.Component;

View file

@ -12,13 +12,14 @@ import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
* GUI: deck editor, used all around the app
*
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public class DeckEditorPane extends MagePane { public class DeckEditorPane extends MagePane {
/** private UUID tableId = null;
* Creates new form TablesPane
*/
public DeckEditorPane() { public DeckEditorPane() {
boolean initialized = false; boolean initialized = false;
if (Plugins.instance.isThemePluginLoaded()) { if (Plugins.instance.isThemePluginLoaded()) {
@ -45,6 +46,7 @@ public class DeckEditorPane extends MagePane {
} }
public void show(DeckEditorMode mode, Deck deck, String name, UUID tableId, int time) { public void show(DeckEditorMode mode, Deck deck, String name, UUID tableId, int time) {
this.tableId = tableId;
if (mode == DeckEditorMode.SIDEBOARDING if (mode == DeckEditorMode.SIDEBOARDING
|| mode == DeckEditorMode.LIMITED_BUILDING || mode == DeckEditorMode.LIMITED_BUILDING
|| mode == DeckEditorMode.LIMITED_SIDEBOARD_BUILDING) { || mode == DeckEditorMode.LIMITED_SIDEBOARD_BUILDING) {
@ -60,6 +62,11 @@ public class DeckEditorPane extends MagePane {
this.repaint(); this.repaint();
} }
@Override
public boolean isActiveTable() {
return this.tableId != null;
}
public DeckEditorMode getDeckEditorMode() { public DeckEditorMode getDeckEditorMode() {
return this.deckEditorPanel1.getDeckEditorMode(); return this.deckEditorPanel1.getDeckEditorMode();
} }

View file

@ -52,6 +52,11 @@ public class CollectionViewerPane extends MagePane {
); );
} }
@Override
public boolean isActiveTable() {
return false;
}
@Override @Override
public void setVisible(boolean aFlag) { public void setVisible(boolean aFlag) {
super.setVisible(aFlag); super.setVisible(aFlag);

View file

@ -1,6 +1,5 @@
package mage.client.dialog; package mage.client.dialog;
import mage.cards.repository.RepositoryUtil;
import mage.choices.Choice; import mage.choices.Choice;
import mage.choices.ChoiceImpl; import mage.choices.ChoiceImpl;
import mage.client.MageFrame; import mage.client.MageFrame;
@ -8,10 +7,8 @@ import mage.client.SessionHandler;
import mage.client.preference.MagePreferences; import mage.client.preference.MagePreferences;
import mage.client.util.ClientDefaultSettings; import mage.client.util.ClientDefaultSettings;
import mage.client.util.gui.countryBox.CountryItemEditor; import mage.client.util.gui.countryBox.CountryItemEditor;
import mage.client.util.sets.ConstructedFormats;
import mage.remote.Connection; import mage.remote.Connection;
import mage.utils.StreamUtils; import mage.utils.StreamUtils;
import mage.utils.ThreadUtils;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import javax.swing.*; import javax.swing.*;
@ -702,7 +699,7 @@ public class ConnectDialog extends MageDialog {
// so the connection dialog will be visible all that time // so the connection dialog will be visible all that time
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
doAfterConnected(); doAfterConnected();
MageFrame.getInstance().prepareAndShowTablesPane(); MageFrame.getInstance().prepareAndShowServerLobby();
btnConnect.setEnabled(true); btnConnect.setEnabled(true);
}); });
} else { } else {

View file

@ -602,7 +602,7 @@ public class NewTableDialog extends MageDialog {
options.setQuitRatio((Integer) this.spnQuitRatio.getValue()); options.setQuitRatio((Integer) this.spnQuitRatio.getValue());
options.setMinimumRating((Integer) this.spnMinimumRating.getValue()); options.setMinimumRating((Integer) this.spnMinimumRating.getValue());
options.setEdhPowerLevel((Integer) this.spnEdhPowerLevel.getValue()); options.setEdhPowerLevel((Integer) this.spnEdhPowerLevel.getValue());
String serverAddress = SessionHandler.getSession().getServerHostname().orElse(""); String serverAddress = SessionHandler.getSession().getServerHost();
options.setBannedUsers(IgnoreList.getIgnoredUsers(serverAddress)); options.setBannedUsers(IgnoreList.getIgnoredUsers(serverAddress));
options.setLimited(options.getDeckType().startsWith("Limited")); options.setLimited(options.getDeckType().startsWith("Limited"));
if (options.getDeckType().startsWith("Variant Magic - Freeform Unlimited Commander")) { if (options.getDeckType().startsWith("Variant Magic - Freeform Unlimited Commander")) {

View file

@ -1322,7 +1322,7 @@ public class NewTournamentDialog extends MageDialog {
} }
} }
String serverAddress = SessionHandler.getSession().getServerHostname().orElse(""); String serverAddress = SessionHandler.getSession().getServerHost();
tOptions.getMatchOptions().setBannedUsers(IgnoreList.getIgnoredUsers(serverAddress)); tOptions.getMatchOptions().setBannedUsers(IgnoreList.getIgnoredUsers(serverAddress));
tOptions.getMatchOptions().setMatchTimeLimit((MatchTimeLimit) this.cbTimeLimit.getSelectedItem()); tOptions.getMatchOptions().setMatchTimeLimit((MatchTimeLimit) this.cbTimeLimit.getSelectedItem());

View file

@ -10,7 +10,8 @@ import javax.swing.plaf.basic.BasicInternalFrameUI;
import java.awt.*; import java.awt.*;
/** /**
* App GUI: confirm some actions from the user (example: close the app) * GUI: global window message with additional action to choose (example: close the app)
* Can be used in any places (in games, in app, etc)
* *
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
@ -74,6 +75,17 @@ public class UserRequestDialog extends MageDialog {
} else { } else {
this.btn3.setVisible(false); this.btn3.setVisible(false);
} }
// improved auto-size
// height looks bad, so change only width
this.pack();
Dimension newPreferedSize = new Dimension(this.getPreferredSize());
newPreferedSize.setSize(
newPreferedSize.width * userRequestMessage.getWindowSizeRatio(),
newPreferedSize.height/* * userRequestMessage.getWindowSizeRatio()*/
);
this.setPreferredSize(newPreferedSize);
this.pack(); this.pack();
this.revalidate(); this.revalidate();
this.repaint(); this.repaint();
@ -177,7 +189,7 @@ public class UserRequestDialog extends MageDialog {
}//GEN-LAST:event_btn3ActionPerformed }//GEN-LAST:event_btn3ActionPerformed
private void sendUserReplay(PlayerAction playerAction) { private void sendUserReplay(PlayerAction playerAction) {
MageFrame.getInstance().sendUserReplay(playerAction, userRequestMessage); SwingUtilities.invokeLater(() ->MageFrame.getInstance().sendUserReplay(playerAction, userRequestMessage));
} }
// Variables declaration - do not modify//GEN-BEGIN:variables // Variables declaration - do not modify//GEN-BEGIN:variables

View file

@ -9,15 +9,14 @@ import mage.client.MagePane;
import mage.client.plugins.impl.Plugins; import mage.client.plugins.impl.Plugins;
/** /**
* Game GUI: draft panel with scrolls * Game GUI: draft frame
* *
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public class DraftPane extends MagePane { public class DraftPane extends MagePane {
/** UUID draftId = null;
* Creates new form DraftPane
*/
public DraftPane() { public DraftPane() {
boolean initialized = false; boolean initialized = false;
if (Plugins.instance.isThemePluginLoaded()) { if (Plugins.instance.isThemePluginLoaded()) {
@ -44,10 +43,16 @@ public class DraftPane extends MagePane {
} }
public void showDraft(UUID draftId) { public void showDraft(UUID draftId) {
this.draftId = draftId;
this.setTitle("Draft - " + draftId); this.setTitle("Draft - " + draftId);
this.draftPanel1.showDraft(draftId); this.draftPanel1.showDraft(draftId);
} }
@Override
public boolean isActiveTable() {
return this.draftId != null;
}
public void removeDraft() { public void removeDraft() {
draftPanel1.cleanUp(); draftPanel1.cleanUp();
this.removeFrame(); this.removeFrame();

View file

@ -92,7 +92,7 @@ public class BattlefieldPanel extends javax.swing.JLayeredPane {
gameUpdateTimer = new Timer(GAME_REDRAW_TIMEOUT_MS, evt -> SwingUtilities.invokeLater(() -> { gameUpdateTimer = new Timer(GAME_REDRAW_TIMEOUT_MS, evt -> SwingUtilities.invokeLater(() -> {
gameUpdateTimer.stop(); gameUpdateTimer.stop();
ClientCallback updateMessage = new ClientCallback(ClientCallbackMethod.GAME_REDRAW_GUI, gameId); ClientCallback updateMessage = new ClientCallback(ClientCallbackMethod.GAME_REDRAW_GUI, gameId);
MageFrame.getInstance().processCallback(updateMessage); MageFrame.getInstance().onCallback(updateMessage);
})); }));
} }

View file

@ -7,7 +7,7 @@ import javax.swing.*;
import mage.client.MagePane; import mage.client.MagePane;
/** /**
* Game GUI: game panel with scrollbars * Game GUI: game frame (game panel with scrolls)
* *
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
@ -31,6 +31,11 @@ public class GamePane extends MagePane {
gamePanel.showGame(gameId, playerId, this); gamePanel.showGame(gameId, playerId, this);
} }
@Override
public boolean isActiveTable() {
return this.gameId != null;
}
public void cleanUp() { public void cleanUp() {
gamePanel.cleanUp(); gamePanel.cleanUp();
} }

View file

@ -1469,6 +1469,10 @@ public final class GamePanel extends javax.swing.JPanel {
this.feedbackPanel.prepareFeedback(FeedbackMode.QUESTION, question, false, options, true, gameView.getPhase()); this.feedbackPanel.prepareFeedback(FeedbackMode.QUESTION, question, false, options, true, gameView.getPhase());
} }
public boolean isMissGameData() {
return lastGameData.game == null || lastGameData.game.getPlayers().isEmpty();
}
private void keepLastGameData(int messageId, GameView game, boolean showPlayable, Map<String, Serializable> options, Set<UUID> targets) { private void keepLastGameData(int messageId, GameView game, boolean showPlayable, Map<String, Serializable> options, Set<UUID> targets) {
lastGameData.messageId = messageId; lastGameData.messageId = messageId;
lastGameData.setNewGame(game); lastGameData.setNewGame(game);

View file

@ -585,7 +585,7 @@ public class PlayerPanelExt extends javax.swing.JPanel {
resized = ImageHelper.getResizedImage(BufferedImageBuilder.bufferImage(image, BufferedImage.TYPE_INT_ARGB), r); resized = ImageHelper.getResizedImage(BufferedImageBuilder.bufferImage(image, BufferedImage.TYPE_INT_ARGB), r);
cheat = new JButton(); cheat = new JButton();
cheat.setIcon(new ImageIcon(resized)); cheat.setIcon(new ImageIcon(resized));
cheat.setToolTipText("Cheat button"); cheat.setToolTipText("Cheat button (activate it on your priority only)");
cheat.addActionListener(e -> btnCheatActionPerformed(e)); cheat.addActionListener(e -> btnCheatActionPerformed(e));
// tools button like hints // tools button like hints

View file

@ -23,7 +23,7 @@ import mage.components.CardInfoPane;
import mage.constants.EnlargeMode; import mage.constants.EnlargeMode;
import mage.constants.Zone; import mage.constants.Zone;
import mage.util.DebugUtil; import mage.util.DebugUtil;
import mage.utils.ThreadUtils; import mage.util.ThreadUtils;
import mage.view.CardView; import mage.view.CardView;
import mage.view.PermanentView; import mage.view.PermanentView;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;

View file

@ -4,6 +4,7 @@ import com.google.common.collect.Sets;
import mage.client.MageFrame; import mage.client.MageFrame;
import mage.client.util.ClientDefaultSettings; import mage.client.util.ClientDefaultSettings;
import java.util.Date;
import java.util.Set; import java.util.Set;
import java.util.prefs.BackingStoreException; import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences; import java.util.prefs.Preferences;
@ -18,6 +19,7 @@ public final class MagePreferences {
private static final String KEY_EMAIL = "email"; private static final String KEY_EMAIL = "email";
private static final String KEY_AUTO_CONNECT = "autoConnect"; private static final String KEY_AUTO_CONNECT = "autoConnect";
private static final String NODE_KEY_IGNORE_LIST = "ignoreListString"; private static final String NODE_KEY_IGNORE_LIST = "ignoreListString";
private static final String NODE_KEY_RESTORE_SESSIONS_LIST = "restoreSessionsListString";
private static String lastServerAddress = ""; private static String lastServerAddress = "";
private static int lastServerPort = 0; private static int lastServerPort = 0;
@ -64,6 +66,7 @@ public final class MagePreferences {
return userName; return userName;
} }
// For clients older than 1.4.7, userName is stored without a serverAddress prefix. // For clients older than 1.4.7, userName is stored without a serverAddress prefix.
// TODO: outdated code, can be removed, 2023-12-05
return prefs().get(KEY_USER_NAME, ""); return prefs().get(KEY_USER_NAME, "");
} }
@ -143,6 +146,19 @@ public final class MagePreferences {
return prefs().node(NODE_KEY_IGNORE_LIST).node(serverAddress); return prefs().node(NODE_KEY_IGNORE_LIST).node(serverAddress);
} }
private static Preferences restoreSessionsListNode(String serverAddress) {
return prefs().node(NODE_KEY_RESTORE_SESSIONS_LIST).node(serverAddress);
}
public static void saveRestoreSession(String serverAddress, String userName, String sessionId) {
restoreSessionsListNode(serverAddress).put(userName, sessionId);
restoreSessionsListNode(serverAddress).putLong("lastUpdated", new Date().getTime());
}
public static String findRestoreSession(String serverAddress, String userName) {
return restoreSessionsListNode(serverAddress).get(userName, "");
}
public static void saveLastServer() { public static void saveLastServer() {
lastServerAddress = getServerAddressWithDefault(ClientDefaultSettings.serverName); lastServerAddress = getServerAddressWithDefault(ClientDefaultSettings.serverName);
lastServerPort = getServerPortWithDefault(ClientDefaultSettings.port); lastServerPort = getServerPortWithDefault(ClientDefaultSettings.port);

View file

@ -28,31 +28,52 @@ import java.awt.event.KeyEvent;
import java.util.*; import java.util.*;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com, JayDi85
*/ */
public class CallbackClientImpl implements CallbackClient { public class CallbackClientImpl implements CallbackClient {
private static final Logger logger = Logger.getLogger(CallbackClientImpl.class); private static final Logger logger = Logger.getLogger(CallbackClientImpl.class);
private static final boolean DEBUG_CALLBACK_MESSAGES_LOG = false; // show all callback messages (server commands)
private final MageFrame frame; private final MageFrame frame;
private final Map<ClientCallbackType, Integer> lastMessages; private final Map<ClientCallbackType, Integer> lastMessages;
private final Map<UUID, GameClientMessage> firstGameData;
public CallbackClientImpl(MageFrame frame) { public CallbackClientImpl(MageFrame frame) {
this.frame = frame; this.frame = frame;
this.lastMessages = new HashMap<>(); this.lastMessages = new HashMap<>();
this.firstGameData = new HashMap<>();
Arrays.stream(ClientCallbackType.values()).forEach(t -> this.lastMessages.put(t, 0)); Arrays.stream(ClientCallbackType.values()).forEach(t -> this.lastMessages.put(t, 0));
} }
@Override @Override
public synchronized void processCallback(final ClientCallback callback) { public void onNewConnection() {
// must clean temp data for each new connection
this.lastMessages.clear();
this.firstGameData.clear();
}
@Override
public synchronized void onCallback(final ClientCallback callback) {
callback.decompressData(); callback.decompressData();
// put replay related code here // put replay related code here
SaveObjectUtil.saveObject(callback.getData(), callback.getMethod().toString()); SaveObjectUtil.saveObject(callback.getData(), callback.getMethod().toString());
// reconnect fix with miss data, part 1 of 2
// on reconnect many events can come in diff order, so init GameView data with first available info
// game_start event can come after game view - must save it here (part 1) before real use in swing thread (part 2)
if (callback.getData() instanceof GameClientMessage) {
firstGameData.putIfAbsent(callback.getObjectId(), (GameClientMessage) callback.getData());
}
// all GUI related code must be executed in swing thread // all GUI related code must be executed in swing thread
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
try { try {
logger.debug("message " + callback.getMessageId() + " - " + callback.getMethod().getType() + " - " + callback.getMethod()); if (DEBUG_CALLBACK_MESSAGES_LOG) {
logger.info("message " + callback.getMessageId() + " - " + callback.getMethod().getType() + " - " + callback.getMethod());
}
// process bad connection (events can income in wrong order, so outdated data must be ignored) // process bad connection (events can income in wrong order, so outdated data must be ignored)
// - table/dialog events like game start, game end, choose dialog - must be processed anyway // - table/dialog events like game start, game end, choose dialog - must be processed anyway
@ -92,6 +113,24 @@ public class CallbackClientImpl implements CallbackClient {
TableClientMessage message = (TableClientMessage) callback.getData(); TableClientMessage message = (TableClientMessage) callback.getData();
GameManager.instance.setCurrentPlayerUUID(message.getPlayerId()); GameManager.instance.setCurrentPlayerUUID(message.getPlayerId());
gameStarted(callback.getMessageId(), message.getGameId(), message.getPlayerId()); gameStarted(callback.getMessageId(), message.getGameId(), message.getPlayerId());
// reconnect fix with miss data, part 2 of 2
// START_GAME event can come after GAME_INIT or any other, so must force update with first info
// START_GAME even raises before a real game start, so it hasn't GameView in payload
GamePanel gamePanel = MageFrame.getGame(callback.getObjectId());
if (gamePanel != null && gamePanel.isMissGameData()) {
GameClientMessage mes = firstGameData.getOrDefault(callback.getObjectId(), null);
if (mes != null) {
logger.warn("Found miss game data, requesting latest info... (possible reason: reconnect)");
gamePanel.init(callback.getMessageId(), mes.getGameView(), true);
// ask server to resent latest data
// TODO: replace by special async request like requestGameUpdate
SessionHandler.sendPlayerUUID(callback.getObjectId(), UUID.randomUUID());
} else {
// it's ok, new game come here
// logger.error("Found miss game data, but can't find any usefull info (report to developers)");
}
}
break; break;
} }
@ -101,12 +140,6 @@ public class CallbackClientImpl implements CallbackClient {
break; break;
} }
case START_DRAFT: {
TableClientMessage message = (TableClientMessage) callback.getData();
draftStarted(callback.getMessageId(), message.getGameId(), message.getPlayerId());
break;
}
case REPLAY_GAME: { case REPLAY_GAME: {
replayGame(callback.getObjectId()); replayGame(callback.getObjectId());
break; break;
@ -126,7 +159,7 @@ public class CallbackClientImpl implements CallbackClient {
ChatMessage message = (ChatMessage) callback.getData(); ChatMessage message = (ChatMessage) callback.getData();
// Drop messages from ignored users // Drop messages from ignored users
if (message.getUsername() != null && IgnoreList.IGNORED_MESSAGE_TYPES.contains(message.getMessageType())) { if (message.getUsername() != null && IgnoreList.IGNORED_MESSAGE_TYPES.contains(message.getMessageType())) {
final String serverAddress = SessionHandler.getSession().getServerHostname().orElse(""); final String serverAddress = SessionHandler.getSession().getServerHost();
if (IgnoreList.userIsIgnored(serverAddress, message.getUsername())) { if (IgnoreList.userIsIgnored(serverAddress, message.getUsername())) {
break; break;
} }
@ -426,6 +459,12 @@ public class CallbackClientImpl implements CallbackClient {
break; break;
} }
case START_DRAFT: {
TableClientMessage message = (TableClientMessage) callback.getData();
draftStarted(callback.getMessageId(), message.getGameId(), message.getPlayerId());
break;
}
case DRAFT_OVER: { case DRAFT_OVER: {
MageFrame.removeDraft(callback.getObjectId()); MageFrame.removeDraft(callback.getObjectId());
break; break;
@ -544,7 +583,7 @@ public class CallbackClientImpl implements CallbackClient {
null, null, MessageType.USER_INFO, ChatMessage.MessageColor.BLUE); null, null, MessageType.USER_INFO, ChatMessage.MessageColor.BLUE);
break; break;
case TABLES: case TABLES:
String serverAddress = SessionHandler.getSession().getServerHostname().orElse(""); String serverAddress = SessionHandler.getSession().getServerHost();
usedPanel.receiveMessage("", new StringBuilder("Download card images by using the \"Images\" main menu.") usedPanel.receiveMessage("", new StringBuilder("Download card images by using the \"Images\" main menu.")
.append("<br/>Download icons and symbols by using the \"Symbols\" main menu.") .append("<br/>Download icons and symbols by using the \"Symbols\" main menu.")
.append("<br/>\\list - show a list of available chat commands.") .append("<br/>\\list - show a list of available chat commands.")

View file

@ -1,4 +1,3 @@
package mage.client.table; package mage.client.table;
import java.util.UUID; import java.util.UUID;
@ -9,14 +8,14 @@ import mage.client.SessionHandler;
import mage.client.plugins.impl.Plugins; import mage.client.plugins.impl.Plugins;
/** /**
* Game GUI: lobby frame
* *
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public class TablesPane extends MagePane { public class TablesPane extends MagePane {
/** UUID roomId = null;
* Creates new form TablesPane
*/
public TablesPane() { public TablesPane() {
boolean initialized = false; boolean initialized = false;
if (Plugins.instance.isThemePluginLoaded()) { if (Plugins.instance.isThemePluginLoaded()) {
@ -47,11 +46,17 @@ public class TablesPane extends MagePane {
public void showTables() { public void showTables() {
UUID roomId = SessionHandler.getSession().getMainRoomId(); UUID roomId = SessionHandler.getSession().getMainRoomId();
if (roomId != null) { if (roomId != null) {
this.setTitle("Tables"); this.roomId = roomId;
this.setTitle("Server's lobby");
tablesPanel.showTables(roomId); tablesPanel.showTables(roomId);
this.repaint(); this.repaint();
} }
}
@Override
public boolean isActiveTable() {
// it's defalt server lobby, so don't count it as active
return false;
} }
public void hideTables() { public void hideTables() {

View file

@ -940,7 +940,7 @@ public class TablesPanel extends javax.swing.JPanel {
// Hide games of ignored players // Hide games of ignored players
java.util.List<RowFilter<Object, Object>> ignoreListFilterList = new ArrayList<>(); java.util.List<RowFilter<Object, Object>> ignoreListFilterList = new ArrayList<>();
String serverAddress = SessionHandler.getSession().getServerHostname().orElse(""); String serverAddress = SessionHandler.getSession().getServerHost();
final Set<String> ignoreListCopy = IgnoreList.getIgnoredUsers(serverAddress); final Set<String> ignoreListCopy = IgnoreList.getIgnoredUsers(serverAddress);
if (!ignoreListCopy.isEmpty()) { if (!ignoreListCopy.isEmpty()) {
ignoreListFilterList.add(new RowFilter<Object, Object>() { ignoreListFilterList.add(new RowFilter<Object, Object>() {
@ -1718,7 +1718,7 @@ public class TablesPanel extends javax.swing.JPanel {
options.setRollbackTurnsAllowed(true); options.setRollbackTurnsAllowed(true);
options.setQuitRatio(100); options.setQuitRatio(100);
options.setMinimumRating(0); options.setMinimumRating(0);
String serverAddress = SessionHandler.getSession().getServerHostname().orElse(""); String serverAddress = SessionHandler.getSession().getServerHost();
options.setBannedUsers(IgnoreList.getIgnoredUsers(serverAddress)); options.setBannedUsers(IgnoreList.getIgnoredUsers(serverAddress));
table = SessionHandler.createTable(roomId, options); table = SessionHandler.createTable(roomId, options);

View file

@ -4,24 +4,30 @@ import java.util.UUID;
import mage.client.MagePane; import mage.client.MagePane;
/** /**
* Game GUI: tournament frame
* *
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public class TournamentPane extends MagePane { public class TournamentPane extends MagePane {
/** UUID tournamentId = null;
* Creates new form TournamentPane
*/
public TournamentPane() { public TournamentPane() {
initComponents(); initComponents();
} }
public void showTournament(UUID tournamentId) { public void showTournament(UUID tournamentId) {
this.tournamentId = tournamentId;
this.setTitle("Tournament " + tournamentId); this.setTitle("Tournament " + tournamentId);
this.tournamentPanel.showTournament(tournamentId); this.tournamentPanel.showTournament(tournamentId);
this.repaint(); this.repaint();
} }
@Override
public boolean isActiveTable() {
return this.tournamentId != null;
}
public void removeTournament() { public void removeTournament() {
tournamentPanel.cleanUp(); tournamentPanel.cleanUp();
removeFrame(); removeFrame();

View file

@ -53,15 +53,15 @@ public final class IgnoreList {
} }
MagePreferences.addIgnoredUser(serverAddress, user); MagePreferences.addIgnoredUser(serverAddress, user);
updateTablesTable(); updateServerLobbyTables();
return "Added " + user + " to your ignore list on " + serverAddress + " (total: " + getIgnoredUsers(serverAddress).size() + ")"; return "Added " + user + " to your ignore list on " + serverAddress + " (total: " + getIgnoredUsers(serverAddress).size() + ")";
} }
private static void updateTablesTable() { private static void updateServerLobbyTables() {
MageFrame mageFrame = MageFrame.getInstance(); MageFrame mageFrame = MageFrame.getInstance();
if (mageFrame != null) { if (mageFrame != null) {
mageFrame.setTableFilter(); mageFrame.setServerLobbyTablesFilter();
} }
} }
@ -70,7 +70,7 @@ public final class IgnoreList {
return usage(serverAddress); return usage(serverAddress);
} }
if (MagePreferences.removeIgnoredUser(serverAddress, user)) { if (MagePreferences.removeIgnoredUser(serverAddress, user)) {
updateTablesTable(); updateServerLobbyTables();
return "Removed " + user + " from your ignore list on " + serverAddress + " (total: " + getIgnoredUsers(serverAddress).size() + ")"; return "Removed " + user + " from your ignore list on " + serverAddress + " (total: " + getIgnoredUsers(serverAddress).size() + ")";
} else { } else {
return "No such user \"" + user + "\" on your ignore list on " + serverAddress + " (total: " + getIgnoredUsers(serverAddress).size() + ")"; return "No such user \"" + user + "\" on your ignore list on " + serverAddress + " (total: " + getIgnoredUsers(serverAddress).size() + ")";

View file

@ -19,7 +19,7 @@ import javax.sound.sampled.SourceDataLine;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import mage.utils.ThreadUtils; import mage.util.ThreadUtils;
public class LinePool { public class LinePool {

View file

@ -86,8 +86,13 @@ public class MultiConnectTest {
} }
@Override @Override
public void processCallback(ClientCallback callback) { public void onNewConnection() {
logger.info("processCallback"); logger.info("onNewConnection");
}
@Override
public void onCallback(ClientCallback callback) {
logger.info("onCallback");
} }
} }

View file

@ -32,7 +32,7 @@ public interface MageServer {
boolean authResetPassword(String sessionId, String email, String authToken, String password) throws MageException; boolean authResetPassword(String sessionId, String email, String authToken, String password) throws MageException;
boolean connectUser(String userName, String password, String sessionId, MageVersion version, String userIdStr) throws MageException; boolean connectUser(String userName, String password, String sessionId, String restoreSessionId, MageVersion version, String userIdStr) throws MageException;
boolean connectAdmin(String password, String sessionId, MageVersion version) throws MageException; boolean connectAdmin(String password, String sessionId, MageVersion version) throws MageException;

View file

@ -1,12 +1,13 @@
package mage.interfaces.callback; package mage.interfaces.callback;
/** /**
* Network: client to process income server commands
* *
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public interface CallbackClient { public interface CallbackClient {
void processCallback(ClientCallback callback); void onNewConnection();
void onCallback(ClientCallback callback);
} }

View file

@ -2,12 +2,14 @@ package mage.interfaces.callback;
import mage.remote.traffic.ZippedObject; import mage.remote.traffic.ZippedObject;
import mage.utils.CompressUtil; import mage.utils.CompressUtil;
import mage.utils.ThreadUtils; import mage.util.ThreadUtils;
import java.io.Serializable; import java.io.Serializable;
import java.util.UUID; import java.util.UUID;
/** /**
* Network: server's event to proccess on client side
*
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public class ClientCallback implements Serializable { public class ClientCallback implements Serializable {

View file

@ -1,7 +1,7 @@
package mage.interfaces.callback; package mage.interfaces.callback;
/** /**
* Server's commands to process on client side. Commands can come in un-synced state due bad/slow network * Network: server's commands to process on client side. Commands can come in un-synced state due bad/slow network
* <p> * <p>
* Can be: * Can be:
* - critical events (messages, game events, choose dialogs, etc) * - critical events (messages, game events, choose dialogs, etc)

View file

@ -1,7 +1,7 @@
package mage.interfaces.callback; package mage.interfaces.callback;
/** /**
* Server event type for processing on the client * Network: server event type for processing on the client
* *
* @author JayDi85 * @author JayDi85
*/ */

View file

@ -2,10 +2,6 @@ package mage.remote;
import mage.MageException; import mage.MageException;
import mage.cards.decks.DeckCardLists; import mage.cards.decks.DeckCardLists;
import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
import mage.cards.repository.ExpansionInfo;
import mage.cards.repository.ExpansionRepository;
import mage.constants.ManaType; import mage.constants.ManaType;
import mage.constants.PlayerAction; import mage.constants.PlayerAction;
import mage.game.GameException; import mage.game.GameException;
@ -18,7 +14,7 @@ import mage.interfaces.callback.ClientCallback;
import mage.players.PlayerType; import mage.players.PlayerType;
import mage.players.net.UserData; import mage.players.net.UserData;
import mage.utils.CompressUtil; import mage.utils.CompressUtil;
import mage.utils.ThreadUtils; import mage.util.ThreadUtils;
import mage.view.*; import mage.view.*;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.jboss.remoting.*; import org.jboss.remoting.*;
@ -44,25 +40,31 @@ import java.util.concurrent.TimeUnit;
*/ */
public class SessionImpl implements Session { public class SessionImpl implements Session {
private static final Logger logger = Logger.getLogger(SessionImpl.class);
public static final String ADMIN_NAME = "Admin"; // if you change here then change in User too
public static final String KEEP_MY_OLD_SESSION = "keep_my_old_session"; // for disconnects without active session lose (keep tables/games)
private enum SessionState { private enum SessionState {
DISCONNECTED, CONNECTED, CONNECTING, DISCONNECTING, SERVER_STARTING DISCONNECTED, CONNECTED, CONNECTING, DISCONNECTING, SERVER_STARTING
} }
private static final Logger logger = Logger.getLogger(SessionImpl.class);
private final MageClient client; private final MageClient client;
private String sessionId; private String sessionId = "";
private String restoreSessionId = "";
private MageServer server; private MageServer server;
private Client callbackClient;
private CallbackHandler callbackHandler; private Client callbackClient; // real connection with a server
private CallbackHandler callbackHandler; // processing commands from a server
private ServerState serverState; private ServerState serverState;
private SessionState sessionState = SessionState.DISCONNECTED; private SessionState sessionState = SessionState.DISCONNECTED;
private Connection connection; private Connection connection;
private RemotingTask lastRemotingTask = null; private RemotingTask lastRemotingTask = null; // single task for a server like connect, register, etc
private static final int PING_CYCLES = 10; private static final int PING_CYCLES = 10;
private final LinkedList<Long> pingTime = new LinkedList<>(); private final LinkedList<Long> pingTime = new LinkedList<>();
private String pingInfo = ""; private String lastPingInfo = "";
private static boolean debugMode = false; private static boolean debugMode = false;
private boolean canceled = false; private boolean canceled = false;
@ -82,6 +84,11 @@ public class SessionImpl implements Session {
return sessionId; return sessionId;
} }
@Override
public void setRestoreSessionId(String restoreSessionId) {
this.restoreSessionId = restoreSessionId;
}
// RemotingTask - do server side works in background and return result, can be canceled at any time // RemotingTask - do server side works in background and return result, can be canceled at any time
public abstract class RemotingTask { public abstract class RemotingTask {
@ -140,7 +147,7 @@ public class SessionImpl implements Session {
try { try {
Thread.sleep(3000); Thread.sleep(3000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
logger.fatal("waiting of error message had failed", e); logger.fatal("Server not responding, can't get error message from it", e);
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
} }
@ -182,7 +189,7 @@ public class SessionImpl implements Session {
showMessageToUser(addMessage + (ex.getMessage() != null ? ex.getMessage() : "")); showMessageToUser(addMessage + (ex.getMessage() != null ? ex.getMessage() : ""));
} catch (MageVersionException ex) { } catch (MageVersionException ex) {
logger.warn("Connect: wrong versions"); logger.warn("Connect: wrong versions");
connectStop(false); connectStop(false, false);
if (!canceled) { if (!canceled) {
showMessageToUser(ex.toString()); showMessageToUser(ex.toString());
} }
@ -193,14 +200,14 @@ public class SessionImpl implements Session {
} catch (Throwable t) { } catch (Throwable t) {
Throwable ex = ThreadUtils.findRootException(t); Throwable ex = ThreadUtils.findRootException(t);
logger.fatal("Connect: FAIL", t); logger.fatal("Connect: FAIL", t);
connectStop(false); connectStop(false, false);
if (!canceled) { if (!canceled) {
showMessageToUser(ex.toString()); showMessageToUser(ex.toString());
} }
} finally { } finally {
lastRemotingTask = null; lastRemotingTask = null;
if (closeConnectionOnFinish) { if (closeConnectionOnFinish) {
connectStop(false); // it's ok on mutiple calls connectStop(false, false); // it's ok on mutiple calls
} }
} }
return false; return false;
@ -256,7 +263,7 @@ public class SessionImpl implements Session {
if (connection.getAdminPassword() == null) { if (connection.getAdminPassword() == null) {
// for backward compatibility. don't remove twice call - first one does nothing but for version checking // for backward compatibility. don't remove twice call - first one does nothing but for version checking
result = server.connectUser(connection.getUsername(), connection.getPassword(), sessionId, client.getVersion(), connection.getUserIdStr()); result = server.connectUser(connection.getUsername(), connection.getPassword(), sessionId, restoreSessionId, client.getVersion(), connection.getUserIdStr());
} else { } else {
result = server.connectAdmin(connection.getAdminPassword(), sessionId, client.getVersion()); result = server.connectAdmin(connection.getAdminPassword(), sessionId, client.getVersion());
} }
@ -272,7 +279,7 @@ public class SessionImpl implements Session {
throw new MageVersionException(client.getVersion(), serverState.getVersion()); throw new MageVersionException(client.getVersion(), serverState.getVersion());
} }
if (!connection.getUsername().equals("Admin")) { if (!connection.getUsername().equals(ADMIN_NAME)) {
server.connectSetUserData(connection.getUsername(), sessionId, connection.getUserData(), client.getVersion().toString(), connection.getUserIdStr()); server.connectSetUserData(connection.getUsername(), sessionId, connection.getUserData(), client.getVersion().toString(), connection.getUserIdStr());
} }
@ -288,8 +295,8 @@ public class SessionImpl implements Session {
} }
@Override @Override
public Optional<String> getServerHostname() { public String getServerHost() {
return isConnected() ? Optional.of(connection.getHost()) : Optional.empty(); return isConnected() ? connection.getHost() : "";
} }
@Override @Override
@ -303,9 +310,13 @@ public class SessionImpl implements Session {
private boolean doRemoteConnection(final Connection connection) { private boolean doRemoteConnection(final Connection connection) {
// connect to server and setup all data, can be canceled // connect to server and setup all data, can be canceled
// it's anon connect without any user data (only version check)
// close current connection
if (isConnected()) { if (isConnected()) {
connectStop(true); connectStop(true, false);
} }
this.connection = connection; this.connection = connection;
this.canceled = false; this.canceled = false;
sessionState = SessionState.CONNECTING; sessionState = SessionState.CONNECTING;
@ -435,7 +446,7 @@ public class SessionImpl implements Session {
listenerMetadata.put(ConnectionValidator.VALIDATOR_PING_PERIOD, "15000"); listenerMetadata.put(ConnectionValidator.VALIDATOR_PING_PERIOD, "15000");
listenerMetadata.put(ConnectionValidator.VALIDATOR_PING_TIMEOUT, "13000"); listenerMetadata.put(ConnectionValidator.VALIDATOR_PING_TIMEOUT, "13000");
} }
callbackClient.connect(new ClientConnectionListener(), listenerMetadata); callbackClient.connect(new MageClientConnectionListener(), listenerMetadata);
Map<String, String> callbackMetadata = new HashMap<>(); Map<String, String> callbackMetadata = new HashMap<>();
callbackMetadata.put(Bisocket.IS_CALLBACK_SERVER, "true"); callbackMetadata.put(Bisocket.IS_CALLBACK_SERVER, "true");
@ -448,11 +459,11 @@ public class SessionImpl implements Session {
if (callbackConnectors.size() != 1) { if (callbackConnectors.size() != 1) {
logger.warn("There should be one callback Connector (number existing = " + callbackConnectors.size() + ')'); logger.warn("There should be one callback Connector (number existing = " + callbackConnectors.size() + ')');
} }
callbackClient.invoke(null); callbackClient.invoke(null);
sessionId = callbackClient.getSessionId(); sessionId = callbackClient.getSessionId();
sessionState = SessionState.CONNECTED; sessionState = SessionState.CONNECTED;
client.onNewConnection();
logger.info("Connect: DONE"); logger.info("Connect: DONE");
return true; return true;
} }
@ -468,7 +479,7 @@ public class SessionImpl implements Session {
if (result) { if (result) {
return true; return true;
} else { } else {
connectStop(false); connectStop(false, false);
return false; return false;
} }
} }
@ -507,11 +518,11 @@ public class SessionImpl implements Session {
} }
/** /**
* @param askForReconnect - true = connection was lost because of error and * @param askForReconnect - ask user to reconnect to server (e.g. on connection error)
* ask the user if they want to try to reconnect * @param keepMySessionActive - keep session active for app reconnect/restart, server will close it after few minutes timeout
*/ */
@Override @Override
public synchronized void connectStop(boolean askForReconnect) { public synchronized void connectStop(boolean askForReconnect, boolean keepMySessionActive) {
if (isConnected()) { if (isConnected()) {
logger.info("Disconnecting..."); logger.info("Disconnecting...");
sessionState = SessionState.DISCONNECTING; sessionState = SessionState.DISCONNECTING;
@ -522,24 +533,37 @@ public class SessionImpl implements Session {
try { try {
if (callbackClient != null && callbackClient.isConnected()) { if (callbackClient != null && callbackClient.isConnected()) {
if (keepMySessionActive) {
// hide real session from a server, so it will be active until timeout
callbackClient.setSessionId(KEEP_MY_OLD_SESSION);
}
callbackClient.removeListener(callbackHandler); callbackClient.removeListener(callbackHandler);
callbackClient.disconnect(); callbackClient.disconnect();
} }
TransporterClient.destroyTransporterClient(server);
} catch (Throwable ex) { } catch (Throwable ex) {
logger.fatal("Disconnecting FAIL", ex); logger.fatal("Disconnecting FAIL", ex);
} }
if (sessionState == SessionState.DISCONNECTING || sessionState == SessionState.CONNECTING) { if (sessionState == SessionState.DISCONNECTING || sessionState == SessionState.CONNECTING) {
// client side only, so no needs in server disconnection
sessionState = SessionState.DISCONNECTED; sessionState = SessionState.DISCONNECTED;
serverState = null; serverState = null;
logger.info("Disconnecting DONE"); logger.info("Disconnecting DONE");
if (askForReconnect) { if (askForReconnect) {
client.showError("Network error. You have been disconnected from " + connection.getHost()); client.showError("Network error. Can't connect to " + connection.getHost());
} }
client.disconnected(askForReconnect); // MageFrame with check to reconnect client.disconnected(askForReconnect); // MageFrame with check to reconnect
pingTime.clear(); pingTime.clear();
} }
// clean resources
if (server != null) {
TransporterClient.destroyTransporterClient(server);
server = null;
}
callbackClient = null;
callbackHandler = null;
serverState = null;
} }
@Override @Override
@ -565,7 +589,7 @@ public class SessionImpl implements Session {
@Override @Override
public void handleCallback(Callback callback) throws HandleCallbackException { public void handleCallback(Callback callback) throws HandleCallbackException {
try { try {
client.processCallback((ClientCallback) callback.getCallbackObject()); client.onCallback((ClientCallback) callback.getCallbackObject());
} catch (Exception ex) { } catch (Exception ex) {
logger.error("handleCallback error", ex); logger.error("handleCallback error", ex);
} }
@ -573,7 +597,10 @@ public class SessionImpl implements Session {
} }
} }
class ClientConnectionListener implements ConnectionListener { /**
* Network, client side: connection monitoring and error processing
*/
class MageClientConnectionListener implements ConnectionListener {
// http://docs.jboss.org/jbossremoting/2.5.3.SP1/html/chapter-connection-failure.html // http://docs.jboss.org/jbossremoting/2.5.3.SP1/html/chapter-connection-failure.html
@Override @Override
@ -1016,7 +1043,6 @@ public class SessionImpl implements Session {
@Override @Override
public boolean leaveChat(UUID chatId) { public boolean leaveChat(UUID chatId) {
// lock.readLock().lock();
try { try {
if (isConnected() && chatId != null) { if (isConnected() && chatId != null) {
server.chatLeave(chatId, sessionId); server.chatLeave(chatId, sessionId);
@ -1026,8 +1052,6 @@ public class SessionImpl implements Session {
handleMageException(ex); handleMageException(ex);
} catch (Throwable t) { } catch (Throwable t) {
handleThrowable(t); handleThrowable(t);
// } finally {
// lock.readLock().unlock();
} }
return false; return false;
} }
@ -1678,14 +1702,18 @@ public class SessionImpl implements Session {
} }
@Override @Override
public boolean ping() { public void ping() {
try { try {
// jboss uses lease mechanic for connection check but xmage needs additional data like pings stats
// ping must work after login only, all other actions are single call (example: register new user) // ping must work after login only, all other actions are single call (example: register new user)
// sessionId fills on connection // sessionId fills on connection
// serverState fills on good login // serverState fills on good login
if (isConnected() && sessionId != null && serverState != null) { if (!isConnected() || sessionId == null || serverState == null) {
return;
}
long startTime = System.nanoTime(); long startTime = System.nanoTime();
if (!server.ping(sessionId, pingInfo)) { if (!server.ping(sessionId, lastPingInfo)) {
logger.error("Ping failed: " + this.getUserName() + " Session: " + sessionId + " to MAGE server at " + connection.getHost() + ':' + connection.getPort()); logger.error("Ping failed: " + this.getUserName() + " Session: " + sessionId + " to MAGE server at " + connection.getHost() + ':' + connection.getPort());
throw new MageException("Ping failed"); throw new MageException("Ping failed");
} }
@ -1700,16 +1728,14 @@ public class SessionImpl implements Session {
sum += time; sum += time;
} }
milliSeconds = TimeUnit.MILLISECONDS.convert(sum / pingTime.size(), TimeUnit.NANOSECONDS); milliSeconds = TimeUnit.MILLISECONDS.convert(sum / pingTime.size(), TimeUnit.NANOSECONDS);
pingInfo = lastPing + " (avg: " + (milliSeconds > 0 ? milliSeconds + "ms" : "<1ms") + ')'; lastPingInfo = lastPing + " (avg: " + (milliSeconds > 0 ? milliSeconds + "ms" : "<1ms") + ')';
}
return true;
} catch (MageException ex) { } catch (MageException ex) {
handleMageException(ex); handleMageException(ex);
connectStop(true); connectStop(true, true);
} catch (Throwable t) { } catch (Throwable t) {
handleThrowable(t); handleThrowable(t);
connectStop(true, true);
} }
return false;
} }
@Override @Override

View file

@ -2,20 +2,20 @@ package mage.remote.interfaces;
import mage.remote.Connection; import mage.remote.Connection;
import java.util.Optional;
/** /**
* Network: client side commands for a server * Network: client side commands for a server
* *
* @author noxx * @author noxx, JayDi85
*/ */
public interface Connect { public interface Connect {
String getSessionId(); String getSessionId();
void setRestoreSessionId(String restoreSessionId);
String getLastError(); String getLastError();
Optional<String> getServerHostname(); String getServerHost();
boolean sendAuthRegister(Connection connection); boolean sendAuthRegister(Connection connection);
@ -27,11 +27,11 @@ public interface Connect {
boolean connectAbort(); boolean connectAbort();
void connectStop(boolean showMessage); void connectStop(boolean askForReconnect, boolean keepMySessionActive);
void connectReconnect(Throwable throwable); void connectReconnect(Throwable throwable);
boolean ping(); void ping();
boolean isConnected(); boolean isConnected();

View file

@ -28,7 +28,6 @@ import mage.util.CardUtil;
import mage.util.MultiAmountMessage; import mage.util.MultiAmountMessage;
import mage.util.RandomUtil; import mage.util.RandomUtil;
import javax.swing.*;
import java.io.File; import java.io.File;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.text.DateFormat; import java.text.DateFormat;
@ -902,28 +901,4 @@ public final class SystemUtil {
} }
return Arrays.asList(cardSet, cardName); return Arrays.asList(cardSet, cardName);
} }
public static void ensureRunInGameThread() {
String name = Thread.currentThread().getName();
if (!name.startsWith("GAME")) {
// how-to fix: use signal logic to inform a game about new command to execute instead direct execute (see example with WantConcede)
// reason: user responses/commands are received by network/call thread, but must be processed by game thread
throw new IllegalArgumentException("Wrong code usage: game related code must run in GAME thread, but it used in " + name, new Throwable());
}
}
public static void ensureRunInCallThread() {
String name = Thread.currentThread().getName();
if (!name.startsWith("CALL")) {
// how-to fix: something wrong in your code logic
throw new IllegalArgumentException("Wrong code usage: client commands code must run in CALL threads, but used in " + name, new Throwable());
}
}
public static void ensureRunInGUISwingThread() {
if (!SwingUtilities.isEventDispatchThread()) {
// hot-to fix: run GUI changeable code by SwingUtilities.invokeLater(() -> {xxx})
throw new IllegalArgumentException("Wrong code usage: GUI related code must run in SWING thread by SwingUtilities.invokeLater", new Throwable());
}
}
} }

View file

@ -17,6 +17,9 @@ public class SimpleCardsView extends LinkedHashMap<UUID, SimpleCardView> {
public SimpleCardsView() {} public SimpleCardsView() {}
public SimpleCardsView(Collection<Card> cards, boolean isGameObject) { public SimpleCardsView(Collection<Card> cards, boolean isGameObject) {
if (cards == null) {
return;
}
for (Card card: cards) { for (Card card: cards) {
this.put(card.getId(), new SimpleCardView(card.getId(), card.getExpansionSetCode(), card.getCardNumber(), card.getUsesVariousArt(), isGameObject)); this.put(card.getId(), new SimpleCardView(card.getId(), card.getExpansionSetCode(), card.getCardNumber(), card.getUsesVariousArt(), isGameObject));
} }

View file

@ -6,6 +6,8 @@ import java.io.Serializable;
import java.util.UUID; import java.util.UUID;
/** /**
* GUI: settings for message window that allows to choose additional action (can be used in game or outside)
*
* @author LevelX2 * @author LevelX2
*/ */
public class UserRequestMessage implements Serializable { public class UserRequestMessage implements Serializable {
@ -14,6 +16,7 @@ public class UserRequestMessage implements Serializable {
private final String title; private final String title;
private final String message; private final String message;
private double windowSizeRatio = 1.0; // increase default window size for big messages or buttons
private UUID relatedUserId; private UUID relatedUserId;
private String relatedUserName; private String relatedUserName;
private UUID matchId; private UUID matchId;
@ -39,6 +42,14 @@ public class UserRequestMessage implements Serializable {
this.button3Action = null; this.button3Action = null;
} }
public void setWindowSizeRatio(double windowSizeRatio) {
this.windowSizeRatio = windowSizeRatio;
}
public double getWindowSizeRatio() {
return this.windowSizeRatio;
}
public void setMatchId(UUID matchId) { public void setMatchId(UUID matchId) {
this.matchId = matchId; this.matchId = matchId;
} }

View file

@ -11,6 +11,7 @@ import java.util.concurrent.ExecutionException;
import javax.swing.*; import javax.swing.*;
import mage.remote.Connection; import mage.remote.Connection;
import mage.remote.Connection.ProxyType; import mage.remote.Connection.ProxyType;
import mage.remote.SessionImpl;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
/** /**
@ -20,6 +21,7 @@ import org.apache.log4j.Logger;
public class ConnectDialog extends JDialog { public class ConnectDialog extends JDialog {
private static final Logger logger = Logger.getLogger(ConnectDialog.class); private static final Logger logger = Logger.getLogger(ConnectDialog.class);
private ConsoleFrame console; private ConsoleFrame console;
private Connection connection; private Connection connection;
private ConnectTask task; private ConnectTask task;
@ -352,7 +354,7 @@ public class ConnectDialog extends JDialog {
connection.setHost(this.txtServer.getText()); connection.setHost(this.txtServer.getText());
connection.setPort(Integer.parseInt(this.txtPort.getText())); connection.setPort(Integer.parseInt(this.txtPort.getText()));
connection.setAdminPassword(new String(txtPassword.getPassword())); connection.setAdminPassword(new String(txtPassword.getPassword()));
connection.setUsername("Admin"); connection.setUsername(SessionImpl.ADMIN_NAME);
connection.setProxyType((ProxyType) this.cbProxyType.getSelectedItem()); connection.setProxyType((ProxyType) this.cbProxyType.getSelectedItem());
if (!this.cbProxyType.getSelectedItem().equals(ProxyType.NONE)) { if (!this.cbProxyType.getSelectedItem().equals(ProxyType.NONE)) {
connection.setProxyHost(this.txtProxyServer.getText()); connection.setProxyHost(this.txtProxyServer.getText());

View file

@ -154,10 +154,11 @@ public class ConsoleFrame extends javax.swing.JFrame implements MageClient {
}// </editor-fold>//GEN-END:initComponents }// </editor-fold>//GEN-END:initComponents
private void btnConnectActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnConnectActionPerformed private void btnConnectActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnConnectActionPerformed
if (session.isConnected()) { if (session.isConnected()) {
if (JOptionPane.showConfirmDialog(this, "Are you sure you want to disconnect?", "Confirm disconnect", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { if (JOptionPane.showConfirmDialog(this, "Are you sure you want to disconnect?", "Confirm disconnect", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
this.consolePanel1.stop(); this.consolePanel1.stop();
session.connectStop(false); session.connectStop(false, false);
} }
} else { } else {
connectDialog.showDialog(this); connectDialog.showDialog(this);
@ -243,7 +244,13 @@ public class ConsoleFrame extends javax.swing.JFrame implements MageClient {
} }
@Override @Override
public void processCallback(ClientCallback callback) { public void onNewConnection() {
// ignore
}
@Override
public void onCallback(ClientCallback callback) {
// ignore
} }
public void exitApp() { public void exitApp() {
@ -251,7 +258,7 @@ public class ConsoleFrame extends javax.swing.JFrame implements MageClient {
if (JOptionPane.showConfirmDialog(this, "You are currently connected. Are you sure you want to disconnect?", "Confirm disconnect", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) { if (JOptionPane.showConfirmDialog(this, "You are currently connected. Are you sure you want to disconnect?", "Confirm disconnect", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) {
return; return;
} }
session.connectStop(false); session.connectStop(false, false);
} else { } else {
if (JOptionPane.showConfirmDialog(this, "Are you sure you want to exit?", "Confirm exit", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) { if (JOptionPane.showConfirmDialog(this, "Are you sure you want to exit?", "Confirm exit", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) {
return; return;

View file

@ -265,7 +265,7 @@ public class HumanPlayer extends PlayerImpl {
* Prepare priority player for new feedback, call it for every choose cycle before waitForResponse * Prepare priority player for new feedback, call it for every choose cycle before waitForResponse
*/ */
protected void prepareForResponse(Game game) { protected void prepareForResponse(Game game) {
SystemUtil.ensureRunInGameThread(); ThreadUtils.ensureRunInGameThread();
// prepare priority player // prepare priority player
// on null - it's a discard in cleanaup and other non-user code, so don't change it here at that moment // on null - it's a discard in cleanaup and other non-user code, so don't change it here at that moment
@ -291,7 +291,7 @@ public class HumanPlayer extends PlayerImpl {
* @param game * @param game
*/ */
protected void waitForResponse(Game game) { protected void waitForResponse(Game game) {
SystemUtil.ensureRunInGameThread(); ThreadUtils.ensureRunInGameThread();
; ;
if (isExecutingMacro()) { if (isExecutingMacro()) {

View file

@ -4,7 +4,6 @@ import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository; import mage.cards.repository.CardRepository;
import mage.constants.Constants; import mage.constants.Constants;
import mage.game.Game; import mage.game.Game;
import mage.server.exceptions.UserNotFoundException;
import mage.server.game.GameController; import mage.server.game.GameController;
import mage.server.managers.ChatManager; import mage.server.managers.ChatManager;
import mage.server.managers.ManagerFactory; import mage.server.managers.ManagerFactory;
@ -56,19 +55,13 @@ public class ChatManagerImpl implements ChatManager {
} else { } else {
logger.trace("Chat to join not found - chatId: " + chatId + " userId: " + userId); logger.trace("Chat to join not found - chatId: " + chatId + " userId: " + userId);
} }
}
@Override
public void clearUserMessageStorage() {
lastUserMessages.clear();
} }
@Override @Override
public void leaveChat(UUID chatId, UUID userId) { public void leaveChat(UUID chatId, UUID userId) {
ChatSession chatSession = chatSessions.get(chatId); ChatSession chatSession = chatSessions.get(chatId);
if (chatSession != null && chatSession.hasUser(userId)) { if (chatSession != null && chatSession.hasUser(userId, false)) {
chatSession.kill(userId, DisconnectReason.CleaningUp); chatSession.disconnectUser(userId, DisconnectReason.DisconnectedByUser);
} }
} }
@ -321,41 +314,16 @@ public class ChatManagerImpl implements ChatManager {
return false; return false;
} }
/**
* use mainly for announcing that a user connection was lost or that a user
* has reconnected
*
* @param userId
* @param message
* @param color
* @throws mage.server.exceptions.UserNotFoundException
*/
@Override
public void broadcast(UUID userId, String message, MessageColor color) throws UserNotFoundException {
managerFactory.userManager().getUser(userId).ifPresent(user -> {
getChatSessions()
.stream()
.filter(chat -> chat.hasUser(userId))
.forEach(session -> session.broadcast(user.getName(), message, color, true, null, MessageType.TALK, null));
});
}
@Override @Override
public void sendReconnectMessage(UUID userId) { public void sendReconnectMessage(UUID userId) {
managerFactory.userManager().getUser(userId).ifPresent(user managerFactory.userManager().getUser(userId).ifPresent(user
-> getChatSessions() -> getChatSessions()
.stream() .stream()
.filter(chat -> chat.hasUser(userId)) .filter(chat -> chat.hasUser(userId, true))
.forEach(chatSession -> chatSession.broadcast(null, user.getName() + " has reconnected", MessageColor.BLUE, true, null, MessageType.STATUS, null))); .forEach(chatSession -> chatSession.broadcast(null, user.getName() + " has reconnected", MessageColor.BLUE, true, null, MessageType.STATUS, null)));
} }
@Override
public void sendLostConnectionMessage(UUID userId, DisconnectReason reason) {
managerFactory.userManager().getUser(userId).ifPresent(user -> sendMessageToUserChats(userId, user.getName() + " " + reason.getMessage()));
}
/** /**
* Send message to all active waiting/tourney/game chats (but not in main lobby) * Send message to all active waiting/tourney/game chats (but not in main lobby)
* *
@ -367,11 +335,11 @@ public class ChatManagerImpl implements ChatManager {
managerFactory.userManager().getUser(userId).ifPresent(user -> { managerFactory.userManager().getUser(userId).ifPresent(user -> {
List<ChatSession> chatSessions = getChatSessions().stream() List<ChatSession> chatSessions = getChatSessions().stream()
.filter(chat -> !chat.getChatId().equals(managerFactory.gamesRoomManager().getMainChatId())) // ignore main lobby .filter(chat -> !chat.getChatId().equals(managerFactory.gamesRoomManager().getMainChatId())) // ignore main lobby
.filter(chat -> chat.hasUser(userId)) .filter(chat -> chat.hasUser(userId, true))
.collect(Collectors.toList()); .collect(Collectors.toList());
if (chatSessions.size() > 0) { if (chatSessions.size() > 0) {
logger.info("INFORM OPPONENTS by " + user.getName() + ": " + message); logger.debug("INFORM OPPONENTS by " + user.getName() + ": " + message);
chatSessions.forEach(chatSession -> chatSession.broadcast(null, message, MessageColor.BLUE, true, null, MessageType.STATUS, null)); chatSessions.forEach(chatSession -> chatSession.broadcast(null, message, MessageColor.BLUE, true, null, MessageType.STATUS, null));
} }
}); });
@ -380,8 +348,8 @@ public class ChatManagerImpl implements ChatManager {
@Override @Override
public void removeUser(UUID userId, DisconnectReason reason) { public void removeUser(UUID userId, DisconnectReason reason) {
for (ChatSession chatSession : getChatSessions()) { for (ChatSession chatSession : getChatSessions()) {
if (chatSession.hasUser(userId)) { if (chatSession.hasUser(userId, false)) {
chatSession.kill(userId, reason); chatSession.disconnectUser(userId, reason);
} }
} }
} }
@ -397,4 +365,25 @@ public class ChatManagerImpl implements ChatManager {
} }
} }
private void clearUserMessageStorage() {
lastUserMessages.clear();
}
@Override
public void checkHealth() {
//logger.info("cheching chats...");
// TODO: add broken chats check and report (with non existing userId)
/*
logger.info("total chats: " + chatSessions.size());
chatSessions.values().forEach(c -> {
logger.info("chat " + c.getInfo() + ", " + c.getChatId());
c.getClients().forEach((userId, userName) -> {
logger.info(" - " + userName + ", " + userId);
});
});
*/
clearUserMessageStorage();
}
} }

View file

@ -10,7 +10,6 @@ import mage.view.ChatMessage.MessageType;
import mage.view.ChatMessage.SoundToPlay; import mage.view.ChatMessage.SoundToPlay;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.text.DateFormat;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
@ -19,89 +18,86 @@ import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com, JayDi85
*/ */
public class ChatSession { public class ChatSession {
private static final Logger logger = Logger.getLogger(ChatSession.class); private static final Logger logger = Logger.getLogger(ChatSession.class);
private static final DateFormat timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT);
private final ManagerFactory managerFactory; private final ManagerFactory managerFactory;
private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final ReadWriteLock lock = new ReentrantReadWriteLock(); // TODO: no needs due ConcurrentHashMap usage?
private final ConcurrentMap<UUID, String> clients = new ConcurrentHashMap<>(); 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; private final UUID chatId;
private final Date createTime; private final Date createTime;
private final String info; private final String info;
public ChatSession(ManagerFactory managerFactory, String info) { public ChatSession(ManagerFactory managerFactory, String info) {
this.managerFactory = managerFactory; this.managerFactory = managerFactory;
chatId = UUID.randomUUID(); this.chatId = UUID.randomUUID();
this.createTime = new Date(); this.createTime = new Date();
this.info = info; this.info = info;
} }
public void join(UUID userId) { public void join(UUID userId) {
managerFactory.userManager().getUser(userId).ifPresent(user -> { managerFactory.userManager().getUser(userId).ifPresent(user -> {
if (!clients.containsKey(userId)) { if (!users.containsKey(userId)) {
String userName = user.getName(); String userName = user.getName();
final Lock w = lock.writeLock(); final Lock w = lock.writeLock();
w.lock(); w.lock();
try { try {
clients.put(userId, userName); users.put(userId, userName);
usersHistory.add(userId);
} finally { } finally {
w.unlock(); w.unlock();
} }
broadcast(null, userName + " has joined (" + user.getClientVersion() + ')', MessageColor.BLUE, true, null, MessageType.STATUS, null); broadcast(null, userName + " has joined", MessageColor.BLUE, true, null, MessageType.STATUS, null);
logger.trace(userName + " joined chat " + chatId);
} }
}); });
} }
public void kill(UUID userId, DisconnectReason reason) { public void disconnectUser(UUID userId, DisconnectReason reason) {
// user will reconnect to all chats, so no needs to keep it
// if you kill session then kill all chats too
try { try {
if (reason == null) { String userName = users.getOrDefault(userId, null);
logger.fatal("User kill without disconnect reason userId: " + userId); if (userName == null) {
reason = DisconnectReason.Undefined; return;
} }
if (userId != null && clients.containsKey(userId)) {
String userName = clients.get(userId); // remove from chat
if (reason != DisconnectReason.LostConnection) { // for lost connection the user will be reconnected or session expire so no removeUserFromAllTablesAndChat of chat yet
final Lock w = lock.writeLock(); final Lock w = lock.writeLock();
w.lock(); w.lock();
try { try {
clients.remove(userId); users.remove(userId);
} finally { } finally {
w.unlock(); w.unlock();
} }
logger.debug(userName + '(' + reason.toString() + ')' + " removed from chatId " + chatId); logger.debug(userName + " (" + reason + ')' + " removed from chatId " + chatId);
}
String message = reason.getMessage();
if (!message.isEmpty()) { // inform other users about disconnect (lobby, system tab)
broadcast(null, userName + message, MessageColor.BLUE, true, null, MessageType.STATUS, null); if (!reason.messageForUser.isEmpty()) {
broadcast(null, userName + reason.messageForUser, MessageColor.BLUE, true, null, MessageType.STATUS, null);
} }
} } catch (Exception e) {
} catch (Exception ex) { logger.fatal("Chat: disconnecting user catch error: " + e, e);
logger.fatal("exception: " + ex.toString());
} }
} }
public boolean broadcastInfoToUser(User toUser, String message) { public void broadcastInfoToUser(User toUser, String message) {
if (clients.containsKey(toUser.getId())) { if (users.containsKey(toUser.getId())) {
toUser.fireCallback(new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId, toUser.fireCallback(new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId,
new ChatMessage(null, message, new Date(), null, MessageColor.BLUE, MessageType.USER_INFO, null))); new ChatMessage(null, message, new Date(), null, MessageColor.BLUE, MessageType.USER_INFO, null)));
return true;
} }
return false;
} }
public boolean broadcastWhisperToUser(User fromUser, User toUser, String message) { public boolean broadcastWhisperToUser(User fromUser, User toUser, String message) {
if (clients.containsKey(toUser.getId())) { if (users.containsKey(toUser.getId())) {
toUser.fireCallback(new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId, toUser.fireCallback(new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId,
new ChatMessage(fromUser.getName(), message, new Date(), null, MessageColor.YELLOW, MessageType.WHISPER_FROM, SoundToPlay.PlayerWhispered))); new ChatMessage(fromUser.getName(), message, new Date(), null, MessageColor.YELLOW, MessageType.WHISPER_FROM, SoundToPlay.PlayerWhispered)));
if (clients.containsKey(fromUser.getId())) { if (users.containsKey(fromUser.getId())) {
fromUser.fireCallback(new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId, fromUser.fireCallback(new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId,
new ChatMessage(toUser.getName(), message, new Date(), null, MessageColor.YELLOW, MessageType.WHISPER_TO, null))); new ChatMessage(toUser.getName(), message, new Date(), null, MessageColor.YELLOW, MessageType.WHISPER_TO, null)));
return true; return true;
@ -111,6 +107,10 @@ public class ChatSession {
} }
public void broadcast(String userName, String message, MessageColor color, boolean withTime, Game game, MessageType messageType, SoundToPlay soundToPlay) { public void broadcast(String userName, String message, MessageColor color, boolean withTime, Game game, MessageType messageType, SoundToPlay soundToPlay) {
// TODO: is it called by single thread for all users?
// TODO: is it freeze on someone's connection fail/freeze?
// 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()) { if (!message.isEmpty()) {
Set<UUID> clientsToRemove = new HashSet<>(); Set<UUID> clientsToRemove = new HashSet<>();
ClientCallback clientCallback = new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId, ClientCallback clientCallback = new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId,
@ -119,7 +119,7 @@ public class ChatSession {
final Lock r = lock.readLock(); final Lock r = lock.readLock();
r.lock(); r.lock();
try { try {
chatUserIds.addAll(clients.keySet()); chatUserIds.addAll(users.keySet());
} finally { } finally {
r.unlock(); r.unlock();
} }
@ -135,12 +135,11 @@ public class ChatSession {
final Lock w = lock.readLock(); final Lock w = lock.readLock();
w.lock(); w.lock();
try { try {
clients.keySet().removeAll(clientsToRemove); users.keySet().removeAll(clientsToRemove);
} finally { } finally {
w.unlock(); w.unlock();
} }
} }
} }
} }
@ -151,12 +150,12 @@ public class ChatSession {
return chatId; return chatId;
} }
public boolean hasUser(UUID userId) { public boolean hasUser(UUID userId, boolean useHistory) {
return clients.containsKey(userId); return useHistory ? usersHistory.contains(userId) : users.containsKey(userId);
} }
public ConcurrentMap<UUID, String> getClients() { public ConcurrentMap<UUID, String> getUsers() {
return clients; return users;
} }
public Date getCreateTime() { public Date getCreateTime() {

View file

@ -1,26 +1,24 @@
package mage.server; package mage.server;
/** /**
* Server: possible reasons for disconnection
* *
* @author LevelX2 * @author LevelX2, JayDi85
*/ */
public enum DisconnectReason { public enum DisconnectReason {
LostConnection(" has lost connection"), LostConnection(false, " has lost connection"),
BecameInactive(" has become inactive"), DisconnectedByUser(true, " has left XMage"),
Disconnected(" has left XMage"), DisconnectedByUserButKeepTables(false, " has left XMage for app restart/reconnect"),
CleaningUp(" [cleaning up]"), DisconnectedByAdmin(true, " was disconnected by admin"),
ConnectingOtherInstance(" reconnected and replaced still active old session"), AnotherUserInstance(false, " disconnected by another user intance"),
AdminDisconnect(" was disconnected by the admin"), AnotherUserInstanceSilent(false, ""), // same user, no need inform in chats
SessionExpired(" session expired"), SessionExpired(true, " session expired");
Undefined("");
String message; final boolean isRemoveUserTables;
final String messageForUser;
DisconnectReason(String message) { DisconnectReason(boolean isRemoveUserTables, String messageForUser) {
this.message = message; this.isRemoveUserTables = isRemoveUserTables;
} this.messageForUser = messageForUser;
public String getMessage() {
return message;
} }
} }

View file

@ -3,9 +3,7 @@ package mage.server;
import mage.MageException; import mage.MageException;
import mage.cards.decks.DeckCardLists; import mage.cards.decks.DeckCardLists;
import mage.cards.decks.DeckValidatorFactory; import mage.cards.decks.DeckValidatorFactory;
import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository; import mage.cards.repository.CardRepository;
import mage.cards.repository.ExpansionInfo;
import mage.cards.repository.ExpansionRepository; import mage.cards.repository.ExpansionRepository;
import mage.constants.Constants; import mage.constants.Constants;
import mage.constants.ManaType; import mage.constants.ManaType;
@ -55,6 +53,7 @@ public class MageServerImpl implements MageServer {
private final ManagerFactory managerFactory; private final ManagerFactory managerFactory;
private final String adminPassword; private final String adminPassword;
private final boolean testMode; private final boolean testMode;
private final boolean detailsMode;
private final LinkedHashMap<String, String> activeAuthTokens = new LinkedHashMap<String, String>() { private final LinkedHashMap<String, String> activeAuthTokens = new LinkedHashMap<String, String>() {
@Override @Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) { protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
@ -63,10 +62,11 @@ public class MageServerImpl implements MageServer {
} }
}; };
public MageServerImpl(ManagerFactory managerFactory, String adminPassword, boolean testMode) { public MageServerImpl(ManagerFactory managerFactory, String adminPassword, boolean testMode, boolean detailsMode) {
this.managerFactory = managerFactory; this.managerFactory = managerFactory;
this.adminPassword = adminPassword; this.adminPassword = adminPassword;
this.testMode = testMode; this.testMode = testMode;
this.detailsMode = detailsMode;
this.callExecutor = managerFactory.threadExecutor().getCallExecutor(); this.callExecutor = managerFactory.threadExecutor().getCallExecutor();
ServerMessagesUtil.instance.getMessages(); ServerMessagesUtil.instance.getMessages();
} }
@ -146,18 +146,21 @@ public class MageServerImpl implements MageServer {
} }
@Override @Override
public boolean connectUser(String userName, String password, String sessionId, MageVersion version, String userIdStr) throws MageException { public boolean connectUser(String userName, String password, String sessionId, String restoreSessionId, MageVersion version, String userIdStr) throws MageException {
try { try {
if (version.compareTo(Main.getVersion()) != 0) { if (version.compareTo(Main.getVersion()) != 0) {
logger.info("MageVersionException: userName=" + userName + ", version=" + version + " sessionId=" + sessionId); logger.debug("MageVersionException: userName=" + userName + ", version=" + version + " sessionId=" + sessionId);
throw new MageVersionException(version, Main.getVersion()); throw new MageVersionException(version, Main.getVersion());
} }
return managerFactory.sessionManager().connectUser(sessionId, userName, password, userIdStr); return managerFactory.sessionManager().connectUser(sessionId, restoreSessionId, userName, password, userIdStr, this.detailsMode);
} catch (MageException ex) { } catch (MageException e) {
if (ex instanceof MageVersionException) { if (e instanceof MageVersionException) {
throw ex; // return wrong version error as is
throw e;
} else {
// all other errors must be logged and hidden under a server error
handleException(e);
} }
handleException(ex);
} }
return false; return false;
} }
@ -508,7 +511,6 @@ public class MageServerImpl implements MageServer {
public void chatJoin(final UUID chatId, final String sessionId, final String userName) throws MageException { public void chatJoin(final UUID chatId, final String sessionId, final String userName) throws MageException {
execute("joinChat", sessionId, () -> { execute("joinChat", sessionId, () -> {
managerFactory.sessionManager().getSession(sessionId).ifPresent(session -> { managerFactory.sessionManager().getSession(sessionId).ifPresent(session -> {
UUID userId = session.getUserId(); UUID userId = session.getUserId();
managerFactory.chatManager().joinChat(chatId, userId); managerFactory.chatManager().joinChat(chatId, userId);
}); });
@ -1025,7 +1027,7 @@ public class MageServerImpl implements MageServer {
@Override @Override
public void adminDisconnectUser(final String sessionId, final String userSessionId) throws MageException { public void adminDisconnectUser(final String sessionId, final String userSessionId) throws MageException {
execute("adminDisconnectUser", sessionId, execute("adminDisconnectUser", sessionId,
() -> managerFactory.sessionManager().disconnectUser(sessionId, userSessionId), () -> managerFactory.sessionManager().disconnectAnother(sessionId, userSessionId),
true true
); );
} }
@ -1049,7 +1051,7 @@ public class MageServerImpl implements MageServer {
user.showUserMessage("Admin info", "Your user profile was locked until " + SystemUtil.dateFormat.format(lockUntil) + '.'); user.showUserMessage("Admin info", "Your user profile was locked until " + SystemUtil.dateFormat.format(lockUntil) + '.');
user.setLockedUntil(lockUntil); user.setLockedUntil(lockUntil);
if (user.isConnected()) { if (user.isConnected()) {
managerFactory.sessionManager().disconnectUser(sessionId, user.getSessionId()); managerFactory.sessionManager().disconnectAnother(sessionId, user.getSessionId());
} }
}); });
}, true); }, true);
@ -1064,7 +1066,7 @@ public class MageServerImpl implements MageServer {
User user = u.get(); User user = u.get();
user.setActive(active); user.setActive(active);
if (!user.isActive() && user.isConnected()) { if (!user.isActive() && user.isConnected()) {
managerFactory.sessionManager().disconnectUser(sessionId, user.getSessionId()); managerFactory.sessionManager().disconnectAnother(sessionId, user.getSessionId());
} }
} else if (authorizedUser != null) { } else if (authorizedUser != null) {
User theUser = new User(managerFactory, userName, "localhost", authorizedUser); User theUser = new User(managerFactory, userName, "localhost", authorizedUser);
@ -1078,7 +1080,7 @@ public class MageServerImpl implements MageServer {
execute("adminToggleActivateUser", sessionId, () -> managerFactory.userManager().getUserByName(userName).ifPresent(user -> { execute("adminToggleActivateUser", sessionId, () -> managerFactory.userManager().getUserByName(userName).ifPresent(user -> {
user.setActive(!user.isActive()); user.setActive(!user.isActive());
if (!user.isActive() && user.isConnected()) { if (!user.isActive() && user.isConnected()) {
managerFactory.sessionManager().disconnectUser(sessionId, user.getSessionId()); managerFactory.sessionManager().disconnectAnother(sessionId, user.getSessionId());
} }
}), true); }), true);
} }
@ -1086,7 +1088,7 @@ public class MageServerImpl implements MageServer {
@Override @Override
public void adminEndUserSession(final String sessionId, final String userSessionId) throws MageException { public void adminEndUserSession(final String sessionId, final String userSessionId) throws MageException {
execute("adminEndUserSession", sessionId, execute("adminEndUserSession", sessionId,
() -> managerFactory.sessionManager().endUserSession(sessionId, userSessionId), () -> managerFactory.sessionManager().disconnectAnother(sessionId, userSessionId),
true true
); );
} }

View file

@ -1,16 +1,17 @@
package mage.server; package mage.server;
import mage.cards.ExpansionSet; import mage.cards.ExpansionSet;
import mage.cards.RateCard;
import mage.cards.Sets; import mage.cards.Sets;
import mage.cards.decks.DeckValidatorFactory; import mage.cards.decks.DeckValidatorFactory;
import mage.cards.repository.CardScanner; import mage.cards.repository.CardScanner;
import mage.cards.repository.PluginClassloaderRegistery; import mage.cards.repository.PluginClassloaderRegistery;
import mage.cards.repository.RepositoryUtil; import mage.cards.repository.RepositoryUtil;
import mage.cards.RateCard;
import mage.game.match.MatchType; import mage.game.match.MatchType;
import mage.game.tournament.TournamentType; import mage.game.tournament.TournamentType;
import mage.interfaces.MageServer; import mage.interfaces.MageServer;
import mage.remote.Connection; import mage.remote.Connection;
import mage.remote.SessionImpl;
import mage.server.draft.CubeFactory; import mage.server.draft.CubeFactory;
import mage.server.game.GameFactory; import mage.server.game.GameFactory;
import mage.server.game.PlayerFactory; import mage.server.game.PlayerFactory;
@ -18,7 +19,10 @@ import mage.server.managers.ConfigSettings;
import mage.server.managers.ManagerFactory; import mage.server.managers.ManagerFactory;
import mage.server.record.UserStatsRepository; import mage.server.record.UserStatsRepository;
import mage.server.tournament.TournamentFactory; import mage.server.tournament.TournamentFactory;
import mage.server.util.*; import mage.server.util.ConfigFactory;
import mage.server.util.ConfigWrapper;
import mage.server.util.PluginClassLoader;
import mage.server.util.ServerMessagesUtil;
import mage.server.util.config.GamePlugin; import mage.server.util.config.GamePlugin;
import mage.server.util.config.Plugin; import mage.server.util.config.Plugin;
import mage.utils.MageVersion; import mage.utils.MageVersion;
@ -44,7 +48,7 @@ import java.nio.file.Paths;
import java.util.*; import java.util.*;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com, JayDi85
*/ */
public final class Main { public final class Main {
@ -56,6 +60,8 @@ public final class Main {
// priority: default setting -> prop setting -> arg setting // priority: default setting -> prop setting -> arg setting
private static final String testModeArg = "-testMode="; private static final String testModeArg = "-testMode=";
private static final String testModeProp = "xmage.testMode"; private static final String testModeProp = "xmage.testMode";
private static final String detailsModeArg = "-detailsMode=";
private static final String detailsModeProp = "xmage.detailsMode";
private static final String adminPasswordArg = "-adminPassword="; private static final String adminPasswordArg = "-adminPassword=";
private static final String adminPasswordProp = "xmage.adminPassword"; private static final String adminPasswordProp = "xmage.adminPassword";
private static final String configPathProp = "xmage.config.path"; private static final String configPathProp = "xmage.config.path";
@ -75,6 +81,7 @@ public final class Main {
// - simplified registration and login (no password check); // - simplified registration and login (no password check);
// - debug main menu for GUI and rendering testing (must use -debug arg for client app); // - debug main menu for GUI and rendering testing (must use -debug arg for client app);
private static boolean testMode; private static boolean testMode;
private static boolean detailsMode;
public static void main(String[] args) { public static void main(String[] args) {
System.setProperty("java.util.Arrays.useLegacyMergeSort", "true"); System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");
@ -85,6 +92,7 @@ public final class Main {
// enable test mode by default for developer build (if you run it from source code) // enable test mode by default for developer build (if you run it from source code)
testMode |= version.isDeveloperBuild(); testMode |= version.isDeveloperBuild();
detailsMode = false;
// settings by properties (-Dxxx=yyy from a launcher) // settings by properties (-Dxxx=yyy from a launcher)
if (System.getProperty(testModeProp) != null) { if (System.getProperty(testModeProp) != null) {
@ -93,6 +101,9 @@ public final class Main {
if (System.getProperty(adminPasswordProp) != null) { if (System.getProperty(adminPasswordProp) != null) {
adminPassword = SystemUtil.sanitize(System.getProperty(adminPasswordProp)); adminPassword = SystemUtil.sanitize(System.getProperty(adminPasswordProp));
} }
if (System.getProperty(detailsModeProp) != null) {
detailsMode = Boolean.parseBoolean(System.getProperty(detailsModeProp));
}
final String configPath; final String configPath;
if (System.getProperty(configPathProp) != null) { if (System.getProperty(configPathProp) != null) {
configPath = System.getProperty(configPathProp); configPath = System.getProperty(configPathProp);
@ -108,6 +119,9 @@ public final class Main {
adminPassword = arg.replace(adminPasswordArg, ""); adminPassword = arg.replace(adminPasswordArg, "");
adminPassword = SystemUtil.sanitize(adminPassword); adminPassword = SystemUtil.sanitize(adminPassword);
} }
if (arg.startsWith(detailsModeArg)) {
detailsMode = Boolean.parseBoolean(arg.replace(detailsModeArg, ""));
}
} }
logger.info(String.format("Reading configuration from path=%s", configPath)); logger.info(String.format("Reading configuration from path=%s", configPath));
@ -234,7 +248,8 @@ public final class Main {
logger.info("Config - max pool size : " + config.getMaxPoolSize()); logger.info("Config - max pool size : " + config.getMaxPoolSize());
logger.info("Config - num accp.threads: " + config.getNumAcceptThreads()); logger.info("Config - num accp.threads: " + config.getNumAcceptThreads());
logger.info("Config - second.bind port: " + config.getSecondaryBindPort()); logger.info("Config - second.bind port: " + config.getSecondaryBindPort());
logger.info("Config - auth. activated : " + (config.isAuthenticationActivated() ? "true" : "false")); logger.info("Config - users registration: " + (config.isAuthenticationActivated() ? "true" : "false"));
logger.info("Config - users anon: " + (!config.isAuthenticationActivated() ? "true" : "false"));
logger.info("Config - mailgun api key : " + config.getMailgunApiKey()); logger.info("Config - mailgun api key : " + config.getMailgunApiKey());
logger.info("Config - mailgun domain : " + config.getMailgunDomain()); logger.info("Config - mailgun domain : " + config.getMailgunDomain());
logger.info("Config - mail smtp Host : " + config.getMailSmtpHost()); logger.info("Config - mail smtp Host : " + config.getMailSmtpHost());
@ -261,7 +276,13 @@ public final class Main {
// Parameter: serializationtype => jboss // Parameter: serializationtype => jboss
InvokerLocator serverLocator = new InvokerLocator(connection.getURI()); InvokerLocator serverLocator = new InvokerLocator(connection.getURI());
if (!isAlreadyRunning(config, serverLocator)) { if (!isAlreadyRunning(config, serverLocator)) {
server = new MageTransporterServer(managerFactory, serverLocator, new MageServerImpl(managerFactory, adminPassword, testMode), MageServer.class.getName(), new MageServerInvocationHandler(managerFactory)); server = new MageTransporterServer(
managerFactory,
serverLocator,
new MageServerImpl(managerFactory, adminPassword, testMode, detailsMode),
MageServer.class.getName(),
new MageServerInvocationHandler(managerFactory)
);
server.start(); server.start();
logger.info("Started MAGE server - listening on " + connection.toString()); logger.info("Started MAGE server - listening on " + connection.toString());
@ -297,67 +318,75 @@ public final class Main {
return false; return false;
} }
static class ClientConnectionListener implements ConnectionListener { /**
* Network, server side: connection monitoring and error processing
*/
static class MageServerConnectionListener implements ConnectionListener {
private final ManagerFactory managerFactory; private final ManagerFactory managerFactory;
public ClientConnectionListener(ManagerFactory managerFactory) { public MageServerConnectionListener(ManagerFactory managerFactory) {
this.managerFactory = managerFactory; this.managerFactory = managerFactory;
} }
@Override @Override
public void handleConnectionException(Throwable throwable, Client client) { public void handleConnectionException(Throwable throwable, Client client) {
String sessionId = client.getSessionId(); String sessionId = client.getSessionId();
Optional<Session> session = managerFactory.sessionManager().getSession(sessionId); Session session = managerFactory.sessionManager().getSession(sessionId).orElse(null);
if (!session.isPresent()) { if (session == null) {
logger.trace("Session not found : " + sessionId); logger.debug("Connection error, session not found : " + sessionId + " - " + throwable);
} else { return;
UUID userId = session.get().getUserId(); }
// fill by user's info
StringBuilder sessionInfo = new StringBuilder(); StringBuilder sessionInfo = new StringBuilder();
Optional<User> user = managerFactory.userManager().getUser(userId); User user = managerFactory.userManager().getUser(session.getUserId()).orElse(null);
if (user.isPresent()) { if (user != null) {
sessionInfo.append(user.get().getName()).append(" [").append(user.get().getGameInfo()).append(']'); sessionInfo.append(user.getName()).append(" [").append(user.getGameInfo()).append(']');
} else { } else {
sessionInfo.append("[user missing] "); sessionInfo.append("[no user]");
} }
sessionInfo.append(" at ").append(session.get().getHost()).append(" sessionId: ").append(session.get().getId()); sessionInfo.append(" at ").append(session.getHost()).append(", sessionId: ").append(session.getId());
// check disconnection reason
// lease ping is inner jboss feature to check connection status
// xmage ping is app's feature to check connection and send additional data like ping statistics
if (throwable instanceof ClientDisconnectedException) { if (throwable instanceof ClientDisconnectedException) {
// Seems like the random diconnects from public server land here and should not be handled as explicit disconnects // client called a disconnect command (full disconnect without tables keep)
// So it should be possible to reconnect to server and continue games if DisconnectReason is set to LostConnection // no need to keep session
//managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.Disconnected);
managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.LostConnection);
logger.info("CLIENT DISCONNECTED - " + sessionInfo); logger.info("CLIENT DISCONNECTED - " + sessionInfo);
logger.debug("Stack Trace", throwable); logger.debug("- cause: client called disconnect command");
} else { managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.DisconnectedByUser, true);
managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.LostConnection); } else if (throwable == null) {
// lease timeout (ping), so server lost connection with a client
// must keep tables
logger.info("LOST CONNECTION - " + sessionInfo); logger.info("LOST CONNECTION - " + sessionInfo);
if (logger.isDebugEnabled()) { logger.debug("- cause: lease expired");
if (throwable == null) { managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.LostConnection, true);
logger.debug("- cause: Lease expired");
} else { } else {
logger.debug(" - cause: " + Session.getBasicCause(throwable).toString()); // unknown error
// must keep tables
logger.info("LOST CONNECTION - " + sessionInfo);
logger.debug("- cause: unknown error - " + throwable);
managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.LostConnection, true);
} }
} }
} }
} /**
* Network, server side: data transport layer
} */
}
static class MageTransporterServer extends TransporterServer { static class MageTransporterServer extends TransporterServer {
protected Connector connector; protected Connector connector;
public MageTransporterServer(ManagerFactory managerFactory, InvokerLocator locator, Object target, String subsystem, MageServerInvocationHandler serverInvocationHandler) throws Exception { public MageTransporterServer(ManagerFactory managerFactory, InvokerLocator locator, Object target, String subsystem, MageServerInvocationHandler serverInvocationHandler) throws Exception {
super(locator, target, subsystem); super(locator, target, subsystem);
connector.addInvocationHandler("callback", serverInvocationHandler); connector.addInvocationHandler("callback", serverInvocationHandler); // commands processing
connector.setLeasePeriod(managerFactory.configSettings().getLeasePeriod());
connector.addConnectionListener(new ClientConnectionListener(managerFactory));
}
public Connector getConnector() throws Exception { // connection monitoring and errors processing
return connector; connector.setLeasePeriod(managerFactory.configSettings().getLeasePeriod());
connector.addConnectionListener(new MageServerConnectionListener(managerFactory));
} }
@Override @Override
@ -368,6 +397,9 @@ public final class Main {
} }
} }
/**
* Network, server side: commands layer for execute (creates one thread per each client connection)
*/
static class MageServerInvocationHandler implements ServerInvocationHandler { static class MageServerInvocationHandler implements ServerInvocationHandler {
private final ManagerFactory managerFactory; private final ManagerFactory managerFactory;
@ -396,6 +428,7 @@ public final class Main {
@Override @Override
public void setInvoker(ServerInvoker invoker) { public void setInvoker(ServerInvoker invoker) {
// connection settings
((BisocketServerInvoker) invoker).setSecondaryBindPort(managerFactory.configSettings().getSecondaryBindPort()); ((BisocketServerInvoker) invoker).setSecondaryBindPort(managerFactory.configSettings().getSecondaryBindPort());
((BisocketServerInvoker) invoker).setBacklog(managerFactory.configSettings().getBacklogSize()); ((BisocketServerInvoker) invoker).setBacklog(managerFactory.configSettings().getBacklogSize());
((BisocketServerInvoker) invoker).setNumAcceptThreads(managerFactory.configSettings().getNumAcceptThreads()); ((BisocketServerInvoker) invoker).setNumAcceptThreads(managerFactory.configSettings().getNumAcceptThreads());
@ -403,7 +436,10 @@ public final class Main {
@Override @Override
public void addListener(InvokerCallbackHandler callbackHandler) { public void addListener(InvokerCallbackHandler callbackHandler) {
// Called for every client connecting to the server // on client connect: remember client session
// TODO: add ban by IP here?
// need creates on first call, required by ping
// but can be removed and re-creates on reconnection (if server contains another user instance)
ServerInvokerCallbackHandler handler = (ServerInvokerCallbackHandler) callbackHandler; ServerInvokerCallbackHandler handler = (ServerInvokerCallbackHandler) callbackHandler;
try { try {
String sessionId = handler.getClientSessionId(); String sessionId = handler.getClientSessionId();
@ -415,8 +451,9 @@ public final class Main {
@Override @Override
public Object invoke(final InvocationRequest invocation) throws Throwable { public Object invoke(final InvocationRequest invocation) throws Throwable {
// on command:
// called for every client connecting to the server (after add Listener) // called for every client connecting to the server (after add Listener)
// TODO: add ban by IP here?
// save client ip-address // save client ip-address
String sessionId = invocation.getSessionId(); String sessionId = invocation.getSessionId();
Map map = invocation.getRequestPayload(); Map map = invocation.getRequestPayload();
@ -438,11 +475,17 @@ public final class Main {
@Override @Override
public void removeListener(InvokerCallbackHandler callbackHandler) { public void removeListener(InvokerCallbackHandler callbackHandler) {
// on client disconnect: remember client session
// if user want to keep session then sessionId will be faked here
ServerInvokerCallbackHandler handler = (ServerInvokerCallbackHandler) callbackHandler; ServerInvokerCallbackHandler handler = (ServerInvokerCallbackHandler) callbackHandler;
String sessionId = handler.getClientSessionId(); String sessionId = handler.getClientSessionId();
managerFactory.sessionManager().disconnect(sessionId, DisconnectReason.Disconnected); DisconnectReason reason = DisconnectReason.DisconnectedByUser;
if (SessionImpl.KEEP_MY_OLD_SESSION.equals(sessionId)) {
// no need in additional code, server will keep original session until inactive timeout
reason = DisconnectReason.DisconnectedByUserButKeepTables;
}
managerFactory.sessionManager().disconnect(sessionId, reason, true);
} }
} }
private static Class<?> loadPlugin(Plugin plugin) { private static Class<?> loadPlugin(Plugin plugin) {

View file

@ -7,9 +7,25 @@ import mage.server.game.ReplayManagerImpl;
import mage.server.managers.*; import mage.server.managers.*;
import mage.server.tournament.TournamentManagerImpl; import mage.server.tournament.TournamentManagerImpl;
import mage.server.util.ThreadExecutorImpl; import mage.server.util.ThreadExecutorImpl;
import org.apache.log4j.Logger;
import java.util.concurrent.TimeUnit;
/**
* @author Burato, JayDi85
*/
public class MainManagerFactory implements ManagerFactory { public class MainManagerFactory implements ManagerFactory {
private final Logger logger = Logger.getLogger(MainManagerFactory.class);
// defines how often checking process should be run on server (in minutes)
// TODO: WARNING, it's can be very buggy but very rare (quit players for no reason, e.g. empty deck bug in sideboard)
// main reason - health code can run in the moment of game move from one stage to another (e.g. on sideboarding prepare)
// TODO: add debug menu like "call server health in 10 seconds"
// TODO: add debug menu like "call server side disconnect in 10 seconds"
// TODO: add debug menu like "call client side disconnect in 10 seconds"
private static final int SERVER_HEALTH_CHECK_TIMEOUT_MINS = 10;
private final ConfigSettings configSettings; private final ConfigSettings configSettings;
private final ThreadExecutor threadExecutor; private final ThreadExecutor threadExecutor;
private final ChatManager chatManager; private final ChatManager chatManager;
@ -24,7 +40,6 @@ public class MainManagerFactory implements ManagerFactory {
private final UserManager userManager; private final UserManager userManager;
private final TournamentManager tournamentManager; private final TournamentManager tournamentManager;
public MainManagerFactory(ConfigSettings configSettings) { public MainManagerFactory(ConfigSettings configSettings) {
this.configSettings = configSettings; this.configSettings = configSettings;
// ThreadExecutorImpl, MailClientImpl and MailGunClient depend only on the config, so they are initialised first // ThreadExecutorImpl, MailClientImpl and MailGunClient depend only on the config, so they are initialised first
@ -47,6 +62,7 @@ public class MainManagerFactory implements ManagerFactory {
this.gamesRoomManager = gamesRoomManager; this.gamesRoomManager = gamesRoomManager;
this.tableManager = tableManager; this.tableManager = tableManager;
this.userManager = userManager; this.userManager = userManager;
// execute the initialisation block of the relevant manager (they start the executor services) // execute the initialisation block of the relevant manager (they start the executor services)
startThreads(gamesRoomManager, tableManager, userManager); startThreads(gamesRoomManager, tableManager, userManager);
} }
@ -55,6 +71,21 @@ public class MainManagerFactory implements ManagerFactory {
userManager.init(); userManager.init();
tableManager.init(); tableManager.init();
gamesRoomManager.init(); gamesRoomManager.init();
threadExecutor().getServerHealthExecutor().scheduleAtFixedRate(() -> {
try {
//logger.info("---");
//logger.info("Server health check started");
this.tableManager().checkHealth();
this.chatManager().checkHealth();
this.userManager().checkHealth();
this.sessionManager().checkHealth();
} catch (Exception ex) {
logger.fatal("Server health check: catch unknown error - " + ex, ex);
}
//logger.info("Server health check end");
//logger.info("---");
}, SERVER_HEALTH_CHECK_TIMEOUT_MINS, SERVER_HEALTH_CHECK_TIMEOUT_MINS, TimeUnit.MINUTES);
} }
@Override @Override

View file

@ -9,8 +9,9 @@ import mage.players.net.UserGroup;
import mage.server.game.GamesRoom; import mage.server.game.GamesRoom;
import mage.server.managers.ConfigSettings; import mage.server.managers.ConfigSettings;
import mage.server.managers.ManagerFactory; import mage.server.managers.ManagerFactory;
import mage.utils.SystemUtil;
import mage.util.RandomUtil; import mage.util.RandomUtil;
import mage.util.ThreadUtils;
import mage.utils.SystemUtil;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.jboss.remoting.callback.AsynchInvokerCallbackHandler; import org.jboss.remoting.callback.AsynchInvokerCallbackHandler;
import org.jboss.remoting.callback.Callback; import org.jboss.remoting.callback.Callback;
@ -27,16 +28,41 @@ import java.util.regex.Pattern;
import static mage.server.DisconnectReason.LostConnection; import static mage.server.DisconnectReason.LostConnection;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com, JayDi85
*/ */
public class Session { public class Session {
private static final Logger logger = Logger.getLogger(Session.class); private static final Logger logger = Logger.getLogger(Session.class);
private static final Pattern alphabetsPattern = Pattern.compile("[a-zA-Z]"); private static final Pattern alphabetsPattern = Pattern.compile("[a-zA-Z]");
private static final Pattern digitsPattern = Pattern.compile("[0-9]"); private static final Pattern digitsPattern = Pattern.compile("[0-9]");
public static final String REGISTRATION_DISABLED_MESSAGE = "Registration has been disabled on the server. You can use any name and empty password to login."; public static final String REGISTRATION_DISABLED_MESSAGE = "Registration has been disabled on the server. You can use any name and empty password to login.";
// Connection and reconnection logic:
// - server allows only one user intance (user name for search, userId for memory/db store)
// - if same user connected then all other user's intances must be removed (disconnected)
// - for auth mode: identify intance by userId (must disconnect all other clients with same user name)
// - for anon mode: identify intance by unique mark like IP/host (must disconnect all other clients with same user name + IP)
// - for any mode: optional identify by restore session (must disconnect all other clients with same user name)
// - TODO: implement client id mark instead host like md5(mac, os, somefolders)
// - if server can't remove another user intance then restrict to connect (example: anon user connected, but there is another anon with diff IP)
private static final boolean ANON_IDENTIFY_BY_HOST = true; // for anon mode only: true - kick all other users with same IP; false - keep first connected user
// async data transfer for all callbacks (transfer of game updates from server to client):
// - pros:
// * SIGNIFICANT performance boost and pings (yep, that's true);
// * no freeze with disconnected/slow watchers/players;
// - cons:
// * data re-sync? TODO: need research, possible problem: outdated battlefield, need click on avatar
// * what about slow connection or big pings? TODO: need research, possible problem: too many re-sync messages in client logs, outdated battlefield, wrong answers
// * what about disconnected players? TODO: need research, possible problem: server can't detect disconnected players too fast
// * what about too many requests to freezed players (too much threads)?
// TODO: need research, possible problem: if something bad with server connection then it can generates
// too many requests to a client and can cause threads overflow/limit (example: watch AI only games?);
// visualvm can help with threads monitor
private static final boolean SUPER_DUPER_BUGGY_AND_FASTEST_ASYNC_CONNECTION = false; // TODO: enable after full research
private final ManagerFactory managerFactory; private final ManagerFactory managerFactory;
private final String sessionId; private final String sessionId;
private UUID userId; private UUID userId;
@ -60,7 +86,7 @@ public class Session {
this.callBackLock = new ReentrantLock(); this.callBackLock = new ReentrantLock();
} }
public String registerUser(String userName, String password, String email) throws MageException { public String registerUser(String userName, String password, String email) {
if (!managerFactory.configSettings().isAuthenticationActivated()) { if (!managerFactory.configSettings().isAuthenticationActivated()) {
String returnMessage = REGISTRATION_DISABLED_MESSAGE; String returnMessage = REGISTRATION_DISABLED_MESSAGE;
sendErrorMessageToClient(returnMessage); sendErrorMessageToClient(returnMessage);
@ -138,10 +164,8 @@ public class Session {
} }
private String validateUserName(String userName) { private String validateUserName(String userName) {
// return error message or null on good name if (userName.equals(User.ADMIN_NAME)) {
if (userName.equals("Admin")) { return "User name already in use";
// virtual user for admin console
return "User name Admin already in use";
} }
String returnMessage = validateUserNameLength(userName); String returnMessage = validateUserNameLength(userName);
@ -195,31 +219,30 @@ public class Session {
return null; return null;
} }
public String connectUser(String userName, String password) throws MageException { public String connectUser(String userName, String password, String restoreSessionId) throws MageException {
String returnMessage = validateUserNameLength(userName); // check username
if (returnMessage != null) { String errorMessage = validateUserNameLength(userName);
sendErrorMessageToClient(returnMessage); if (errorMessage != null) {
return returnMessage; return errorMessage;
} }
returnMessage = connectUserHandling(userName, password);
if (returnMessage != null) { // check auth/anon
try { errorMessage = connectUserHandling(userName, password, restoreSessionId);
Thread.sleep(3000); return errorMessage;
} catch (InterruptedException e) {
logger.fatal("waiting of error message had failed", e);
Thread.currentThread().interrupt();
}
sendErrorMessageToClient(returnMessage);
}
return returnMessage;
} }
public boolean isLocked() { public boolean isLocked() {
return lock.isLocked(); return lock.isLocked();
} }
public String connectUserHandling(String userName, String password) throws MageException { /**
* Auth user on server (link current session with server's user)
* Return null on good or error message on bad
*/
public String connectUserHandling(String userName, String password, String restoreSessionId) {
this.isAdmin = false; this.isAdmin = false;
// find auth user
AuthorizedUser authorizedUser = null; AuthorizedUser authorizedUser = null;
if (managerFactory.configSettings().isAuthenticationActivated()) { if (managerFactory.configSettings().isAuthenticationActivated()) {
authorizedUser = AuthorizedUserRepository.getInstance().getByName(userName); authorizedUser = AuthorizedUserRepository.getInstance().getByName(userName);
@ -247,46 +270,95 @@ public class Session {
} }
} }
Optional<User> selectUser = managerFactory.userManager().createUser(userName, host, authorizedUser); // create new user instance (auth or anon)
boolean reconnect = false; boolean isReconnection = false;
if (!selectUser.isPresent()) { User newUser = managerFactory.userManager().createUser(userName, host, authorizedUser).orElse(null);
// user already connected
selectUser = managerFactory.userManager().getUserByName(userName); // if user instance already exists then keep only one instance
if (selectUser.isPresent()) { if (newUser == null) {
User user = selectUser.get(); User anotherUser = managerFactory.userManager().getUserByName(userName).orElse(null);
// If authentication is not activated, check the identity using IP address. if (anotherUser != null) {
if (managerFactory.configSettings().isAuthenticationActivated() || user.getHost().equals(host)) { boolean canDisconnectAuthDueAnotherInstance = managerFactory.configSettings().isAuthenticationActivated();
user.updateLastActivity(null); // minimizes possible expiration boolean canDisconnectAnonDueSameHost = !managerFactory.configSettings().isAuthenticationActivated()
this.userId = user.getId(); && ANON_IDENTIFY_BY_HOST
if (user.getSessionId().isEmpty()) { && Objects.equals(anotherUser.getHost(), host);
logger.info("Reconnecting session for " + userName); boolean canDisconnectAnyDueSessionRestore = Objects.equals(restoreSessionId, anotherUser.getRestoreSessionId());
reconnect = true; if (canDisconnectAuthDueAnotherInstance
} else { || canDisconnectAnonDueSameHost
//disconnect previous session || canDisconnectAnyDueSessionRestore) {
logger.info("Disconnecting another user instance: " + userName); anotherUser.updateLastActivity(null); // minimizes possible expiration
managerFactory.sessionManager().disconnect(user.getSessionId(), DisconnectReason.ConnectingOtherInstance);
// disconnect another user instance, but keep all active tables
if (!anotherUser.getSessionId().isEmpty()) {
// do not use DisconnectReason.Disconnected here - it will remove user from all tables,
// but it must remove only session without any tables
String instanceReason = canDisconnectAnyDueSessionRestore ? " (session reconnect)"
: canDisconnectAnonDueSameHost ? " (same host)"
: canDisconnectAuthDueAnotherInstance ? " (same user)" : "";
String mes = "";
mes += String.format("Disconnecting another user instance for %s (reason: %s)", userName, instanceReason);
if (logger.isDebugEnabled()) {
mes += String.format("\n - reason: auth (%s), anon host (%s), any session (%s)",
(canDisconnectAuthDueAnotherInstance ? "yes" : "no"),
(canDisconnectAnonDueSameHost ? "yes" : "no"),
(canDisconnectAnyDueSessionRestore ? "yes" : "no")
);
mes += String.format("\n - sessionId: %s => %s", anotherUser.getSessionId(), sessionId);
mes += String.format("\n - name: %s => %s", anotherUser.getName(), userName);
mes += String.format("\n - host: %s => %s", anotherUser.getHost(), host);
} }
logger.info(mes);
// kill another instance
DisconnectReason reason = DisconnectReason.AnotherUserInstance;
if (ANON_IDENTIFY_BY_HOST && Objects.equals(anotherUser.getHost(), host)) {
// if user reconnects by itself then must hide another instance message
reason = DisconnectReason.AnotherUserInstanceSilent;
}
managerFactory.userManager().disconnect(anotherUser.getId(), reason);
}
isReconnection = true;
newUser = anotherUser;
} else { } else {
return "User name " + userName + " already in use (or your IP address changed)"; return "User " + userName + " already connected or your IP address changed - try another user";
} }
} else { } else {
// code never goes here // code never goes here
return "Can't find connected user name " + userName; return "Can't find connected user name " + userName;
} }
} }
User user = selectUser.get();
if (!managerFactory.userManager().connectToSession(sessionId, user.getId())) { // link user with current session
return "Error connecting " + userName; newUser.setRestoreSessionId("");
this.userId = newUser.getId();
if (!managerFactory.userManager().connectToSession(sessionId, this.userId)) {
return "Error link user " + userName + " with session " + sessionId;
} }
this.userId = user.getId();
if (reconnect) { // must be connected to receive the message // connect to lobby (other chats must be joined from a client side on table panel creating process)
Optional<GamesRoom> room = managerFactory.gamesRoomManager().getRoom(managerFactory.gamesRoomManager().getMainRoomId()); GamesRoom lobby = managerFactory.gamesRoomManager().getRoom(managerFactory.gamesRoomManager().getMainRoomId()).orElse(null);
if (!room.isPresent()) { if (lobby != null) {
logger.warn("main room not found"); // after server restart users try to use old rooms on reconnect managerFactory.chatManager().joinChat(lobby.getChatId(), this.userId);
return null; } else {
// TODO: outdated code? Need research server logs and fix or delete it, 2023-12-04
logger.warn("main room not found"); // after server restart users try to use old rooms on reconnect for some reason
} }
managerFactory.chatManager().joinChat(room.get().getChatId(), userId);
managerFactory.chatManager().sendReconnectMessage(userId); // all fine
newUser.setUserState(User.UserState.Connected);
newUser.setRestoreSessionId(newUser.getSessionId());
// restore all active tables
// run in diff thread, so user will be connected anyway (e.g. on some errors in onReconnect)
final User reconnectUser = newUser;
if (isReconnection) {
managerFactory.threadExecutor().getCallExecutor().execute(reconnectUser::onReconnect);
}
// inform about reconnection
if (isReconnection) {
managerFactory.chatManager().sendReconnectMessage(this.userId);
} }
return null; return null;
@ -294,13 +366,13 @@ public class Session {
public void connectAdmin() { public void connectAdmin() {
this.isAdmin = true; this.isAdmin = true;
User user = managerFactory.userManager().createUser("Admin", host, null).orElse( User user = managerFactory.userManager().createUser(User.ADMIN_NAME, host, null)
managerFactory.userManager().getUserByName("Admin").get()); .orElse(managerFactory.userManager().getUserByName(User.ADMIN_NAME).get());
UserData adminUserData = UserData.getDefaultUserDataView(); UserData adminUserData = UserData.getDefaultUserDataView();
adminUserData.setGroupId(UserGroup.ADMIN.getGroupId()); adminUserData.setGroupId(UserGroup.ADMIN.getGroupId());
user.setUserData(adminUserData); user.setUserData(adminUserData);
if (!managerFactory.userManager().connectToSession(sessionId, user.getId())) { if (!managerFactory.userManager().connectToSession(sessionId, user.getId())) {
logger.info("Error connecting Admin!"); logger.info("Error connecting as admin");
} else { } else {
user.setUserState(User.UserState.Connected); user.setUserState(User.UserState.Connected);
} }
@ -354,67 +426,39 @@ public class Session {
return sessionId; return sessionId;
} }
// because different threads can activate this /**
public void userLostConnection() { * Send event/command to the client
Optional<User> _user = managerFactory.userManager().getUser(userId); */
if (!_user.isPresent()) {
return; //user was already disconnected by other thread
}
User user = _user.get();
if (!user.isConnected()) {
return;
}
if (!user.getSessionId().equals(sessionId)) {
// user already reconnected with another instance
logger.info("OLD SESSION IGNORED - " + user.getName());
} else {
// logger.info("LOST CONNECTION - " + user.getName() + " id: " + userId);
}
}
public void kill(DisconnectReason reason) {
boolean lockSet = false;
try {
if (lock.tryLock(5000, TimeUnit.MILLISECONDS)) {
lockSet = true;
logger.debug("SESSION LOCK SET sessionId: " + sessionId);
} else {
logger.error("SESSION LOCK - kill: userId " + userId);
}
managerFactory.userManager().removeUserFromAllTablesAndChat(userId, reason);
} catch (InterruptedException ex) {
logger.error("SESSION LOCK - kill: userId " + userId, ex);
} finally {
if (lockSet) {
lock.unlock();
logger.debug("SESSION LOCK UNLOCK sessionId: " + sessionId);
}
}
}
public void fireCallback(final ClientCallback call) { public void fireCallback(final ClientCallback call) {
boolean lockSet = false; boolean lockSet = false; // TODO: research about locks, why it here? 2023-12-06
try { try {
if (valid && callBackLock.tryLock(50, TimeUnit.MILLISECONDS)) { if (valid && callBackLock.tryLock(50, TimeUnit.MILLISECONDS)) {
call.setMessageId(messageId.incrementAndGet()); call.setMessageId(messageId.incrementAndGet());
lockSet = true; lockSet = true;
Callback callback = new Callback(call); Callback callback = new Callback(call);
callbackHandler.handleCallbackOneway(callback); callbackHandler.handleCallbackOneway(callback, SUPER_DUPER_BUGGY_AND_FASTEST_ASYNC_CONNECTION);
} }
} catch (InterruptedException ex) { } catch (InterruptedException ex) {
logger.warn("SESSION LOCK - fireCallback - userId: " + userId + " messageId: " + call.getMessageId(), ex); // already sending another command (connection problem?)
if (call.getMethod().equals(ClientCallbackMethod.GAME_INIT)
|| call.getMethod().equals(ClientCallbackMethod.START_GAME)) {
// it's ok use case, user has connection problem so can't send game init (see sendInfoAboutPlayersNotJoinedYetAndTryToFixIt)
} else {
logger.warn("SESSION LOCK, possible connection problem - fireCallback - userId: " + userId + " messageId: " + call.getMessageId(), ex);
}
} catch (HandleCallbackException ex) { } catch (HandleCallbackException ex) {
// something wrong, maybe connection problem
logger.warn("SESSION CALLBACK EXCEPTION - " + ThreadUtils.findRootException(ex) + ", userId " + userId + ", messageId: " + call.getMessageId(), ex);
// do not send data anymore (user must reconnect)
this.valid = false; this.valid = false;
managerFactory.userManager().getUser(userId).ifPresent(user -> { managerFactory.sessionManager().disconnect(sessionId, LostConnection, true);
user.setUserState(User.UserState.Disconnected); } catch (Throwable ex) {
logger.warn("SESSION CALLBACK EXCEPTION - " + user.getName() + " userId " + userId + " messageId: " + call.getMessageId() + " - cause: " + getBasicCause(ex).toString()); logger.error("SESSION CALLBACK UNKNOWN EXCEPTION - " + ThreadUtils.findRootException(ex) + ", userId " + userId + ", messageId: " + call.getMessageId(), ex);
logger.trace("Stack trace:", ex);
managerFactory.sessionManager().disconnect(sessionId, LostConnection); // do not send data anymore (user must reconnect)
}); this.valid = false;
} catch (Exception ex) { managerFactory.sessionManager().disconnect(sessionId, LostConnection, true);
logger.warn("Unspecific exception:", ex);
} finally { } finally {
if (lockSet) { if (lockSet) {
callBackLock.unlock(); callBackLock.unlock();

View file

@ -2,8 +2,8 @@ package mage.server;
import mage.MageException; import mage.MageException;
import mage.players.net.UserData; import mage.players.net.UserData;
import mage.server.managers.SessionManager;
import mage.server.managers.ManagerFactory; import mage.server.managers.ManagerFactory;
import mage.server.managers.SessionManager;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.jboss.remoting.callback.InvokerCallbackHandler; import org.jboss.remoting.callback.InvokerCallbackHandler;
@ -12,7 +12,9 @@ import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
* @author BetaSteward_at_googlemail.com * Server: manage all connections (first connection, after auth, after anon)
*
* @author BetaSteward_at_googlemail.com, JayDi85
*/ */
public class SessionManagerImpl implements SessionManager { public class SessionManagerImpl implements SessionManager {
@ -27,18 +29,7 @@ public class SessionManagerImpl implements SessionManager {
@Override @Override
public Optional<Session> getSession(@Nonnull String sessionId) { public Optional<Session> getSession(@Nonnull String sessionId) {
Session session = sessions.get(sessionId); return Optional.ofNullable(sessions.getOrDefault(sessionId, null));
if (session == null) {
logger.trace("Session with sessionId " + sessionId + " is not found");
return Optional.empty();
}
if (session.getUserId() != null && !managerFactory.userManager().getUser(session.getUserId()).isPresent()) {
logger.error("User for session " + sessionId + " with userId " + session.getUserId() + " is missing. Session removed.");
// can happen if user from same host signs in multiple time with multiple clients, after they disconnect with one client
disconnect(sessionId, DisconnectReason.ConnectingOtherInstance, session); // direct disconnect
return Optional.empty();
}
return Optional.of(session);
} }
@Override @Override
@ -67,18 +58,33 @@ public class SessionManagerImpl implements SessionManager {
} }
@Override @Override
public boolean connectUser(String sessionId, String userName, String password, String userIdStr) throws MageException { public boolean connectUser(String sessionId, String restoreSessionId, String userName, String password, String userInfo, boolean detailsMode) throws MageException {
Session session = sessions.get(sessionId); Session session = sessions.get(sessionId);
if (session != null) { if (session != null) {
String returnMessage = session.connectUser(userName, password); String errorMessage = session.connectUser(userName, password, restoreSessionId);
if (returnMessage == null) { if (errorMessage == null) {
logger.info(userName + " connected to server"); logger.info(userName + " connected to server by sessionId " + sessionId
+ (restoreSessionId.isEmpty() ? "" : ", restoreSessionId " + restoreSessionId));
if (detailsMode) {
logger.info("- details: " + userInfo);
}
logger.debug("- userId: " + session.getUserId()); logger.debug("- userId: " + session.getUserId());
logger.debug("- sessionId: " + sessionId); logger.debug("- sessionId: " + sessionId);
logger.debug("- restoreSessionId: " + restoreSessionId);
logger.debug("- host: " + session.getHost()); logger.debug("- host: " + session.getHost());
return true; return true;
} else { } else {
logger.debug(userName + " not connected: " + returnMessage); logger.debug(userName + " not connected: " + errorMessage);
// send error to client side
try {
// ddos/bruteforce protection
Thread.sleep(3000);
} catch (InterruptedException e) {
logger.fatal("waiting of error message had failed", e);
Thread.currentThread().interrupt();
}
session.sendErrorMessageToClient(errorMessage);
} }
} else { } else {
logger.error(userName + " tried to connect with no sessionId"); logger.error(userName + " tried to connect with no sessionId");
@ -106,46 +112,27 @@ public class SessionManagerImpl implements SessionManager {
} }
@Override @Override
public void disconnect(String sessionId, DisconnectReason reason) { public void disconnect(String sessionId, DisconnectReason reason, boolean checkUserDisconnection) {
disconnect(sessionId, reason, null); Session session = getSession(sessionId).orElse(null);
} if (session == null) {
@Override
public void disconnect(String sessionId, DisconnectReason reason, Session directSession) {
if (directSession == null) {
// find real session to disconnects
getSession(sessionId).ifPresent(session -> {
if (!isValidSession(sessionId)) {
// session was removed meanwhile by another thread so we can return
return; return;
} }
logger.debug("DISCONNECT " + reason.toString() + " - sessionId: " + sessionId);
sessions.remove(sessionId); if (!isValidSession(sessionId)) {
switch (reason) { logger.info("DISCONNECT session, already invalid: " + reason + " - sessionId: " + sessionId);
case AdminDisconnect: // session was removed meanwhile by another thread
session.kill(reason); // TODO: otudated code? If no logs on server then remove that code, 2023-12-06
break; return;
case ConnectingOtherInstance:
case Disconnected: // regular session end or wrong client version
managerFactory.userManager().disconnect(session.getUserId(), reason);
break;
case SessionExpired: // session ends after no reconnect happens in the defined time span
break;
case LostConnection: // user lost connection - session expires countdown starts
session.userLostConnection();
managerFactory.userManager().disconnect(session.getUserId(), reason);
break;
default:
logger.trace("endSession: unexpected reason " + reason.toString() + " - sessionId: " + sessionId);
}
});
} else {
// direct session to disconnects
sessions.remove(sessionId);
directSession.kill(reason);
}
} }
if (checkUserDisconnection) {
managerFactory.userManager().getUser(session.getUserId()).ifPresent(user -> {
user.onLostConnection(reason);
});
}
sessions.remove(sessionId);
}
/** /**
* Admin requested the disconnect of a user * Admin requested the disconnect of a user
@ -154,36 +141,24 @@ public class SessionManagerImpl implements SessionManager {
* @param userSessionId * @param userSessionId
*/ */
@Override @Override
public void disconnectUser(String sessionId, String userSessionId) { public void disconnectAnother(String sessionId, String userSessionId) {
if (!checkAdminAccess(sessionId)) { if (!checkAdminAccess(sessionId)) {
return; return;
} }
getUserFromSession(sessionId).ifPresent(admin -> {
Optional<User> u = getUserFromSession(userSessionId); User admin = getUserFromSession(sessionId).orElse(null);
if (u.isPresent()) { User user = getUserFromSession(userSessionId).orElse(null);
User user = u.get(); if (admin == null || user == null) {
user.showUserMessage("Admin action", "Your session was disconnected by admin"); return;
admin.showUserMessage("Admin result", "User " + user.getName() + " was disconnected");
disconnect(userSessionId, DisconnectReason.AdminDisconnect);
} else {
admin.showUserMessage("Admin result", "User with sessionId " + userSessionId + " could not be found");
} }
});
user.showUserMessage("Admin action", "Your session was disconnected by admin");
disconnect(userSessionId, DisconnectReason.DisconnectedByAdmin, true);
admin.showUserMessage("Admin result", "User " + user.getName() + " was disconnected");
} }
private Optional<User> getUserFromSession(String sessionId) { private Optional<User> getUserFromSession(String sessionId) {
return getSession(sessionId) return getSession(sessionId).flatMap(s -> managerFactory.userManager().getUser(s.getUserId()));
.flatMap(s -> managerFactory.userManager().getUser(s.getUserId()));
}
@Override
public void endUserSession(String sessionId, String userSessionId) {
if (!checkAdminAccess(sessionId)) {
return;
}
disconnect(userSessionId, DisconnectReason.AdminDisconnect);
} }
@Override @Override
@ -233,4 +208,10 @@ public class SessionManagerImpl implements SessionManager {
} }
session.sendErrorMessageToClient(message); session.sendErrorMessageToClient(message);
} }
@Override
public void checkHealth() {
//logger.info("Checking sessions...");
// TODO: add lone sessions check and report (with lost user)
}
} }

View file

@ -14,8 +14,8 @@ import mage.game.tournament.TournamentOptions;
import mage.game.tournament.TournamentPlayer; import mage.game.tournament.TournamentPlayer;
import mage.players.PlayerType; import mage.players.PlayerType;
import mage.server.game.GameController; import mage.server.game.GameController;
import mage.server.managers.TableManager;
import mage.server.managers.ManagerFactory; import mage.server.managers.ManagerFactory;
import mage.server.managers.TableManager;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.text.DateFormat; import java.text.DateFormat;
@ -23,9 +23,6 @@ import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
@ -34,7 +31,6 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public class TableManagerImpl implements TableManager { public class TableManagerImpl implements TableManager {
protected final ScheduledExecutorService expireExecutor = Executors.newSingleThreadScheduledExecutor();
private final ManagerFactory managerFactory; private final ManagerFactory managerFactory;
private final Logger logger = Logger.getLogger(TableManagerImpl.class); private final Logger logger = Logger.getLogger(TableManagerImpl.class);
@ -46,22 +42,11 @@ public class TableManagerImpl implements TableManager {
private final ConcurrentHashMap<UUID, Table> tables = new ConcurrentHashMap<>(); private final ConcurrentHashMap<UUID, Table> tables = new ConcurrentHashMap<>();
private final ReadWriteLock tablesLock = new ReentrantReadWriteLock(); private final ReadWriteLock tablesLock = new ReentrantReadWriteLock();
// defines how often checking process should be run on server (in minutes)
private static final int TABLE_HEALTH_CHECK_TIMEOUT_MINS = 10;
public TableManagerImpl(ManagerFactory managerFactory) { public TableManagerImpl(ManagerFactory managerFactory) {
this.managerFactory = managerFactory; this.managerFactory = managerFactory;
} }
public void init() { public void init() {
expireExecutor.scheduleAtFixedRate(() -> {
try {
managerFactory.chatManager().clearUserMessageStorage();
checkTableHealthState();
} catch (Exception ex) {
logger.fatal("Check table health state job error:", ex);
}
}, TABLE_HEALTH_CHECK_TIMEOUT_MINS, TABLE_HEALTH_CHECK_TIMEOUT_MINS, TimeUnit.MINUTES);
} }
@Override @Override
@ -426,7 +411,7 @@ public class TableManagerImpl implements TableManager {
List<ChatSession> chatSessions = managerFactory.chatManager().getChatSessions(); List<ChatSession> chatSessions = managerFactory.chatManager().getChatSessions();
logger.debug("------- ChatSessions: " + chatSessions.size() + " ----------------------------------"); logger.debug("------- ChatSessions: " + chatSessions.size() + " ----------------------------------");
for (ChatSession chatSession : chatSessions) { for (ChatSession chatSession : chatSessions) {
logger.debug(chatSession.getChatId() + " " + formatter.format(chatSession.getCreateTime()) + ' ' + chatSession.getInfo() + ' ' + chatSession.getClients().values().toString()); logger.debug(chatSession.getChatId() + " " + formatter.format(chatSession.getCreateTime()) + ' ' + chatSession.getInfo() + ' ' + chatSession.getUsers().values().toString());
} }
logger.debug("------- Games: " + managerFactory.gameManager().getNumberActiveGames() + " --------------------------------------------"); logger.debug("------- Games: " + managerFactory.gameManager().getNumberActiveGames() + " --------------------------------------------");
logger.debug(" Active Game Worker: " + managerFactory.threadExecutor().getActiveThreads(managerFactory.threadExecutor().getGameExecutor())); logger.debug(" Active Game Worker: " + managerFactory.threadExecutor().getActiveThreads(managerFactory.threadExecutor().getGameExecutor()));
@ -436,7 +421,8 @@ public class TableManagerImpl implements TableManager {
logger.debug("--- Server state END ------------------------------------------"); logger.debug("--- Server state END ------------------------------------------");
} }
private void checkTableHealthState() { private void removeOutdatedTables() {
// TODO: need research and check - is it actual code or not, 2023-12-06
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
debugServerState(); debugServerState();
} }
@ -465,6 +451,13 @@ public class TableManagerImpl implements TableManager {
} }
} }
logger.debug("TABLE HEALTH CHECK - END"); logger.debug("TABLE HEALTH CHECK - END");
}
@Override
public void checkHealth() {
//logger.info("Checking tables...");
// TODO: add memory reports
// TODO: add broken tables check and report (without seats, without gamecontroller, without active players, too long playing, etc)
removeOutdatedTables();
} }
} }

View file

@ -29,12 +29,14 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com, JayDi85
*/ */
public class User { public class User {
private static final Logger logger = Logger.getLogger(User.class); private static final Logger logger = Logger.getLogger(User.class);
public static final String ADMIN_NAME = "Admin"; // if you change here then change in SessionImpl too
public enum UserState { public enum UserState {
Created, // Used if user is created an not connected to the session Created, // Used if user is created an not connected to the session
Connected, // Used if user is correctly connected Connected, // Used if user is correctly connected
@ -56,6 +58,7 @@ public class User {
private final Map<UUID, Deck> sideboarding; private final Map<UUID, Deck> sideboarding;
private final List<UUID> watchedGames; private final List<UUID> watchedGames;
private String sessionId; private String sessionId;
private String restoreSessionId; // keep sessionId for possible restore on reconnect
private String pingInfo = ""; private String pingInfo = "";
private Date lastActivity; private Date lastActivity;
private UserState userState; private UserState userState;
@ -118,6 +121,10 @@ public class User {
return sessionId; return sessionId;
} }
public String getRestoreSessionId() {
return restoreSessionId;
}
public Date getChatLockedUntil() { public Date getChatLockedUntil() {
return chatLockedUntil; return chatLockedUntil;
} }
@ -132,19 +139,10 @@ public class User {
public void setSessionId(String sessionId) { public void setSessionId(String sessionId) {
this.sessionId = sessionId; this.sessionId = sessionId;
if (sessionId.isEmpty()) {
setUserState(UserState.Disconnected);
lostConnection();
logger.trace("USER - lost connection: " + userName + " id: " + userId);
} else if (userState == UserState.Created) {
setUserState(UserState.Connected);
logger.trace("USER - created: " + userName + " id: " + userId);
} else {
setUserState(UserState.Connected);
reconnect();
logger.trace("USER - reconnected: " + userName + " id: " + userId);
} }
public void setRestoreSessionId(String restoreSessionId) {
this.restoreSessionId = restoreSessionId;
} }
public void setClientVersion(String clientVersion) { public void setClientVersion(String clientVersion) {
@ -178,14 +176,44 @@ public class User {
updateAuthorizedUser(); updateAuthorizedUser();
} }
public void lostConnection() { /**
// Because watched games don't get restored after reconnection call stop watching * Shared code for any user disconnection
* <p>
* Full user instance remove will be done by reconnect or by server health check
*
* @param reason
*/
public void onLostConnection(DisconnectReason reason) {
// stats for bad connections only
if (reason != DisconnectReason.DisconnectedByUser
&& reason != DisconnectReason.DisconnectedByUserButKeepTables) {
ServerMessagesUtil.instance.incLostConnection();
}
// chats restored on reconnection from a scrach by game panels - so don't keep it
managerFactory.chatManager().removeUser(userId, reason);
// watched games don't get restored after reconnection call - so don't keep it
for (Iterator<UUID> iterator = watchedGames.iterator(); iterator.hasNext(); ) { for (Iterator<UUID> iterator = watchedGames.iterator(); iterator.hasNext(); ) {
UUID gameId = iterator.next(); UUID gameId = iterator.next();
managerFactory.gameManager().stopWatching(gameId, userId); managerFactory.gameManager().stopWatching(gameId, userId);
iterator.remove(); iterator.remove();
} }
ServerMessagesUtil.instance.incLostConnection();
// game tables
if (reason.isRemoveUserTables) {
// full disconnection
removeUserFromAllTables(reason); // it's duplicates some chat/watch code inside, but it ok
setUserState(UserState.Offline);
managerFactory.userManager().removeUser(getId());
} else {
setUserState(UserState.Disconnected);
}
// kill active session
String oldSessionId = sessionId;
setSessionId("");
managerFactory.sessionManager().disconnect(oldSessionId, reason, false);
} }
public boolean isConnected() { public boolean isConnected() {
@ -317,10 +345,8 @@ public class User {
this.pingInfo = pingInfo; this.pingInfo = pingInfo;
} }
lastActivity = new Date(); lastActivity = new Date();
if (userState == UserState.Disconnected) { // this can happen if user reconnects very fast after disconnect
setUserState(UserState.Connected); setUserState(UserState.Connected);
} }
}
public boolean isExpired(Date expired) { public boolean isExpired(Date expired) {
if (lastActivity.before(expired)) { if (lastActivity.before(expired)) {
@ -332,11 +358,19 @@ public class User {
} }
private void reconnect() { /**
logger.trace(userName + " started reconnect"); * Restore all active tables and another data
*/
public void onReconnect() {
// TODO: research tables restore - it's old code, so check table state, timers, actions, ?concede?, ?skips?
ServerMessagesUtil.instance.incReconnects();
// active tables
for (Entry<UUID, Table> entry : tables.entrySet()) { for (Entry<UUID, Table> entry : tables.entrySet()) {
ccJoinedTable(entry.getValue().getRoomId(), entry.getValue().getId(), entry.getValue().isTournament()); ccJoinedTable(entry.getValue().getRoomId(), entry.getValue().getId(), entry.getValue().isTournament());
} }
// active tourneys
for (Iterator<Entry<UUID, UUID>> iterator = userTournaments.entrySet().iterator(); iterator.hasNext(); ) { for (Iterator<Entry<UUID, UUID>> iterator = userTournaments.entrySet().iterator(); iterator.hasNext(); ) {
Entry<UUID, UUID> next = iterator.next(); Entry<UUID, UUID> next = iterator.next();
Optional<TournamentController> tournamentController = managerFactory.tournamentManager().getTournamentController(next.getValue()); Optional<TournamentController> tournamentController = managerFactory.tournamentManager().getTournamentController(next.getValue());
@ -347,21 +381,27 @@ public class User {
iterator.remove(); // tournament has ended meanwhile iterator.remove(); // tournament has ended meanwhile
} }
} }
// active games
for (Entry<UUID, GameSessionPlayer> entry : gameSessions.entrySet()) { for (Entry<UUID, GameSessionPlayer> entry : gameSessions.entrySet()) {
ccGameStarted(entry.getValue().getGameId(), entry.getKey()); ccGameStarted(entry.getValue().getGameId(), entry.getKey());
entry.getValue().init(); entry.getValue().init();
managerFactory.gameManager().sendPlayerString(entry.getValue().getGameId(), userId, ""); managerFactory.gameManager().sendPlayerString(entry.getValue().getGameId(), userId, "");
} }
// active drafts
for (Entry<UUID, DraftSession> entry : draftSessions.entrySet()) { for (Entry<UUID, DraftSession> entry : draftSessions.entrySet()) {
ccDraftStarted(entry.getValue().getDraftId(), entry.getKey()); ccDraftStarted(entry.getValue().getDraftId(), entry.getKey());
entry.getValue().init(); entry.getValue().init();
entry.getValue().update(); entry.getValue().update();
} }
// active constructing
for (Entry<UUID, TournamentSession> entry : constructing.entrySet()) { for (Entry<UUID, TournamentSession> entry : constructing.entrySet()) {
entry.getValue().construct(0); // TODO: Check if this is correct entry.getValue().construct(0); // TODO: Check if this is correct
} }
// active sideboarding
for (Entry<UUID, Deck> entry : sideboarding.entrySet()) { for (Entry<UUID, Deck> entry : sideboarding.entrySet()) {
Optional<TableController> controller = managerFactory.tableManager().getController(entry.getKey()); Optional<TableController> controller = managerFactory.tableManager().getController(entry.getKey());
if (controller.isPresent()) { if (controller.isPresent()) {
@ -372,8 +412,6 @@ public class User {
logger.debug(getName() + " reconnects during sideboarding but tableId not found: " + entry.getKey()); logger.debug(getName() + " reconnects during sideboarding but tableId not found: " + entry.getKey());
} }
} }
ServerMessagesUtil.instance.incReconnects();
logger.trace(userName + " ended reconnect");
} }
public void addGame(UUID playerId, GameSessionPlayer gameSession) { public void addGame(UUID playerId, GameSessionPlayer gameSession) {

View file

@ -1,8 +1,7 @@
package mage.server; package mage.server;
import mage.server.User.UserState;
import mage.server.managers.UserManager;
import mage.server.managers.ManagerFactory; import mage.server.managers.ManagerFactory;
import mage.server.managers.UserManager;
import mage.server.record.UserStats; import mage.server.record.UserStats;
import mage.server.record.UserStatsRepository; import mage.server.record.UserStatsRepository;
import mage.view.UserView; import mage.view.UserView;
@ -15,19 +14,19 @@ import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
/** /**
* manages users - if a user is disconnected and 10 minutes have passed with no * Server: manage active user instances (connected to the server)
* activity the user is removed
* *
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com, JayDi85
*/ */
public class UserManagerImpl implements UserManager { public class UserManagerImpl implements UserManager {
private static final int SERVER_TIMEOUTS_USER_INFORM_OPPONENTS_ABOUT_DISCONNECT_AFTER_SECS = 30; // send to chat info about disconnection troubles, must be more than ping timeout // timeouts on user's activity (on connection problems)
private static final int SERVER_TIMEOUTS_USER_DISCONNECT_FROM_SERVER_AFTER_SECS = 3 * 60; // removes from all games and chats too (can be seen in users list with disconnected status) private static final int USER_CONNECTION_TIMEOUTS_CHECK_SECS = 30; // TODO: replace to 60 before merge
private static final int SERVER_TIMEOUTS_USER_REMOVE_FROM_SERVER_AFTER_SECS = 8 * 60; // removes from users list private static final int USER_CONNECTION_TIMEOUT_INFORM_AFTER_SECS = 30; // inform user's opponents about problem
private static final int USER_CONNECTION_TIMEOUT_SESSION_EXPIRE_AFTER_SECS = 3 * 60; // session expire - remove from all tables and chats (can't reconnect after it)
private static final int USER_CONNECTION_TIMEOUT_REMOVE_FROM_SERVER_SECS = 8 * 60; // removes from users list
private static final int SERVER_TIMEOUTS_USER_EXPIRE_CHECK_SECS = 60; private static final int SERVER_USERS_LIST_UPDATE_SECS = 4; // server side updates (client use own timeouts to request users list)
private static final int SERVER_TIMEOUTS_USERS_LIST_UPDATE_SECS = 4; // server side updates (client use own timeouts to request users list)
private static final Logger logger = Logger.getLogger(UserManagerImpl.class); private static final Logger logger = Logger.getLogger(UserManagerImpl.class);
@ -49,8 +48,8 @@ public class UserManagerImpl implements UserManager {
public void init() { public void init() {
USER_EXECUTOR = managerFactory.threadExecutor().getCallExecutor(); USER_EXECUTOR = managerFactory.threadExecutor().getCallExecutor();
expireExecutor.scheduleAtFixedRate(this::checkExpired, SERVER_TIMEOUTS_USER_EXPIRE_CHECK_SECS, SERVER_TIMEOUTS_USER_EXPIRE_CHECK_SECS, TimeUnit.SECONDS); expireExecutor.scheduleAtFixedRate(this::checkExpired, USER_CONNECTION_TIMEOUTS_CHECK_SECS, USER_CONNECTION_TIMEOUTS_CHECK_SECS, TimeUnit.SECONDS);
userListExecutor.scheduleAtFixedRate(this::updateUserInfoList, SERVER_TIMEOUTS_USERS_LIST_UPDATE_SECS, SERVER_TIMEOUTS_USERS_LIST_UPDATE_SECS, TimeUnit.SECONDS); userListExecutor.scheduleAtFixedRate(this::updateUserInfoList, SERVER_USERS_LIST_UPDATE_SECS, SERVER_USERS_LIST_UPDATE_SECS, TimeUnit.SECONDS);
} }
@Override @Override
@ -71,11 +70,16 @@ public class UserManagerImpl implements UserManager {
@Override @Override
public Optional<User> getUser(UUID userId) { public Optional<User> getUser(UUID userId) {
if (!users.containsKey(userId)) { if (userId == null) {
//logger.warn(String.format("User with id %s could not be found", userId), new Throwable()); // TODO: remove after session freezes fixed
return Optional.empty(); return Optional.empty();
} else { }
return Optional.of(users.get(userId));
final Lock r = lock.readLock();
r.lock();
try {
return Optional.ofNullable(users.getOrDefault(userId, null));
} finally {
r.unlock();
} }
} }
@ -84,12 +88,13 @@ public class UserManagerImpl implements UserManager {
final Lock r = lock.readLock(); final Lock r = lock.readLock();
r.lock(); r.lock();
try { try {
return users.values().stream().filter(user -> user.getName().equals(userName)) return users.values()
.stream()
.filter(user -> user.getName().equals(userName))
.findFirst(); .findFirst();
} finally { } finally {
r.unlock(); r.unlock();
} }
} }
@Override @Override
@ -108,9 +113,9 @@ public class UserManagerImpl implements UserManager {
@Override @Override
public boolean connectToSession(String sessionId, UUID userId) { public boolean connectToSession(String sessionId, UUID userId) {
if (userId != null) { if (userId != null) {
User user = users.get(userId); Optional<User> user = getUser(userId);
if (user != null) { if (user.isPresent()) {
user.setSessionId(sessionId); user.get().setSessionId(sessionId);
return true; return true;
} }
} }
@ -119,45 +124,17 @@ public class UserManagerImpl implements UserManager {
@Override @Override
public void disconnect(UUID userId, DisconnectReason reason) { public void disconnect(UUID userId, DisconnectReason reason) {
Optional<User> user = getUser(userId); User user = getUser(userId).orElse(null);
if (user.isPresent()) { if (user != null) {
user.get().setSessionId(""); user.onLostConnection(reason);
if (reason == DisconnectReason.Disconnected) {
removeUserFromAllTablesAndChat(userId, reason);
user.get().setUserState(UserState.Offline);
}
} }
} }
@Override @Override
public boolean isAdmin(UUID userId) { public boolean isAdmin(UUID userId) {
if (userId != null) { return getUser(userId)
User user = users.get(userId); .filter(u -> u.getName().equals(User.ADMIN_NAME))
if (user != null) { .isPresent();
return user.getName().equals("Admin");
}
}
return false;
}
@Override
public void removeUserFromAllTablesAndChat(final UUID userId, final DisconnectReason reason) {
if (userId != null) {
getUser(userId).ifPresent(user
-> USER_EXECUTOR.execute(
() -> {
try {
logger.info("USER REMOVE - " + user.getName() + " (" + reason.toString() + ") userId: " + userId + " [" + user.getGameInfo() + ']');
user.removeUserFromAllTables(reason);
managerFactory.chatManager().removeUser(user.getId(), reason);
logger.debug("USER REMOVE END - " + user.getName());
} catch (Exception ex) {
handleException(ex);
}
}
));
}
} }
@Override @Override
@ -195,13 +172,13 @@ public class UserManagerImpl implements UserManager {
*/ */
private void checkExpired() { private void checkExpired() {
try { try {
Calendar calendarInform = Calendar.getInstance(); Calendar calInform = Calendar.getInstance();
calendarInform.add(Calendar.SECOND, -1 * SERVER_TIMEOUTS_USER_INFORM_OPPONENTS_ABOUT_DISCONNECT_AFTER_SECS); calInform.add(Calendar.SECOND, -1 * USER_CONNECTION_TIMEOUT_INFORM_AFTER_SECS);
Calendar calendarExp = Calendar.getInstance(); Calendar calSessionExpire = Calendar.getInstance();
calendarExp.add(Calendar.SECOND, -1 * SERVER_TIMEOUTS_USER_DISCONNECT_FROM_SERVER_AFTER_SECS); calSessionExpire.add(Calendar.SECOND, -1 * USER_CONNECTION_TIMEOUT_SESSION_EXPIRE_AFTER_SECS);
Calendar calendarRemove = Calendar.getInstance(); Calendar calUserRemove = Calendar.getInstance();
calendarRemove.add(Calendar.SECOND, -1 * SERVER_TIMEOUTS_USER_REMOVE_FROM_SERVER_AFTER_SECS); calUserRemove.add(Calendar.SECOND, -1 * USER_CONNECTION_TIMEOUT_REMOVE_FROM_SERVER_SECS);
List<User> toRemove = new ArrayList<>(); List<User> usersToRemove = new ArrayList<>();
logger.debug("Start Check Expired"); logger.debug("Start Check Expired");
List<User> userList = new ArrayList<>(); List<User> userList = new ArrayList<>();
final Lock r = lock.readLock(); final Lock r = lock.readLock();
@ -213,37 +190,60 @@ public class UserManagerImpl implements UserManager {
} }
for (User user : userList) { for (User user : userList) {
try { try {
if (user.getUserState() != UserState.Offline // expire logic:
&& user.isExpired(calendarInform.getTime())) { // - any user actions will update user's last activity date
long secsInfo = (Calendar.getInstance().getTimeInMillis() - user.getLastActivity().getTime()) / 1000; // - expire code schedules to check last activity every few seconds (minutes)
informUserOpponents(user.getId(), user.getName() + " got connection problem for " + secsInfo + " secs"); // - if something outdated then it will be removed from a server
// - user lifecycle: created -> connected/disconnected (bad connection, bad session) -> offline
boolean isBadConnection = user.isExpired(calInform.getTime());
boolean isBadSession = user.isExpired(calSessionExpire.getTime());
boolean isBadUser = user.isExpired(calUserRemove.getTime());
switch (user.getUserState()) {
case Created: {
// ignore
break;
} }
if (user.getUserState() == UserState.Offline) { case Offline: {
if (user.isExpired(calendarRemove.getTime())) { // remove user from a server (users list for GUI)
// removes from users list if (isBadUser) {
toRemove.add(user); usersToRemove.add(user);
} }
} else { break;
if (user.isExpired(calendarExp.getTime())) {
// set disconnected status and removes from all activities (tourney/tables/games/drafts/chats)
if (user.getUserState() == UserState.Connected) {
user.lostConnection();
disconnect(user.getId(), DisconnectReason.BecameInactive);
} }
removeUserFromAllTablesAndChat(user.getId(), DisconnectReason.SessionExpired);
user.setUserState(UserState.Offline); case Connected:
case Disconnected: {
if (isBadConnection) {
long secsInfo = (Calendar.getInstance().getTimeInMillis() - user.getLastActivity().getTime()) / 1000;
informUserOpponents(user.getId(), String.format("%s catch connection problems for %s secs (left before expire: %d secs)",
user.getName(),
secsInfo,
Math.max(0, USER_CONNECTION_TIMEOUT_SESSION_EXPIRE_AFTER_SECS - secsInfo)
));
}
if (isBadSession) {
// full disconnect
disconnect(user.getId(), DisconnectReason.SessionExpired);
}
break;
}
default: {
throw new IllegalArgumentException("Unknown user state: " + user.getUserState());
} }
} }
} catch (Exception ex) { } catch (Exception ex) {
handleException(ex); handleException(ex);
} }
} }
logger.debug("Users to remove " + toRemove.size()); logger.debug("Users to remove " + usersToRemove.size());
final Lock w = lock.readLock(); final Lock w = lock.writeLock();
w.lock(); w.lock();
try { try {
for (User user : toRemove) { for (User user : usersToRemove) {
users.remove(user.getId()); users.remove(user.getId());
} }
} finally { } finally {
@ -255,6 +255,28 @@ public class UserManagerImpl implements UserManager {
} }
} }
/**
* Remove user instance from a server
* Warning, call it after all tables/chats and other user related data removes
*
* @param userId
*/
@Override
public void removeUser(UUID userId) {
try {
final Lock w = lock.writeLock();
w.lock();
try {
users.remove(userId);
} finally {
w.unlock();
}
} catch (Exception e) {
handleException(e);
}
}
/** /**
* This method recreated the user list that will be sent to all clients * This method recreated the user list that will be sent to all clients
*/ */
@ -322,4 +344,10 @@ public class UserManagerImpl implements UserManager {
} }
}); });
} }
@Override
public void checkHealth() {
//logger.info("Checking users...");
// TODO: add broken users check and report (too long without sessions)
}
} }

View file

@ -52,11 +52,16 @@ public class DraftSession {
if (!killed) { if (!killed) {
Optional<User> user = managerFactory.userManager().getUser(userId); Optional<User> user = managerFactory.userManager().getUser(userId);
if (user.isPresent()) { if (user.isPresent()) {
int remaining;
if (futureTimeout != null && !futureTimeout.isDone()) { if (futureTimeout != null && !futureTimeout.isDone()) {
int remaining = (int) futureTimeout.getDelay(TimeUnit.SECONDS); // picking already runs
remaining = (int) futureTimeout.getDelay(TimeUnit.SECONDS);
} else {
// picking not started yet
remaining = draft.getPickTimeout();
}
user.get().fireCallback(new ClientCallback(ClientCallbackMethod.DRAFT_INIT, draft.getId(), user.get().fireCallback(new ClientCallback(ClientCallbackMethod.DRAFT_INIT, draft.getId(),
new DraftClientMessage(getDraftView(), getDraftPickView(remaining)))); new DraftClientMessage(getDraftView(), getDraftPickView(remaining))));
}
return true; return true;
} }
} }

View file

@ -5,15 +5,13 @@ import mage.abilities.Ability;
import mage.abilities.common.PassAbility; import mage.abilities.common.PassAbility;
import mage.cards.Card; import mage.cards.Card;
import mage.cards.Cards; import mage.cards.Cards;
import mage.cards.decks.Deck;
import mage.cards.decks.DeckCardLists;
import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
import mage.choices.Choice; import mage.choices.Choice;
import mage.constants.ManaType; import mage.constants.ManaType;
import mage.constants.PlayerAction; import mage.constants.PlayerAction;
import mage.constants.Zone; import mage.game.Game;
import mage.game.*; import mage.game.GameOptions;
import mage.game.GameState;
import mage.game.Table;
import mage.game.command.Plane; import mage.game.command.Plane;
import mage.game.events.Listener; import mage.game.events.Listener;
import mage.game.events.PlayerQueryEvent; import mage.game.events.PlayerQueryEvent;
@ -46,7 +44,7 @@ import java.util.stream.Collectors;
import java.util.zip.GZIPOutputStream; import java.util.zip.GZIPOutputStream;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com, JayDi85
*/ */
public class GameController implements GameCallback { public class GameController implements GameCallback {
@ -317,7 +315,6 @@ public class GameController implements GameCallback {
} }
user.get().addGame(playerId, gameSession); user.get().addGame(playerId, gameSession);
logger.debug("Player " + player.getName() + ' ' + playerId + " has " + joinType + " gameId: " + game.getId()); logger.debug("Player " + player.getName() + ' ' + playerId + " has " + joinType + " gameId: " + game.getId());
// TODO: force user update?!
managerFactory.chatManager().broadcast(chatId, "", game.getPlayer(playerId).getLogName() + " has " + joinType + " the game", MessageColor.ORANGE, true, game, MessageType.GAME, null); managerFactory.chatManager().broadcast(chatId, "", game.getPlayer(playerId).getLogName() + " has " + joinType + " the game", MessageColor.ORANGE, true, game, MessageType.GAME, null);
checkStart(); checkStart();
} }
@ -347,7 +344,7 @@ public class GameController implements GameCallback {
} }
private void sendInfoAboutPlayersNotJoinedYetAndTryToFixIt() { private void sendInfoAboutPlayersNotJoinedYetAndTryToFixIt() {
// runs every 5 secs untill all players join // runs every 10 secs untill all players join
for (Player player : game.getPlayers().values()) { for (Player player : game.getPlayers().values()) {
if (player.canRespond() && player.isHuman()) { if (player.canRespond() && player.isHuman()) {
Optional<User> requestedUser = getUserByPlayerId(player.getId()); Optional<User> requestedUser = getUserByPlayerId(player.getId());
@ -358,7 +355,7 @@ public class GameController implements GameCallback {
// join the game because player has not joined or was removed because of disconnect // join the game because player has not joined or was removed because of disconnect
String problemPlayerFixes; String problemPlayerFixes;
user.removeConstructing(player.getId()); user.removeConstructing(player.getId());
managerFactory.gameManager().joinGame(game.getId(), user.getId()); managerFactory.gameManager().joinGame(game.getId(), user.getId()); // will generate new session on empty
logger.warn("Forced join of player " + player.getName() + " (" + user.getUserState() + ") to gameId: " + game.getId()); logger.warn("Forced join of player " + player.getName() + " (" + user.getUserState() + ") to gameId: " + game.getId());
if (user.isConnected()) { if (user.isConnected()) {
// init game session, see reconnect() // init game session, see reconnect()
@ -370,9 +367,8 @@ public class GameController implements GameCallback {
session.init(); session.init();
managerFactory.gameManager().sendPlayerString(session.getGameId(), user.getId(), ""); managerFactory.gameManager().sendPlayerString(session.getGameId(), user.getId(), "");
} else { } else {
problemPlayerFixes = "leave on broken game session"; throw new IllegalStateException("Wrong code usage: session can't be null cause it created in forced joinGame already");
logger.error("Can't find game session for forced join, leave it: player " + player.getName() + " in gameId: " + game.getId()); //player.leave();
player.leave();
} }
} else { } else {
problemPlayerFixes = "leave on disconnected"; problemPlayerFixes = "leave on disconnected";
@ -873,7 +869,7 @@ public class GameController implements GameCallback {
perform(playerId, playerId1 -> getGameSession(playerId1).getMultiAmount(messages, min, max, options)); perform(playerId, playerId1 -> getGameSession(playerId1).getMultiAmount(messages, min, max, options));
} }
private void informOthers(UUID playerId) throws MageException { private void informOthers(UUID playerId) {
StringBuilder message = new StringBuilder(); StringBuilder message = new StringBuilder();
if (game.getStep() != null) { if (game.getStep() != null) {
message.append(game.getTurnStepType().toString()).append(" - "); message.append(game.getTurnStepType().toString()).append(" - ");
@ -889,7 +885,7 @@ public class GameController implements GameCallback {
} }
} }
private void informOthers(List<UUID> players) throws MageException { private void informOthers(List<UUID> players) {
// first player is always original controller // first player is always original controller
Player controller = null; Player controller = null;
if (players != null && !players.isEmpty()) { if (players != null && !players.isEmpty()) {
@ -974,18 +970,19 @@ public class GameController implements GameCallback {
* *
* @param playerId * @param playerId
* @param command * @param command
* @throws MageException
*/ */
private void perform(UUID playerId, Command command) throws MageException { private void perform(UUID playerId, Command command) {
perform(playerId, command, true); perform(playerId, command, true);
} }
private void perform(UUID playerId, Command command, boolean informOthers) throws MageException { private void perform(UUID playerId, Command command, boolean informOthers) {
if (game.getPlayer(playerId).isGameUnderControl()) { // is the player controlling it's own turn if (game.getPlayer(playerId).isGameUnderControl()) { // is the player controlling it's own turn
if (gameSessions.containsKey(playerId)) { if (gameSessions.containsKey(playerId)) {
setupTimeout(playerId); setupTimeout(playerId);
command.execute(playerId); command.execute(playerId);
} }
// TODO: if watcher disconnects then game freezes with active timer, must be fix for such use case
// same for another player (can be fixed by super-duper connection)
if (informOthers) { if (informOthers) {
informOthers(playerId); informOthers(playerId);
} }
@ -997,6 +994,8 @@ public class GameController implements GameCallback {
command.execute(uuid); command.execute(uuid);
} }
} }
// TODO: if watcher disconnects then game freeze with active timer, must be fix for such use case
// same for another player (can be fixed by super-duper connection)
if (informOthers) { if (informOthers) {
informOthers(players); informOthers(players);
} }

View file

@ -31,6 +31,8 @@ public class GamesRoomImpl extends RoomImpl implements GamesRoom, Serializable {
private static final Logger LOGGER = Logger.getLogger(GamesRoomImpl.class); private static final Logger LOGGER = Logger.getLogger(GamesRoomImpl.class);
private static final int MAX_FINISHED_TABLES = 50;
private static final ScheduledExecutorService UPDATE_EXECUTOR = Executors.newSingleThreadScheduledExecutor(); private static final ScheduledExecutorService UPDATE_EXECUTOR = Executors.newSingleThreadScheduledExecutor();
private static List<TableView> tableView = new ArrayList<>(); private static List<TableView> tableView = new ArrayList<>();
private static List<MatchView> matchView = new ArrayList<>(); private static List<MatchView> matchView = new ArrayList<>();
@ -65,7 +67,7 @@ public class GamesRoomImpl extends RoomImpl implements GamesRoom, Serializable {
for (Table table : allTables) { for (Table table : allTables) {
if (table.getState() != TableState.FINISHED) { if (table.getState() != TableState.FINISHED) {
tableList.add(new TableView(table)); tableList.add(new TableView(table));
} else if (matchList.size() < 50) { } else if (matchList.size() < MAX_FINISHED_TABLES) {
matchList.add(new MatchView(table)); matchList.add(new MatchView(table));
} else { } else {
// more since 50 matches finished since this match so removeUserFromAllTablesAndChat it // more since 50 matches finished since this match so removeUserFromAllTablesAndChat it
@ -79,7 +81,8 @@ public class GamesRoomImpl extends RoomImpl implements GamesRoom, Serializable {
matchView = matchList; matchView = matchList;
List<UsersView> users = new ArrayList<>(); List<UsersView> users = new ArrayList<>();
for (User user : managerFactory.userManager().getUsers()) { for (User user : managerFactory.userManager().getUsers()) {
if (user.getUserState() != User.UserState.Offline && !user.getName().equals("Admin")) { if (user.getUserState() != User.UserState.Offline
&& !user.getName().equals(User.ADMIN_NAME)) {
try { try {
users.add(new UsersView(user.getUserData().getFlagName(), user.getName(), users.add(new UsersView(user.getUserData().getFlagName(), user.getName(),
user.getMatchHistory(), user.getMatchQuitRatio(), user.getTourneyHistory(), user.getMatchHistory(), user.getMatchQuitRatio(), user.getTourneyHistory(),
@ -245,11 +248,3 @@ class TableListSorter implements Comparator<Table> {
return 0; return 0;
} }
} }
class UserNameSorter implements Comparator<UsersView> {
@Override
public int compare(UsersView one, UsersView two) {
return one.getUserName().compareToIgnoreCase(two.getUserName());
}
}

View file

@ -10,27 +10,24 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
public interface ChatManager { public interface ChatManager {
UUID createChatSession(String info); UUID createChatSession(String info);
void joinChat(UUID chatId, UUID userId); void joinChat(UUID chatId, UUID userId);
void clearUserMessageStorage();
void leaveChat(UUID chatId, UUID userId); void leaveChat(UUID chatId, UUID userId);
void destroyChatSession(UUID chatId); void destroyChatSession(UUID chatId);
void broadcast(UUID chatId, String userName, String message, ChatMessage.MessageColor color, boolean withTime, Game game, ChatMessage.MessageType messageType, ChatMessage.SoundToPlay soundToPlay); void broadcast(UUID chatId, String userName, String message, ChatMessage.MessageColor color, boolean withTime, Game game, ChatMessage.MessageType messageType, ChatMessage.SoundToPlay soundToPlay);
void broadcast(UUID userId, String message, ChatMessage.MessageColor color) throws UserNotFoundException;
void sendReconnectMessage(UUID userId); void sendReconnectMessage(UUID userId);
void sendLostConnectionMessage(UUID userId, DisconnectReason reason);
void sendMessageToUserChats(UUID userId, String message); void sendMessageToUserChats(UUID userId, String message);
void removeUser(UUID userId, DisconnectReason reason); void removeUser(UUID userId, DisconnectReason reason);
List<ChatSession> getChatSessions(); List<ChatSession> getChatSessions();
void checkHealth();
} }

View file

@ -18,19 +18,22 @@ public interface SessionManager {
boolean registerUser(String sessionId, String userName, String password, String email) throws MageException; boolean registerUser(String sessionId, String userName, String password, String email) throws MageException;
boolean connectUser(String sessionId, String userName, String password, String userIdStr) throws MageException; boolean connectUser(String sessionId, String restoreSessionId, String userName, String password, String userInfo, boolean detailsMode) throws MageException;
boolean connectAdmin(String sessionId); boolean connectAdmin(String sessionId);
boolean setUserData(String userName, String sessionId, UserData userData, String clientVersion, String userIdStr) throws MageException; boolean setUserData(String userName, String sessionId, UserData userData, String clientVersion, String userIdStr) throws MageException;
void disconnect(String sessionId, DisconnectReason reason); /**
* Disconnect from a session side, e.g. on connection error
*
* @param sessionId
* @param reason
* @param checkUserDisconnection check and remove user's tables too, use it by default
*/
void disconnect(String sessionId, DisconnectReason reason, boolean checkUserDisconnection);
void disconnect(String sessionId, DisconnectReason reason, Session directSession); void disconnectAnother(String sessionId, String userSessionId);
void disconnectUser(String sessionId, String userSessionId);
void endUserSession(String sessionId, String userSessionId);
boolean checkAdminAccess(String sessionId); boolean checkAdminAccess(String sessionId);
@ -41,4 +44,6 @@ public interface SessionManager {
boolean extendUserSession(String sessionId, String pingInfo); boolean extendUserSession(String sessionId, String pingInfo);
void sendErrorMessageToClient(String sessionId, String message); void sendErrorMessageToClient(String sessionId, String message);
void checkHealth();
} }

View file

@ -83,4 +83,6 @@ public interface TableManager {
void removeTable(UUID tableId); void removeTable(UUID tableId);
void debugServerState(); void debugServerState();
void checkHealth();
} }

View file

@ -1,7 +1,9 @@
package mage.server.managers; package mage.server.managers;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
public interface ThreadExecutor { public interface ThreadExecutor {
int getActiveThreads(ExecutorService executerService); int getActiveThreads(ExecutorService executerService);
@ -13,4 +15,6 @@ public interface ThreadExecutor {
ScheduledExecutorService getTimeoutExecutor(); ScheduledExecutorService getTimeoutExecutor();
ScheduledExecutorService getTimeoutIdleExecutor(); ScheduledExecutorService getTimeoutIdleExecutor();
ScheduledExecutorService getServerHealthExecutor();
} }

View file

@ -11,6 +11,7 @@ import java.util.Optional;
import java.util.UUID; import java.util.UUID;
public interface UserManager { public interface UserManager {
Optional<User> createUser(String userName, String host, AuthorizedUser authorizedUser); Optional<User> createUser(String userName, String host, AuthorizedUser authorizedUser);
Optional<User> getUser(UUID userId); Optional<User> getUser(UUID userId);
@ -25,12 +26,12 @@ public interface UserManager {
boolean isAdmin(UUID userId); boolean isAdmin(UUID userId);
void removeUserFromAllTablesAndChat(UUID userId, DisconnectReason reason);
void informUserOpponents(UUID userId, String message); void informUserOpponents(UUID userId, String message);
boolean extendUserSession(UUID userId, String pingInfo); boolean extendUserSession(UUID userId, String pingInfo);
void removeUser(UUID userId);
List<UserView> getUserInfoList(); List<UserView> getUserInfoList();
void handleException(Exception ex); void handleException(Exception ex);
@ -38,4 +39,6 @@ public interface UserManager {
String getUserHistory(String userName); String getUserHistory(String userName);
void updateUserHistory(); void updateUserHistory();
void checkHealth();
} }

View file

@ -38,7 +38,7 @@ public enum ServerMessagesUtil {
private static final AtomicInteger gamesEnded = new AtomicInteger(0); private static final AtomicInteger gamesEnded = new AtomicInteger(0);
private static final AtomicInteger tournamentsStarted = new AtomicInteger(0); private static final AtomicInteger tournamentsStarted = new AtomicInteger(0);
private static final AtomicInteger tournamentsEnded = new AtomicInteger(0); private static final AtomicInteger tournamentsEnded = new AtomicInteger(0);
private static final AtomicInteger lostConnection = new AtomicInteger(0); private static final AtomicInteger lostConnection = new AtomicInteger(0); // bad connections only
private static final AtomicInteger reconnects = new AtomicInteger(0); private static final AtomicInteger reconnects = new AtomicInteger(0);
ServerMessagesUtil() { ServerMessagesUtil() {

View file

@ -2,7 +2,7 @@ package mage.server.util;
import mage.server.managers.ConfigSettings; import mage.server.managers.ConfigSettings;
import mage.server.managers.ThreadExecutor; import mage.server.managers.ThreadExecutor;
import mage.utils.ThreadUtils; import mage.util.ThreadUtils;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.util.concurrent.*; import java.util.concurrent.*;
@ -18,6 +18,7 @@ public class ThreadExecutorImpl implements ThreadExecutor {
private final ExecutorService gameExecutor; // game threads to run long tasks, one per game (example: run game and wait user's feedback) private final ExecutorService gameExecutor; // game threads to run long tasks, one per game (example: run game and wait user's feedback)
private final ScheduledExecutorService timeoutExecutor; private final ScheduledExecutorService timeoutExecutor;
private final ScheduledExecutorService timeoutIdleExecutor; private final ScheduledExecutorService timeoutIdleExecutor;
private final ScheduledExecutorService serverHealthExecutor;
/** /**
* noxx: what the settings below do is setting the ability to keep OS * noxx: what the settings below do is setting the ability to keep OS
@ -33,23 +34,27 @@ public class ThreadExecutorImpl implements ThreadExecutor {
public ThreadExecutorImpl(ConfigSettings config) { public ThreadExecutorImpl(ConfigSettings config) {
//callExecutor = Executors.newCachedThreadPool(); //callExecutor = Executors.newCachedThreadPool();
callExecutor = new CachedThreadPoolWithException(); callExecutor = new CachedThreadPoolWithException();
//gameExecutor = Executors.newFixedThreadPool(config.getMaxGameThreads());
gameExecutor = new FixedThreadPoolWithException(config.getMaxGameThreads());
timeoutExecutor = Executors.newScheduledThreadPool(4);
timeoutIdleExecutor = Executors.newScheduledThreadPool(4);
((ThreadPoolExecutor) callExecutor).setKeepAliveTime(60, TimeUnit.SECONDS); ((ThreadPoolExecutor) callExecutor).setKeepAliveTime(60, TimeUnit.SECONDS);
((ThreadPoolExecutor) callExecutor).allowCoreThreadTimeOut(true); ((ThreadPoolExecutor) callExecutor).allowCoreThreadTimeOut(true);
((ThreadPoolExecutor) callExecutor).setThreadFactory(new XMageThreadFactory("CALL")); ((ThreadPoolExecutor) callExecutor).setThreadFactory(new XMageThreadFactory("CALL"));
//gameExecutor = Executors.newFixedThreadPool(config.getMaxGameThreads());
gameExecutor = new FixedThreadPoolWithException(config.getMaxGameThreads());
((ThreadPoolExecutor) gameExecutor).setKeepAliveTime(60, TimeUnit.SECONDS); ((ThreadPoolExecutor) gameExecutor).setKeepAliveTime(60, TimeUnit.SECONDS);
((ThreadPoolExecutor) gameExecutor).allowCoreThreadTimeOut(true); ((ThreadPoolExecutor) gameExecutor).allowCoreThreadTimeOut(true);
((ThreadPoolExecutor) gameExecutor).setThreadFactory(new XMageThreadFactory("GAME")); ((ThreadPoolExecutor) gameExecutor).setThreadFactory(new XMageThreadFactory("GAME"));
timeoutExecutor = Executors.newScheduledThreadPool(4);
((ThreadPoolExecutor) timeoutExecutor).setKeepAliveTime(60, TimeUnit.SECONDS); ((ThreadPoolExecutor) timeoutExecutor).setKeepAliveTime(60, TimeUnit.SECONDS);
((ThreadPoolExecutor) timeoutExecutor).allowCoreThreadTimeOut(true); ((ThreadPoolExecutor) timeoutExecutor).allowCoreThreadTimeOut(true);
((ThreadPoolExecutor) timeoutExecutor).setThreadFactory(new XMageThreadFactory("TIMEOUT")); ((ThreadPoolExecutor) timeoutExecutor).setThreadFactory(new XMageThreadFactory("TIMEOUT"));
timeoutIdleExecutor = Executors.newScheduledThreadPool(4);
((ThreadPoolExecutor) timeoutIdleExecutor).setKeepAliveTime(60, TimeUnit.SECONDS); ((ThreadPoolExecutor) timeoutIdleExecutor).setKeepAliveTime(60, TimeUnit.SECONDS);
((ThreadPoolExecutor) timeoutIdleExecutor).allowCoreThreadTimeOut(true); ((ThreadPoolExecutor) timeoutIdleExecutor).allowCoreThreadTimeOut(true);
((ThreadPoolExecutor) timeoutIdleExecutor).setThreadFactory(new XMageThreadFactory("TIMEOUT_IDLE")); ((ThreadPoolExecutor) timeoutIdleExecutor).setThreadFactory(new XMageThreadFactory("TIMEOUT_IDLE"));
serverHealthExecutor = Executors.newSingleThreadScheduledExecutor(new XMageThreadFactory("HEALTH"));
} }
static class CachedThreadPoolWithException extends ThreadPoolExecutor { static class CachedThreadPoolWithException extends ThreadPoolExecutor {
@ -119,6 +124,10 @@ public class ThreadExecutorImpl implements ThreadExecutor {
return timeoutIdleExecutor; return timeoutIdleExecutor;
} }
@Override
public ScheduledExecutorService getServerHealthExecutor() {
return serverHealthExecutor;
}
} }
class XMageThreadFactory implements ThreadFactory { class XMageThreadFactory implements ThreadFactory {

View file

@ -41,7 +41,12 @@ public class LoadCallbackClient implements CallbackClient {
} }
@Override @Override
public void processCallback(ClientCallback callback) { public void onNewConnection() {
// nothing to do, only one time connection for LoadClient
}
@Override
public void onCallback(ClientCallback callback) {
callback.decompressData(); callback.decompressData();
controlCount = 0; controlCount = 0;

View file

@ -47,7 +47,7 @@ public class LoadTest {
private static final Boolean TEST_SHOW_GAME_LOGS_AS_HTML = false; // html is original format with full data, but can be too bloated private static final Boolean TEST_SHOW_GAME_LOGS_AS_HTML = false; // html is original format with full data, but can be too bloated
private static final String TEST_AI_GAME_MODE = "Freeform Commander Free For All"; private static final String TEST_AI_GAME_MODE = "Freeform Commander Free For All";
private static final String TEST_AI_DECK_TYPE = "Variant Magic - Freeform Commander"; private static final String TEST_AI_DECK_TYPE = "Variant Magic - Freeform Commander";
private static final String TEST_AI_RANDOM_DECK_SETS = "NEO"; // set for random generated decks (empty for all sets usage) private static final String TEST_AI_RANDOM_DECK_SETS = "NEO"; // set for random generated decks (empty for all sets usage, PELP for lands only - communication test)
private static final String TEST_AI_CUSTOM_DECK_PATH_1 = ""; // custom deck file instead random for player 1 (empty for random) private static final String TEST_AI_CUSTOM_DECK_PATH_1 = ""; // custom deck file instead random for player 1 (empty for random)
private static final String TEST_AI_CUSTOM_DECK_PATH_2 = ""; // custom deck file instead random for player 2 (empty for random) private static final String TEST_AI_CUSTOM_DECK_PATH_2 = ""; // custom deck file instead random for player 2 (empty for random)
@ -223,8 +223,8 @@ public class LoadTest {
UUID tableId = game.getTableId(); UUID tableId = game.getTableId();
// deck load // deck load
DeckCardLists deckList1 = loadGameDeck(1, deckColors, false, deckAllowedSets); DeckCardLists deckList1 = loadGameDeck(1, deckColors, deckAllowedSets.equals("PELP"), deckAllowedSets);
DeckCardLists deckList2 = loadGameDeck(2, deckColors, false, deckAllowedSets); DeckCardLists deckList2 = loadGameDeck(2, deckColors, deckAllowedSets.equals("PELP"), deckAllowedSets);
// join AI // join AI
Assert.assertTrue(monitor.session.joinTable(monitor.roomID, tableId, "ai_1", PlayerType.COMPUTER_MAD, 5, deckList1, "")); Assert.assertTrue(monitor.session.joinTable(monitor.roomID, tableId, "ai_1", PlayerType.COMPUTER_MAD, 5, deckList1, ""));
@ -607,7 +607,7 @@ public class LoadTest {
} }
public void disconnect() { public void disconnect() {
this.session.connectStop(false); this.session.connectStop(false, false);
} }
public void concede() { public void concede() {
@ -819,7 +819,7 @@ public class LoadTest {
List<String> data = Arrays.asList( List<String> data = Arrays.asList(
"TOTAL/AVG", //"index", "TOTAL/AVG", //"index",
String.valueOf(this.size()), //"name", String.valueOf(this.size()), //"name",
"", // "random sid", "total, secs: " + String.format("%.3f", (float) this.getTotalDurationMs() / 1000), // "random sid",
String.valueOf(this.getAvgTurn()), // turn String.valueOf(this.getAvgTurn()), // turn
String.valueOf(this.getAvgLife1()), // player 1 String.valueOf(this.getAvgLife1()), // player 1
String.valueOf(this.getAvgLife2()), // player 2 String.valueOf(this.getAvgLife2()), // player 2
@ -841,6 +841,10 @@ public class LoadTest {
return this.values().stream().mapToInt(LoadTestGameResult::getLife2).sum() / this.size(); return this.values().stream().mapToInt(LoadTestGameResult::getLife2).sum() / this.size();
} }
private int getTotalDurationMs() {
return this.values().stream().mapToInt(LoadTestGameResult::getDurationMs).sum();
}
private int getAvgDurationMs() { private int getAvgDurationMs() {
return this.values().stream().mapToInt(LoadTestGameResult::getDurationMs).sum() / this.size(); return this.values().stream().mapToInt(LoadTestGameResult::getDurationMs).sum() / this.size();
} }

View file

@ -54,9 +54,14 @@ public class SimpleMageClient implements MageClient {
} }
@Override @Override
public void processCallback(ClientCallback callback) { public void onNewConnection() {
callbackClient.onNewConnection();
}
@Override
public void onCallback(ClientCallback callback) {
try { try {
callbackClient.processCallback(callback); callbackClient.onCallback(callback);
} catch (Throwable e) { } catch (Throwable e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
} }

View file

@ -18,7 +18,7 @@ import mage.game.permanent.PermanentImpl;
import mage.remote.traffic.ZippedObjectImpl; import mage.remote.traffic.ZippedObjectImpl;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.utils.CompressUtil; import mage.utils.CompressUtil;
import mage.utils.ThreadUtils; import mage.util.ThreadUtils;
import mage.view.GameView; import mage.view.GameView;
import org.jboss.remoting.serialization.impl.java.ClearableObjectOutputStream; import org.jboss.remoting.serialization.impl.java.ClearableObjectOutputStream;
import org.jboss.serial.io.JBossObjectInputStream; import org.jboss.serial.io.JBossObjectInputStream;

View file

@ -45,13 +45,17 @@ public enum PlayerAction {
REQUEST_AUTO_ANSWER_TEXT_NO, REQUEST_AUTO_ANSWER_TEXT_NO,
REQUEST_AUTO_ANSWER_RESET_ALL, REQUEST_AUTO_ANSWER_RESET_ALL,
CLIENT_DOWNLOAD_SYMBOLS, CLIENT_DOWNLOAD_SYMBOLS,
CLIENT_DISCONNECT,
CLIENT_QUIT_TOURNAMENT, CLIENT_QUIT_TOURNAMENT,
CLIENT_QUIT_DRAFT_TOURNAMENT, CLIENT_QUIT_DRAFT_TOURNAMENT,
CLIENT_CONCEDE_GAME, CLIENT_CONCEDE_GAME,
CLIENT_CONCEDE_MATCH, CLIENT_CONCEDE_MATCH,
CLIENT_STOP_WATCHING, CLIENT_STOP_WATCHING,
CLIENT_EXIT,
CLIENT_DISCONNECT_FULL, // send disconnect to server and exit (concede)
CLIENT_DISCONNECT_KEEP_GAMES, // close app only (can re-connect again)
CLIENT_EXIT_FULL,
CLIENT_EXIT_KEEP_GAMES,
CLIENT_REMOVE_TABLE, CLIENT_REMOVE_TABLE,
CLIENT_DOWNLOAD_CARD_IMAGES, CLIENT_DOWNLOAD_CARD_IMAGES,
CLIENT_RECONNECT, CLIENT_RECONNECT,

View file

@ -45,6 +45,8 @@ public interface Draft extends MageItem, Serializable {
void addPlayerQueryEventListener(Listener<PlayerQueryEvent> listener); void addPlayerQueryEventListener(Listener<PlayerQueryEvent> listener);
void firePickCardEvent(UUID playerId); void firePickCardEvent(UUID playerId);
int getPickTimeout();
boolean isAbort(); boolean isAbort();
void setAbort(boolean abort); void setAbort(boolean abort);

View file

@ -313,6 +313,11 @@ public abstract class DraftImpl implements Draft {
@Override @Override
public void firePickCardEvent(UUID playerId) { public void firePickCardEvent(UUID playerId) {
DraftPlayer player = players.get(playerId); DraftPlayer player = players.get(playerId);
playerQueryEventSource.pickCard(playerId, "Pick card", player.getBooster(), getPickTimeout());
}
@Override
public int getPickTimeout() {
int cardNum = Math.min(15, this.cardNum); int cardNum = Math.min(15, this.cardNum);
int time = timing.getPickTimeout(cardNum); int time = timing.getPickTimeout(cardNum);
// if the pack is re-sent to a player because they haven't been able to successfully load it, the pick time is reduced appropriately because of the elapsed time // if the pack is re-sent to a player because they haven't been able to successfully load it, the pick time is reduced appropriately because of the elapsed time
@ -320,7 +325,7 @@ public abstract class DraftImpl implements Draft {
if (time > 0) { if (time > 0) {
time = Math.max(1, time - boosterLoadingCounter * BOOSTER_LOADING_INTERVAL); time = Math.max(1, time - boosterLoadingCounter * BOOSTER_LOADING_INTERVAL);
} }
playerQueryEventSource.pickCard(playerId, "Pick card", player.getBooster(), time); return time;
} }
@Override @Override

View file

@ -11,8 +11,10 @@ import mage.game.events.TableEventSource;
import mage.game.result.ResultProtos.MatchProto; import mage.game.result.ResultProtos.MatchProto;
import mage.game.result.ResultProtos.MatchQuitStatus; import mage.game.result.ResultProtos.MatchQuitStatus;
import mage.players.Player; import mage.players.Player;
import mage.util.CardUtil;
import mage.util.DateFormat; import mage.util.DateFormat;
import mage.util.RandomUtil; import mage.util.RandomUtil;
import mage.util.ThreadUtils;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.util.*; import java.util.*;
@ -318,6 +320,8 @@ public abstract class MatchImpl implements Match {
@Override @Override
public void sideboard() { public void sideboard() {
ThreadUtils.ensureRunInGameThread();
for (MatchPlayer player : this.players) { for (MatchPlayer player : this.players) {
if (!player.hasQuit()) { if (!player.hasQuit()) {
if (player.getDeck() != null) { if (player.getDeck() != null) {

View file

@ -8,6 +8,8 @@ import mage.players.Player;
import mage.players.PlayerType; import mage.players.PlayerType;
import mage.util.DeckBuildUtils; import mage.util.DeckBuildUtils;
import mage.util.TournamentUtil; import mage.util.TournamentUtil;
import org.apache.log4j.Logger;
import java.util.Set; import java.util.Set;
/** /**
@ -15,13 +17,15 @@ import java.util.Set;
*/ */
public class TournamentPlayer { public class TournamentPlayer {
private static final Logger logger = Logger.getLogger(TournamentPlayer.class);
protected int points; protected int points;
protected PlayerType playerType; protected PlayerType playerType;
protected TournamentPlayerState state; protected TournamentPlayerState state;
protected String stateInfo; protected String stateInfo;
protected String disconnectInfo; protected String disconnectInfo;
protected Player player; protected Player player;
protected Deck deck; protected Deck deck = null;
protected String results; protected String results;
protected boolean eliminated = false; protected boolean eliminated = false;
protected boolean quit = false; protected boolean quit = false;
@ -93,6 +97,9 @@ public class TournamentPlayer {
boolean validDeck = (getDeck().getDeckCompleteHashCode() == deck.getDeckCompleteHashCode()); boolean validDeck = (getDeck().getDeckCompleteHashCode() == deck.getDeckCompleteHashCode());
if (!validDeck) { if (!validDeck) {
// Clear the deck so the player cheating looses the game // Clear the deck so the player cheating looses the game
// TODO: inform other players about cheating?!
logger.error("Found cheating player " + getPlayer().getName()
+ " with changed deck, main " + deck.getCards().size() + ", side " + deck.getSideboard().size());
deck.getCards().clear(); deck.getCards().clear();
deck.getSideboard().clear(); deck.getSideboard().clear();
} }
@ -100,11 +107,11 @@ public class TournamentPlayer {
return validDeck; return validDeck;
} }
public Deck generateDeck(int minDeckSize) { /**
/* * If user fails to submit deck on time, submit deck as is if meets minimum size,
If user fails to submit deck on time, submit deck as is if meets minimum size, * else add basic lands per suggested land counts
else add basic lands per suggested land counts
*/ */
public Deck generateDeck(int minDeckSize) {
if (deck.getMaindeckCards().size() < minDeckSize) { if (deck.getMaindeckCards().size() < minDeckSize) {
int[] lands = DeckBuildUtils.landCountSuggestion(minDeckSize, deck.getMaindeckCards()); int[] lands = DeckBuildUtils.landCountSuggestion(minDeckSize, deck.getMaindeckCards());
Set<String> landSets = TournamentUtil.getLandSetCodeForDeckSets(deck.getExpansionSetCodes()); Set<String> landSets = TournamentUtil.getLandSetCodeForDeckSets(deck.getExpansionSetCodes());

View file

@ -1,7 +1,8 @@
package mage.utils; package mage.util;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import javax.swing.*;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future; import java.util.concurrent.Future;
@ -9,7 +10,7 @@ import java.util.concurrent.Future;
/** /**
* Util method to work with threads. * Util method to work with threads.
* *
* @author ayrat * @author ayrat, JayDi85
*/ */
public final class ThreadUtils { public final class ThreadUtils {
@ -31,7 +32,6 @@ public final class ThreadUtils {
/** /**
* Find real exception object after thread task completed. Can be used in afterExecute * Find real exception object after thread task completed. Can be used in afterExecute
*
*/ */
public static Throwable findRunnableException(Runnable r, Throwable t) { public static Throwable findRunnableException(Runnable r, Throwable t) {
// executer.submit - return exception in result // executer.submit - return exception in result
@ -53,4 +53,28 @@ public final class ThreadUtils {
public static Throwable findRootException(Throwable t) { public static Throwable findRootException(Throwable t) {
return Throwables.getRootCause(t); return Throwables.getRootCause(t);
} }
public static void ensureRunInGameThread() {
String name = Thread.currentThread().getName();
if (!name.startsWith("GAME")) {
// how-to fix: use signal logic to inform a game about new command to execute instead direct execute (see example with WantConcede)
// reason: user responses/commands are received by network/call thread, but must be processed by game thread
throw new IllegalArgumentException("Wrong code usage: game related code must run in GAME thread, but it used in " + name, new Throwable());
}
}
public static void ensureRunInCallThread() {
String name = Thread.currentThread().getName();
if (!name.startsWith("CALL")) {
// how-to fix: something wrong in your code logic
throw new IllegalArgumentException("Wrong code usage: client commands code must run in CALL threads, but used in " + name, new Throwable());
}
}
public static void ensureRunInGUISwingThread() {
if (!SwingUtilities.isEventDispatchThread()) {
// hot-to fix: run GUI changeable code by SwingUtilities.invokeLater(() -> {xxx})
throw new IllegalArgumentException("Wrong code usage: GUI related code must run in SWING thread by SwingUtilities.invokeLater", new Throwable());
}
}
} }