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);

View file

@ -3,21 +3,19 @@
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
*/
public abstract class MagePane extends javax.swing.JLayeredPane {
private String title = "no title set";
/**
* Creates new form MagePane
*/
public MagePane() {
initComponents();
}
public void changeGUISize() {
}
public void setTitle(String title) {
@ -53,6 +51,12 @@
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.
* WARNING: Do NOT modify this code. The content of this method is always

View file

@ -5,6 +5,7 @@ import static mage.cards.decks.DeckFormats.XMAGE;
import mage.client.chat.LocalCommands;
import mage.client.constants.Constants.DeckEditorMode;
import mage.client.dialog.PreferencesDialog;
import mage.client.preference.MagePreferences;
import mage.constants.ManaType;
import mage.constants.PlayerAction;
import mage.game.match.MatchOptions;
@ -41,7 +42,6 @@ public final class SessionHandler {
}
public static void startSession(MageFrame mageFrame) {
session = new SessionImpl(mageFrame);
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) {
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)) {
// save current session for restore
MagePreferences.saveRestoreSession(connection.getHost(), connection.getUsername(), getSessionId());
return true;
} else {
lastConnectError = session.getLastError();
disconnect(false, true);
return false;
}
}
@ -80,8 +92,16 @@ public final class SessionHandler {
return session.connectAbort();
}
public static void disconnect(boolean showmessage) {
session.connectStop(showmessage);
public static void disconnect(boolean askForReconnect, boolean keepMySessionActive) {
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) {

View file

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

View file

@ -37,7 +37,7 @@ public final class LocalCommands {
return false;
}
final String serverAddress = SessionHandler.getSession().getServerHostname().orElse("");
final String serverAddress = SessionHandler.getSession().getServerHost();
Optional<String> response = Optional.empty();
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();
ClientCallback chatMessage = new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId,
new ChatMessage("", text, new Date(), null, ChatMessage.MessageColor.BLUE));
MageFrame.getInstance().processCallback(chatMessage);
MageFrame.getInstance().onCallback(chatMessage);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,8 @@ import javax.swing.plaf.basic.BasicInternalFrameUI;
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
*/
@ -74,6 +75,17 @@ public class UserRequestDialog extends MageDialog {
} else {
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.revalidate();
this.repaint();
@ -177,7 +189,7 @@ public class UserRequestDialog extends MageDialog {
}//GEN-LAST:event_btn3ActionPerformed
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

View file

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

View file

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

View file

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

View file

@ -1469,6 +1469,10 @@ public final class GamePanel extends javax.swing.JPanel {
this.feedbackPanel.prepareFeedback(FeedbackMode.QUESTION, question, false, options, true, gameView.getPhase());
}
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) {
lastGameData.messageId = messageId;
lastGameData.setNewGame(game);

View file

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

View file

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

View file

@ -4,6 +4,7 @@ import com.google.common.collect.Sets;
import mage.client.MageFrame;
import mage.client.util.ClientDefaultSettings;
import java.util.Date;
import java.util.Set;
import java.util.prefs.BackingStoreException;
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_AUTO_CONNECT = "autoConnect";
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 int lastServerPort = 0;
@ -64,6 +66,7 @@ public final class MagePreferences {
return userName;
}
// 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, "");
}
@ -143,6 +146,19 @@ public final class MagePreferences {
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() {
lastServerAddress = getServerAddressWithDefault(ClientDefaultSettings.serverName);
lastServerPort = getServerPortWithDefault(ClientDefaultSettings.port);

View file

@ -28,31 +28,52 @@ import java.awt.event.KeyEvent;
import java.util.*;
/**
* @author BetaSteward_at_googlemail.com
* @author BetaSteward_at_googlemail.com, JayDi85
*/
public class CallbackClientImpl implements CallbackClient {
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 Map<ClientCallbackType, Integer> lastMessages;
private final Map<UUID, GameClientMessage> firstGameData;
public CallbackClientImpl(MageFrame frame) {
this.frame = frame;
this.lastMessages = new HashMap<>();
this.firstGameData = new HashMap<>();
Arrays.stream(ClientCallbackType.values()).forEach(t -> this.lastMessages.put(t, 0));
}
@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();
// put replay related code here
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
SwingUtilities.invokeLater(() -> {
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)
// - 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();
GameManager.instance.setCurrentPlayerUUID(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;
}
@ -101,12 +140,6 @@ public class CallbackClientImpl implements CallbackClient {
break;
}
case START_DRAFT: {
TableClientMessage message = (TableClientMessage) callback.getData();
draftStarted(callback.getMessageId(), message.getGameId(), message.getPlayerId());
break;
}
case REPLAY_GAME: {
replayGame(callback.getObjectId());
break;
@ -126,7 +159,7 @@ public class CallbackClientImpl implements CallbackClient {
ChatMessage message = (ChatMessage) callback.getData();
// Drop messages from ignored users
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())) {
break;
}
@ -426,6 +459,12 @@ public class CallbackClientImpl implements CallbackClient {
break;
}
case START_DRAFT: {
TableClientMessage message = (TableClientMessage) callback.getData();
draftStarted(callback.getMessageId(), message.getGameId(), message.getPlayerId());
break;
}
case DRAFT_OVER: {
MageFrame.removeDraft(callback.getObjectId());
break;
@ -544,7 +583,7 @@ public class CallbackClientImpl implements CallbackClient {
null, null, MessageType.USER_INFO, ChatMessage.MessageColor.BLUE);
break;
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.")
.append("<br/>Download icons and symbols by using the \"Symbols\" main menu.")
.append("<br/>\\list - show a list of available chat commands.")

View file

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

View file

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

View file

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

View file

@ -53,15 +53,15 @@ public final class IgnoreList {
}
MagePreferences.addIgnoredUser(serverAddress, user);
updateTablesTable();
updateServerLobbyTables();
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();
if (mageFrame != null) {
mageFrame.setTableFilter();
mageFrame.setServerLobbyTablesFilter();
}
}
@ -70,7 +70,7 @@ public final class IgnoreList {
return usage(serverAddress);
}
if (MagePreferences.removeIgnoredUser(serverAddress, user)) {
updateTablesTable();
updateServerLobbyTables();
return "Removed " + user + " from your ignore list on " + serverAddress + " (total: " + getIgnoredUsers(serverAddress).size() + ")";
} else {
return "No such user \"" + user + "\" on your ignore list on " + serverAddress + " (total: " + getIgnoredUsers(serverAddress).size() + ")";

View file

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