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

@ -45,13 +45,17 @@ public enum PlayerAction {
REQUEST_AUTO_ANSWER_TEXT_NO,
REQUEST_AUTO_ANSWER_RESET_ALL,
CLIENT_DOWNLOAD_SYMBOLS,
CLIENT_DISCONNECT,
CLIENT_QUIT_TOURNAMENT,
CLIENT_QUIT_DRAFT_TOURNAMENT,
CLIENT_CONCEDE_GAME,
CLIENT_CONCEDE_MATCH,
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_DOWNLOAD_CARD_IMAGES,
CLIENT_RECONNECT,

View file

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

View file

@ -313,6 +313,11 @@ public abstract class DraftImpl implements Draft {
@Override
public void firePickCardEvent(UUID 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 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
@ -320,7 +325,7 @@ public abstract class DraftImpl implements Draft {
if (time > 0) {
time = Math.max(1, time - boosterLoadingCounter * BOOSTER_LOADING_INTERVAL);
}
playerQueryEventSource.pickCard(playerId, "Pick card", player.getBooster(), time);
return time;
}
@Override

View file

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

View file

@ -8,6 +8,8 @@ import mage.players.Player;
import mage.players.PlayerType;
import mage.util.DeckBuildUtils;
import mage.util.TournamentUtil;
import org.apache.log4j.Logger;
import java.util.Set;
/**
@ -15,13 +17,15 @@ import java.util.Set;
*/
public class TournamentPlayer {
private static final Logger logger = Logger.getLogger(TournamentPlayer.class);
protected int points;
protected PlayerType playerType;
protected TournamentPlayerState state;
protected String stateInfo;
protected String disconnectInfo;
protected Player player;
protected Deck deck;
protected Deck deck = null;
protected String results;
protected boolean eliminated = false;
protected boolean quit = false;
@ -93,6 +97,9 @@ public class TournamentPlayer {
boolean validDeck = (getDeck().getDeckCompleteHashCode() == deck.getDeckCompleteHashCode());
if (!validDeck) {
// 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.getSideboard().clear();
}
@ -100,11 +107,11 @@ public class TournamentPlayer {
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) {
/*
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) {
int[] lands = DeckBuildUtils.landCountSuggestion(minDeckSize, deck.getMaindeckCards());
Set<String> landSets = TournamentUtil.getLandSetCodeForDeckSets(deck.getExpansionSetCodes());

View file

@ -0,0 +1,80 @@
package mage.util;
import com.google.common.base.Throwables;
import javax.swing.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/**
* Util method to work with threads.
*
* @author ayrat, JayDi85
*/
public final class ThreadUtils {
public static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException ignored) {
}
}
public static void wait(Object lock) {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException ex) {
}
}
}
/**
* Find real exception object after thread task completed. Can be used in afterExecute
*/
public static Throwable findRunnableException(Runnable r, Throwable t) {
// executer.submit - return exception in result
// executer.execute - return exception in t
if (t == null && r instanceof Future<?>) {
try {
Object result = ((Future<?>) r).get();
} catch (CancellationException ce) {
t = ce;
} catch (ExecutionException ee) {
t = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // ignore/reset
}
}
return t;
}
public static Throwable findRootException(Throwable 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());
}
}
}