Network upgrade and new reconnection mode (#11527)

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

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

View file

@ -124,6 +124,9 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
private static int startPort = -1;
private static 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, GamePanel> GAMES = 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 {
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
@ -283,6 +283,23 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
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());
setSize(1024, 768);
@ -303,8 +320,12 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
updateMemUsageTask = new UpdateMemUsageTask(jMemUsageLabel);
// create default server lobby and hide it until connect
tablesPane = new TablesPane();
desktopPane.add(tablesPane, javax.swing.JLayeredPane.DEFAULT_LAYER);
SwingUtilities.invokeLater(() -> {
this.hideServerLobby();
});
addTooltipContainer();
setBackground();
@ -565,15 +586,27 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
}
private AbstractButton createSwitchPanelsButton() {
final JToggleButton switchPanelsButton = new JToggleButton("Switch panels");
switchPanelsButton.addItemListener(e -> {
this.switchPanelsButton = new JToggleButton(SWITCH_PANELS_BUTTON_NAME);
this.switchPanelsButton.addItemListener(e -> {
if (e.getStateChange() == ItemEvent.SELECTED) {
createAndShowSwitchPanelsMenu((JComponent) e.getSource(), switchPanelsButton);
createAndShowSwitchPanelsMenu((JComponent) e.getSource(), this.switchPanelsButton);
}
});
switchPanelsButton.setFocusable(false);
switchPanelsButton.setHorizontalTextPosition(SwingConstants.LEADING);
return switchPanelsButton;
this.switchPanelsButton.setFocusable(false);
this.switchPanelsButton.setHorizontalTextPosition(SwingConstants.LEADING);
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) {
@ -762,17 +795,27 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
}
public void showTournament(UUID tournamentId) {
// existing tourney
TournamentPane tournamentPane = null;
for (Component component : desktopPane.getComponents()) {
if (component instanceof TournamentPane
&& ((TournamentPane) component).getTournamentId().equals(tournamentId)) {
setActive((TournamentPane) component);
return;
tournamentPane = (TournamentPane) component;
}
}
TournamentPane tournamentPane = new TournamentPane();
desktopPane.add(tournamentPane, JLayeredPane.DEFAULT_LAYER);
tournamentPane.setVisible(true);
tournamentPane.showTournament(tournamentId);
// new tourney
if (tournamentPane == null) {
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);
}
@ -843,7 +886,7 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
LOGGER.debug("connecting (auto): " + currentConnection.getProxyType().toString()
+ ' ' + currentConnection.getProxyHost() + ' ' + currentConnection.getProxyPort() + ' ' + currentConnection.getProxyUsername());
if (MageFrame.connect(currentConnection)) {
prepareAndShowTablesPane();
prepareAndShowServerLobby();
return true;
} else {
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
if (SessionHandler.isConnected()) {
UserRequestMessage message = new UserRequestMessage("Confirm disconnect", "Are you sure you want to disconnect?");
message.setButton1("No", null);
message.setButton2("Yes", PlayerAction.CLIENT_DISCONNECT);
showUserRequestDialog(message);
tryDisconnectOrExit(false);
} else {
connectDialog.showDialog();
setWindowTitle();
@ -1148,15 +1188,34 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
}
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()) {
UserRequestMessage message = new UserRequestMessage("Confirm disconnect", "You are currently connected. Are you sure you want to disconnect?");
message.setButton1("No", null);
message.setButton2("Yes", PlayerAction.CLIENT_EXIT);
int activeTables = MageFrame.getInstance().getPanelsCount(true);
UserRequestMessage message = new UserRequestMessage(
"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);
} else {
UserRequestMessage message = new UserRequestMessage("Confirm exit", "Are you sure you want to exit?");
message.setButton1("No", null);
message.setButton2("Yes", PlayerAction.CLIENT_EXIT);
UserRequestMessage message = new UserRequestMessage(
"Confirm " + actionName,
"Are you sure you want to " + actionName + "?"
);
message.setButton1("Cancel", null);
message.setButton2("Yes", actionFull);
message.setWindowSizeRatio(windowSizeRatio);
MageFrame.getInstance().showUserRequestDialog(message);
}
}
@ -1171,17 +1230,18 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
btnDeckEditor.setEnabled(true);
}
public void hideTables() {
public void hideServerLobby() {
this.tablesPane.hideTables();
updateSwitchPanelsButton();
}
public void setTableFilter() {
public void setServerLobbyTablesFilter() {
if (this.tablesPane != null) {
this.tablesPane.setTableFilter();
}
}
public void prepareAndShowTablesPane() {
public void prepareAndShowServerLobby() {
// Update the tables pane with the new session
this.tablesPane.showTables();
@ -1191,6 +1251,8 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
if (topPanebefore != null && topPanebefore != tablesPane) {
setActive(topPanebefore);
}
updateSwitchPanelsButton();
}
public void hideGames() {
@ -1253,14 +1315,18 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
}
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);
desktopPane.add(userRequestDialog, JLayeredPane.MODAL_LAYER);
if (SwingUtilities.isEventDispatchThread()) {
userRequestDialog.showDialog(userRequestMessage);
} else {
SwingUtilities.invokeLater(() -> userRequestDialog.showDialog(userRequestMessage));
}
userRequestDialog.showDialog(userRequestMessage);
}
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);
}
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
public void connected(final String message) {
if (SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(() -> {
setConnectButtonText(message);
enableButtons();
} else {
SwingUtilities.invokeLater(() -> {
setConnectButtonText(message);
enableButtons();
});
}
});
}
@Override
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
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);
disableButtons();
hideGames();
hideTables();
} else {
// USER mode, e.g. user plays and got disconnect
LOGGER.info("Disconnected from user mode");
SwingUtilities.invokeLater(() -> {
SessionHandler.disconnect(false); // user already disconnected, can't do any online actions like quite chat
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);
}
}
);
}
hideServerLobby();
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
@ -1539,8 +1610,13 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
}
@Override
public void processCallback(ClientCallback callback) {
callbackClient.processCallback(callback);
public void onCallback(ClientCallback callback) {
callbackClient.onCallback(callback);
}
@Override
public void onNewConnection() {
callbackClient.onNewConnection();
}
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:
DownloadPicturesService.startDownload();
break;
case CLIENT_DISCONNECT:
if (SessionHandler.isConnected()) {
SessionHandler.disconnect(false);
}
tablesPane.clearChat();
showMessage("You have disconnected");
setWindowTitle();
case CLIENT_DISCONNECT_FULL:
doClientDisconnect(false, "You have disconnected");
break;
case CLIENT_DISCONNECT_KEEP_GAMES:
doClientDisconnect(true, "You have disconnected and have few minutes to reconnect");
break;
case CLIENT_QUIT_TOURNAMENT:
SessionHandler.quitTournament(userRequestMessage.getTournamentId());
@ -1580,15 +1654,13 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
}
removeGame(userRequestMessage.getGameId());
break;
case CLIENT_EXIT:
if (SessionHandler.isConnected()) {
SessionHandler.disconnect(false);
}
CardRepository.instance.closeDB();
tablesPane.cleanUp();
Plugins.instance.shutdown();
dispose();
System.exit(0);
case CLIENT_EXIT_FULL:
doClientDisconnect(false, "");
doClientShutdownAndExit();
break;
case CLIENT_EXIT_KEEP_GAMES:
doClientDisconnect(true, "");
doClientShutdownAndExit();
break;
case CLIENT_REMOVE_TABLE:
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() {
for (UUID gameId : GAMES.keySet()) {
SessionHandler.quitMatch(gameId);