forked from External/mage
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:
parent
7f0558ff3c
commit
960e896903
71 changed files with 1274 additions and 802 deletions
|
|
@ -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();
|
|
||||||
desktopPane.add(tournamentPane, JLayeredPane.DEFAULT_LAYER);
|
// new tourney
|
||||||
tournamentPane.setVisible(true);
|
if (tournamentPane == null) {
|
||||||
tournamentPane.showTournament(tournamentId);
|
tournamentPane = new TournamentPane();
|
||||||
|
desktopPane.add(tournamentPane, JLayeredPane.DEFAULT_LAYER);
|
||||||
|
tournamentPane.setVisible(true);
|
||||||
|
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()) {
|
SwingUtilities.invokeLater(() -> {
|
||||||
setConnectButtonText(message);
|
setConnectButtonText(message);
|
||||||
enableButtons();
|
enableButtons();
|
||||||
} else {
|
});
|
||||||
SwingUtilities.invokeLater(() -> {
|
|
||||||
setConnectButtonText(message);
|
|
||||||
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");
|
||||||
|
} else {
|
||||||
|
// USER mode, e.g. user plays and got disconnect
|
||||||
|
LOGGER.info("Disconnected from client side");
|
||||||
|
}
|
||||||
|
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
// 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();
|
||||||
} else {
|
if (askToReconnect) {
|
||||||
// USER mode, e.g. user plays and got disconnect
|
UserRequestMessage message = new UserRequestMessage("Connection lost", "The connection to server was lost. Reconnect to " + MagePreferences.getLastServerAddress() + "?");
|
||||||
LOGGER.info("Disconnected from user mode");
|
message.setButton1("No", null);
|
||||||
SwingUtilities.invokeLater(() -> {
|
message.setButton2("Yes", PlayerAction.CLIENT_RECONNECT);
|
||||||
SessionHandler.disconnect(false); // user already disconnected, can't do any online actions like quite chat
|
showUserRequestDialog(message);
|
||||||
setConnectButtonText(NOT_CONNECTED_BUTTON);
|
}
|
||||||
disableButtons();
|
});
|
||||||
hideGames();
|
|
||||||
hideTables();
|
|
||||||
if (askToReconnect) {
|
|
||||||
UserRequestMessage message = new UserRequestMessage("Connection lost", "The connection to server was lost. Reconnect to " + MagePreferences.getLastServerAddress() + "?");
|
|
||||||
message.setButton1("No", null);
|
|
||||||
message.setButton2("Yes", PlayerAction.CLIENT_RECONNECT);
|
|
||||||
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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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")) {
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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.")
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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() + ")";
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -984,7 +1011,7 @@ public class SessionImpl implements Session {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean setBoosterLoaded(UUID draftId) {
|
public boolean setBoosterLoaded(UUID draftId) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -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,38 +1702,40 @@ 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) {
|
||||||
long startTime = System.nanoTime();
|
return;
|
||||||
if (!server.ping(sessionId, pingInfo)) {
|
|
||||||
logger.error("Ping failed: " + this.getUserName() + " Session: " + sessionId + " to MAGE server at " + connection.getHost() + ':' + connection.getPort());
|
|
||||||
throw new MageException("Ping failed");
|
|
||||||
}
|
|
||||||
pingTime.add(System.nanoTime() - startTime);
|
|
||||||
long milliSeconds = TimeUnit.MILLISECONDS.convert(pingTime.getLast(), TimeUnit.NANOSECONDS);
|
|
||||||
String lastPing = milliSeconds > 0 ? milliSeconds + "ms" : "<1ms";
|
|
||||||
if (pingTime.size() > PING_CYCLES) {
|
|
||||||
pingTime.poll();
|
|
||||||
}
|
|
||||||
long sum = 0;
|
|
||||||
for (Long time : pingTime) {
|
|
||||||
sum += time;
|
|
||||||
}
|
|
||||||
milliSeconds = TimeUnit.MILLISECONDS.convert(sum / pingTime.size(), TimeUnit.NANOSECONDS);
|
|
||||||
pingInfo = lastPing + " (avg: " + (milliSeconds > 0 ? milliSeconds + "ms" : "<1ms") + ')';
|
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
|
long startTime = System.nanoTime();
|
||||||
|
if (!server.ping(sessionId, lastPingInfo)) {
|
||||||
|
logger.error("Ping failed: " + this.getUserName() + " Session: " + sessionId + " to MAGE server at " + connection.getHost() + ':' + connection.getPort());
|
||||||
|
throw new MageException("Ping failed");
|
||||||
|
}
|
||||||
|
pingTime.add(System.nanoTime() - startTime);
|
||||||
|
long milliSeconds = TimeUnit.MILLISECONDS.convert(pingTime.getLast(), TimeUnit.NANOSECONDS);
|
||||||
|
String lastPing = milliSeconds > 0 ? milliSeconds + "ms" : "<1ms";
|
||||||
|
if (pingTime.size() > PING_CYCLES) {
|
||||||
|
pingTime.poll();
|
||||||
|
}
|
||||||
|
long sum = 0;
|
||||||
|
for (Long time : pingTime) {
|
||||||
|
sum += time;
|
||||||
|
}
|
||||||
|
milliSeconds = TimeUnit.MILLISECONDS.convert(sum / pingTime.size(), TimeUnit.NANOSECONDS);
|
||||||
|
lastPingInfo = lastPing + " (avg: " + (milliSeconds > 0 ? milliSeconds + "ms" : "<1ms") + ')';
|
||||||
} 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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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()) {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
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();
|
|
||||||
w.lock();
|
|
||||||
try {
|
|
||||||
clients.remove(userId);
|
|
||||||
} finally {
|
|
||||||
w.unlock();
|
|
||||||
}
|
|
||||||
logger.debug(userName + '(' + reason.toString() + ')' + " removed from chatId " + chatId);
|
|
||||||
}
|
|
||||||
String message = reason.getMessage();
|
|
||||||
|
|
||||||
if (!message.isEmpty()) {
|
// remove from chat
|
||||||
broadcast(null, userName + message, MessageColor.BLUE, true, null, MessageType.STATUS, null);
|
final Lock w = lock.writeLock();
|
||||||
}
|
w.lock();
|
||||||
|
try {
|
||||||
|
users.remove(userId);
|
||||||
|
} finally {
|
||||||
|
w.unlock();
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
logger.debug(userName + " (" + reason + ')' + " removed from chatId " + chatId);
|
||||||
logger.fatal("exception: " + ex.toString());
|
|
||||||
|
// inform other users about disconnect (lobby, system tab)
|
||||||
|
if (!reason.messageForUser.isEmpty()) {
|
||||||
|
broadcast(null, userName + reason.messageForUser, MessageColor.BLUE, true, null, MessageType.STATUS, null);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.fatal("Chat: disconnecting user catch error: " + e, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
StringBuilder sessionInfo = new StringBuilder();
|
|
||||||
Optional<User> user = managerFactory.userManager().getUser(userId);
|
|
||||||
if (user.isPresent()) {
|
|
||||||
sessionInfo.append(user.get().getName()).append(" [").append(user.get().getGameInfo()).append(']');
|
|
||||||
} else {
|
|
||||||
sessionInfo.append("[user missing] ");
|
|
||||||
}
|
|
||||||
sessionInfo.append(" at ").append(session.get().getHost()).append(" sessionId: ").append(session.get().getId());
|
|
||||||
if (throwable instanceof ClientDisconnectedException) {
|
|
||||||
// Seems like the random diconnects from public server land here and should not be handled as explicit disconnects
|
|
||||||
// So it should be possible to reconnect to server and continue games if DisconnectReason is set to LostConnection
|
|
||||||
//managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.Disconnected);
|
|
||||||
managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.LostConnection);
|
|
||||||
logger.info("CLIENT DISCONNECTED - " + sessionInfo);
|
|
||||||
logger.debug("Stack Trace", throwable);
|
|
||||||
} else {
|
|
||||||
managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.LostConnection);
|
|
||||||
logger.info("LOST CONNECTION - " + sessionInfo);
|
|
||||||
if (logger.isDebugEnabled()) {
|
|
||||||
if (throwable == null) {
|
|
||||||
logger.debug("- cause: Lease expired");
|
|
||||||
} else {
|
|
||||||
logger.debug(" - cause: " + Session.getBasicCause(throwable).toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fill by user's info
|
||||||
|
StringBuilder sessionInfo = new StringBuilder();
|
||||||
|
User user = managerFactory.userManager().getUser(session.getUserId()).orElse(null);
|
||||||
|
if (user != null) {
|
||||||
|
sessionInfo.append(user.getName()).append(" [").append(user.getGameInfo()).append(']');
|
||||||
|
} else {
|
||||||
|
sessionInfo.append("[no user]");
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
// client called a disconnect command (full disconnect without tables keep)
|
||||||
|
// no need to keep session
|
||||||
|
logger.info("CLIENT DISCONNECTED - " + sessionInfo);
|
||||||
|
logger.debug("- cause: client called disconnect command");
|
||||||
|
managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.DisconnectedByUser, true);
|
||||||
|
} else if (throwable == null) {
|
||||||
|
// lease timeout (ping), so server lost connection with a client
|
||||||
|
// must keep tables
|
||||||
|
logger.info("LOST CONNECTION - " + sessionInfo);
|
||||||
|
logger.debug("- cause: lease expired");
|
||||||
|
managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.LostConnection, true);
|
||||||
|
} else {
|
||||||
|
// 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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -119,7 +145,7 @@ public class Session {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String validateUserNameLength(String userName) {
|
private String validateUserNameLength(String userName) {
|
||||||
ConfigSettings config = managerFactory.configSettings();
|
ConfigSettings config = managerFactory.configSettings();
|
||||||
if (userName.length() < config.getMinUserNameLength()) {
|
if (userName.length() < config.getMinUserNameLength()) {
|
||||||
|
|
@ -138,12 +164,10 @@ 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);
|
||||||
if (returnMessage != null) {
|
if (returnMessage != null) {
|
||||||
return returnMessage;
|
return returnMessage;
|
||||||
|
|
@ -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
|
||||||
managerFactory.chatManager().joinChat(room.get().getChatId(), userId);
|
logger.warn("main room not found"); // after server restart users try to use old rooms on reconnect for some reason
|
||||||
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();
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
return;
|
||||||
@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;
|
|
||||||
}
|
|
||||||
logger.debug("DISCONNECT " + reason.toString() + " - sessionId: " + sessionId);
|
|
||||||
sessions.remove(sessionId);
|
|
||||||
switch (reason) {
|
|
||||||
case AdminDisconnect:
|
|
||||||
session.kill(reason);
|
|
||||||
break;
|
|
||||||
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 (!isValidSession(sessionId)) {
|
||||||
|
logger.info("DISCONNECT session, already invalid: " + reason + " - sessionId: " + sessionId);
|
||||||
|
// session was removed meanwhile by another thread
|
||||||
|
// TODO: otudated code? If no logs on server then remove that code, 2023-12-06
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
user.showUserMessage("Admin action", "Your session was disconnected by admin");
|
||||||
admin.showUserMessage("Admin result", "User with sessionId " + userSessionId + " could not be found");
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
public void setRestoreSessionId(String restoreSessionId) {
|
||||||
setUserState(UserState.Connected);
|
this.restoreSessionId = restoreSessionId;
|
||||||
logger.trace("USER - created: " + userName + " id: " + userId);
|
|
||||||
} else {
|
|
||||||
setUserState(UserState.Connected);
|
|
||||||
reconnect();
|
|
||||||
logger.trace("USER - reconnected: " + userName + " id: " + userId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,9 +345,7 @@ 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) {
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
if (user.getUserState() == UserState.Offline) {
|
boolean isBadConnection = user.isExpired(calInform.getTime());
|
||||||
if (user.isExpired(calendarRemove.getTime())) {
|
boolean isBadSession = user.isExpired(calSessionExpire.getTime());
|
||||||
// removes from users list
|
boolean isBadUser = user.isExpired(calUserRemove.getTime());
|
||||||
toRemove.add(user);
|
|
||||||
|
switch (user.getUserState()) {
|
||||||
|
case Created: {
|
||||||
|
// ignore
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (user.isExpired(calendarExp.getTime())) {
|
case Offline: {
|
||||||
// set disconnected status and removes from all activities (tourney/tables/games/drafts/chats)
|
// remove user from a server (users list for GUI)
|
||||||
if (user.getUserState() == UserState.Connected) {
|
if (isBadUser) {
|
||||||
user.lostConnection();
|
usersToRemove.add(user);
|
||||||
disconnect(user.getId(), DisconnectReason.BecameInactive);
|
|
||||||
}
|
}
|
||||||
removeUserFromAllTablesAndChat(user.getId(), DisconnectReason.SessionExpired);
|
break;
|
||||||
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
user.get().fireCallback(new ClientCallback(ClientCallbackMethod.DRAFT_INIT, draft.getId(),
|
remaining = (int) futureTimeout.getDelay(TimeUnit.SECONDS);
|
||||||
new DraftClientMessage(getDraftView(), getDraftPickView(remaining))));
|
} else {
|
||||||
|
// picking not started yet
|
||||||
|
remaining = draft.getPickTimeout();
|
||||||
}
|
}
|
||||||
|
user.get().fireCallback(new ClientCallback(ClientCallbackMethod.DRAFT_INIT, draft.getId(),
|
||||||
|
new DraftClientMessage(getDraftView(), getDraftPickView(remaining))));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
@ -868,12 +864,12 @@ public class GameController implements GameCallback {
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void multiAmount(UUID playerId, final List<MultiAmountMessage> messages,
|
private synchronized void multiAmount(UUID playerId, final List<MultiAmountMessage> messages,
|
||||||
final int min, final int max, final Map<String, Serializable> options)
|
final int min, final int max, final Map<String, Serializable> options)
|
||||||
throws MageException {
|
throws MageException {
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
@ -244,12 +247,4 @@ 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,4 +83,6 @@ public interface TableManager {
|
||||||
void removeTable(UUID tableId);
|
void removeTable(UUID tableId);
|
||||||
|
|
||||||
void debugServerState();
|
void debugServerState();
|
||||||
|
|
||||||
|
void checkHealth();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If user fails to submit deck on time, submit deck as is if meets minimum size,
|
||||||
|
* else add basic lands per suggested land counts
|
||||||
|
*/
|
||||||
public Deck generateDeck(int minDeckSize) {
|
public Deck generateDeck(int minDeckSize) {
|
||||||
/*
|
|
||||||
If user fails to submit deck on time, submit deck as is if meets minimum size,
|
|
||||||
else add basic lands per suggested land counts
|
|
||||||
*/
|
|
||||||
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());
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue