From 7817a5cac6b11d80f961efaaf50a96d44ab62d41 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Wed, 10 Apr 2024 22:18:07 +0400 Subject: [PATCH] deck improves: * gui: removed public deck hash info; * gui: improved xmage dck-file - now it correctly load a card's amount (related to files from third party services); * server: fixed wrong cheating warning on deck construction (closes #11877); * refactor: removed outdated hash code and calculations; * other: added docs, added multiple deck hash tests; --- .../src/main/java/mage/client/MageFrame.java | 5 +- .../java/mage/client/cards/DragCardGrid.java | 4 +- .../deck/generator/DeckGeneratorDialog.java | 2 +- .../client/deckeditor/DeckEditorPanel.java | 22 +- .../deckeditor/DeckExportClipboardDialog.java | 2 +- .../client/deckeditor/DeckLegalityPanel.java | 2 +- .../client/dialog/CustomOptionsDialog.java | 4 +- .../java/mage/client/game/PlayerPanelExt.java | 1 - .../main/java/mage/remote/SessionImpl.java | 14 - .../src/main/java/mage/view/PlayerView.java | 7 - .../src/mage/player/ai/ComputerPlayer6.java | 2 +- .../mage/tournament/cubes/CubeFromDeck.java | 12 +- .../java/mage/server/TableController.java | 95 +++++-- .../server/managers/TournamentManager.java | 2 +- .../tournament/TournamentController.java | 10 +- .../tournament/TournamentManagerImpl.java | 5 +- .../server/tournament/TournamentSession.java | 4 +- .../test/serverside/deck/DeckHashTest.java | 241 ++++++++++++++++++ .../org/mage/test/stub/TournamentStub.java | 4 +- Mage/src/main/java/mage/cards/decks/Deck.java | 163 ++++++------ .../java/mage/cards/decks/DeckCardInfo.java | 49 ++-- .../java/mage/cards/decks/DeckCardLists.java | 7 +- .../java/mage/cards/decks/DeckFormats.java | 2 + .../cards/repository/ExpansionRepository.java | 5 +- .../game/command/emblems/EmblemOfCard.java | 3 +- Mage/src/main/java/mage/game/match/Match.java | 2 +- .../main/java/mage/game/match/MatchImpl.java | 24 +- .../java/mage/game/match/MatchPlayer.java | 36 ++- .../java/mage/game/tournament/Tournament.java | 2 +- .../mage/game/tournament/TournamentImpl.java | 10 +- .../game/tournament/TournamentPlayer.java | 14 +- Mage/src/main/java/mage/util/DeckUtil.java | 43 +++- 32 files changed, 551 insertions(+), 247 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/serverside/deck/DeckHashTest.java diff --git a/Mage.Client/src/main/java/mage/client/MageFrame.java b/Mage.Client/src/main/java/mage/client/MageFrame.java index 6fb4a8aa0d6..b6b43d00d70 100644 --- a/Mage.Client/src/main/java/mage/client/MageFrame.java +++ b/Mage.Client/src/main/java/mage/client/MageFrame.java @@ -87,7 +87,6 @@ import java.util.prefs.Preferences; public class MageFrame extends javax.swing.JFrame implements MageClient { private static final String TITLE_NAME = "XMage"; - private static final Logger logger = Logger.getLogger(MageFrame.class); private static final Logger LOGGER = Logger.getLogger(MageFrame.class); private static final String LITE_MODE_ARG = "-lite"; @@ -398,7 +397,7 @@ public class MageFrame extends javax.swing.JFrame implements MageClient { } private void bootstrapSetsAndFormats() { - logger.info("Loading sets and formats..."); + LOGGER.info("Loading sets and formats..."); ConstructedFormats.ensureLists(); } @@ -1460,7 +1459,7 @@ public class MageFrame extends javax.swing.JFrame implements MageClient { try { instance = new MageFrame(); } catch (Throwable e) { - logger.fatal("Critical error on start up, app will be closed: " + e.getMessage(), e); + LOGGER.fatal("Critical error on start up, app will be closed: " + e.getMessage(), e); System.exit(1); } diff --git a/Mage.Client/src/main/java/mage/client/cards/DragCardGrid.java b/Mage.Client/src/main/java/mage/client/cards/DragCardGrid.java index 5b59987d83d..c65df33bbf9 100644 --- a/Mage.Client/src/main/java/mage/client/cards/DragCardGrid.java +++ b/Mage.Client/src/main/java/mage/client/cards/DragCardGrid.java @@ -1861,9 +1861,9 @@ public class DragCardGrid extends JPanel implements DragCardSource, DragCardTarg List gridStack = new ArrayList<>(); gridRow.add(gridStack); for (DeckCardInfo info : stack) { - if (trackedCards.containsKey(info.getSetCode()) && trackedCards.get(info.getSetCode()).containsKey(info.getCardNum())) { + if (trackedCards.containsKey(info.getSetCode()) && trackedCards.get(info.getSetCode()).containsKey(info.getCardNumber())) { List candidates - = trackedCards.get(info.getSetCode()).get(info.getCardNum()); + = trackedCards.get(info.getSetCode()).get(info.getCardNumber()); if (!candidates.isEmpty()) { gridStack.add(candidates.remove(0)); thisMaxStackSize = Math.max(thisMaxStackSize, gridStack.size()); diff --git a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorDialog.java b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorDialog.java index 4f27cc4f1a2..4186651ba01 100644 --- a/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorDialog.java +++ b/Mage.Client/src/main/java/mage/client/deck/generator/DeckGeneratorDialog.java @@ -328,7 +328,7 @@ public class DeckGeneratorDialog { tmp.getParentFile().mkdirs(); tmp.createNewFile(); deck.setName(deckName); - XMAGE.getExporter().writeDeck(tmp.getAbsolutePath(), deck.getDeckCardLists()); + XMAGE.getExporter().writeDeck(tmp.getAbsolutePath(), deck.prepareCardsOnlyDeck()); cleanUp(); return tmp.getAbsolutePath(); } catch (Exception e) { diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/DeckEditorPanel.java b/Mage.Client/src/main/java/mage/client/deckeditor/DeckEditorPanel.java index 764d25ef3f5..2ed8dff85fe 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/DeckEditorPanel.java +++ b/Mage.Client/src/main/java/mage/client/deckeditor/DeckEditorPanel.java @@ -794,18 +794,14 @@ public class DeckEditorPanel extends javax.swing.JPanel { dialog.showDialog(); if (!dialog.getTmpPath().isEmpty()) { - Deck deckToAppend = null; StringBuilder errorMessages = new StringBuilder(); MageFrame.getDesktop().setCursor(new Cursor(Cursor.WAIT_CURSOR)); try { - deckToAppend = Deck.load(DeckImporter.importDeckFromFile(dialog.getTmpPath(), errorMessages, false), true, true); + Deck deckToAppend = Deck.load(DeckImporter.importDeckFromFile(dialog.getTmpPath(), errorMessages, false), true, true); processAndShowImportErrors(errorMessages); - - if (deckToAppend != null) { - deck = Deck.append(deckToAppend, deck); - refreshDeck(); - } + this.deck = Deck.append(deckToAppend, this.deck); + refreshDeck(); } catch (GameException e1) { JOptionPane.showMessageDialog(MageFrame.getDesktop(), e1.getMessage(), "Error loading deck", JOptionPane.ERROR_MESSAGE); } finally { @@ -864,7 +860,7 @@ public class DeckEditorPanel extends javax.swing.JPanel { MageFrame.getDesktop().setCursor(new Cursor(Cursor.WAIT_CURSOR)); try { - DeckFormats.writeDeck(needFileName, deck.getDeckCardLists()); + DeckFormats.writeDeck(needFileName, deck.prepareCardsOnlyDeck()); try { MageFrame.getPreferences().put("lastExportFolder", file.getCanonicalPath()); @@ -1364,7 +1360,7 @@ public class DeckEditorPanel extends javax.swing.JPanel { useDeckInfo = true; } MageFrame.getDesktop().setCursor(new Cursor(Cursor.WAIT_CURSOR)); - DeckCardLists cardLists = deck.getDeckCardLists(); + DeckCardLists cardLists = deck.prepareCardsOnlyDeck(); cardLists.setCardLayout(deckArea.getCardLayout()); cardLists.setSideboardLayout(deckArea.getSideboardLayout()); if (!useDeckInfo) { @@ -1444,12 +1440,14 @@ public class DeckEditorPanel extends javax.swing.JPanel { if (mode == DeckEditorMode.SIDEBOARDING || mode == DeckEditorMode.LIMITED_BUILDING || mode == DeckEditorMode.LIMITED_SIDEBOARD_BUILDING) { + // in game mode - move all to sideboard for (Card card : deck.getCards()) { deck.getSideboard().add(card); } deck.getCards().clear(); cardSelector.loadSideboard(new ArrayList<>(deck.getSideboard()), this.bigCard); } else { + // in deck editor mode - clear all cards deck = new Deck(); } refreshDeck(); @@ -1488,7 +1486,7 @@ public class DeckEditorPanel extends javax.swing.JPanel { updateDeckTask.cancel(true); } - if (SessionHandler.submitDeck(mode, tableId, deck.getDeckCardLists())) { + if (SessionHandler.submitDeck(mode, tableId, deck.prepareCardsOnlyDeck())) { removeDeckEditor(); } }//GEN-LAST:event_btnSubmitActionPerformed @@ -1504,7 +1502,7 @@ public class DeckEditorPanel extends javax.swing.JPanel { updateDeckTask.cancel(true); } - if (SessionHandler.submitDeck(mode, tableId, deck.getDeckCardLists())) { + if (SessionHandler.submitDeck(mode, tableId, deck.prepareCardsOnlyDeck())) { SwingUtilities.invokeLater(this::removeDeckEditor); } return null; @@ -1617,7 +1615,7 @@ class UpdateDeckTask extends SwingWorker { @Override protected Void doInBackground() throws Exception { while (!isCancelled()) { - SessionHandler.updateDeck(tableId, deck.getDeckCardLists()); + SessionHandler.updateDeck(tableId, deck.prepareCardsOnlyDeck()); TimeUnit.SECONDS.sleep(5); } return null; diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/DeckExportClipboardDialog.java b/Mage.Client/src/main/java/mage/client/deckeditor/DeckExportClipboardDialog.java index 2e83a36577f..b7e00e3e6c1 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/DeckExportClipboardDialog.java +++ b/Mage.Client/src/main/java/mage/client/deckeditor/DeckExportClipboardDialog.java @@ -89,7 +89,7 @@ public class DeckExportClipboardDialog extends MageDialog { DeckExporter exporter = formats.get(formatIndex).getExporter(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); - exporter.writeDeck(baos, deck.getDeckCardLists()); + exporter.writeDeck(baos, deck.prepareCardsOnlyDeck()); editData.setText(baos.toString()); editData.setCaretPosition(0); } diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/DeckLegalityPanel.java b/Mage.Client/src/main/java/mage/client/deckeditor/DeckLegalityPanel.java index d92a684faf3..0e5e2ff3d91 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/DeckLegalityPanel.java +++ b/Mage.Client/src/main/java/mage/client/deckeditor/DeckLegalityPanel.java @@ -135,7 +135,7 @@ public class DeckLegalityPanel extends javax.swing.JPanel { } else { // contains mock cards, e.g. it's a Deck Editor try { - deckToValidate = Deck.load(deck.getDeckCardLists(), true, false); + deckToValidate = Deck.load(deck.prepareCardsOnlyDeck(), true, false); } catch (Exception ex) { logger.error("Can't load real deck cards for validate: " + ex.getMessage(), ex); return; diff --git a/Mage.Client/src/main/java/mage/client/dialog/CustomOptionsDialog.java b/Mage.Client/src/main/java/mage/client/dialog/CustomOptionsDialog.java index 1f1e220d680..1b394785aed 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/CustomOptionsDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/CustomOptionsDialog.java @@ -557,7 +557,7 @@ public class CustomOptionsDialog extends MageDialog { } if (perPlayerEmblemDeck != null) { perPlayerEmblemDeck.clearLayouts(); - options.setPerPlayerEmblemCards(perPlayerEmblemDeck.getDeckCardLists().getCards()); + options.setPerPlayerEmblemCards(perPlayerEmblemDeck.prepareCardsOnlyDeck().getCards()); } else { options.setPerPlayerEmblemCards(Collections.emptySet()); } @@ -571,7 +571,7 @@ public class CustomOptionsDialog extends MageDialog { } if (startingPlayerEmblemDeck != null) { startingPlayerEmblemDeck.clearLayouts(); - options.setGlobalEmblemCards(startingPlayerEmblemDeck.getDeckCardLists().getCards()); + options.setGlobalEmblemCards(startingPlayerEmblemDeck.prepareCardsOnlyDeck().getCards()); } else { options.setGlobalEmblemCards(Collections.emptySet()); } diff --git a/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java b/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java index cae24e1d8ce..19c92a8c344 100644 --- a/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java +++ b/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java @@ -394,7 +394,6 @@ public class PlayerPanelExt extends javax.swing.JPanel { String countryName = CountryUtil.getCountryName(flagName); basicTooltipText = "Name: " + player.getName() + "
Flag: " + (countryName == null ? "Unknown" : countryName) - + "
Deck hash code: " + player.getDeckHashCode() + "
This match wins: " + player.getWins() + " of " + player.getWinsNeeded() + " (to win the match)"; } diff --git a/Mage.Common/src/main/java/mage/remote/SessionImpl.java b/Mage.Common/src/main/java/mage/remote/SessionImpl.java index afbb9edc24e..8cf820fdb65 100644 --- a/Mage.Common/src/main/java/mage/remote/SessionImpl.java +++ b/Mage.Common/src/main/java/mage/remote/SessionImpl.java @@ -1297,20 +1297,6 @@ public class SessionImpl implements Session { return false; } - // @Override -// public boolean startChallenge(UUID roomId, UUID tableId, UUID challengeId) { -// try { -// if (isConnected()) { -// server.startChallenge(sessionId, roomId, tableId, challengeId); -// return true; -// } -// } catch (MageException ex) { -// handleMageException(ex); -// } catch (Throwable t) { -// handleThrowable(t); -// } -// return false; -// } @Override public boolean submitDeck(UUID tableId, DeckCardLists deck) { try { diff --git a/Mage.Common/src/main/java/mage/view/PlayerView.java b/Mage.Common/src/main/java/mage/view/PlayerView.java index 326b243319b..a18cad86475 100644 --- a/Mage.Common/src/main/java/mage/view/PlayerView.java +++ b/Mage.Common/src/main/java/mage/view/PlayerView.java @@ -30,7 +30,6 @@ public class PlayerView implements Serializable { private final Counters counters; private final int wins; private final int winsNeeded; - private final long deckHashCode; private final int libraryCount; private final int handCount; private final boolean isActive; @@ -68,8 +67,6 @@ public class PlayerView implements Serializable { this.counters = player.getCounters(); this.wins = player.getMatchPlayer().getWins(); this.winsNeeded = player.getMatchPlayer().getWinsNeeded(); - // If match ended immediately before, deck can be set to null so check is necessarry here - this.deckHashCode = player.getMatchPlayer().getDeck() != null ? player.getMatchPlayer().getDeck().getDeckHashCode() : 0; this.libraryCount = player.getLibrary().size(); this.handCount = player.getHand().size(); this.manaPool = new ManaPoolView(player.getManaPool()); @@ -206,10 +203,6 @@ public class PlayerView implements Serializable { return winsNeeded; } - public long getDeckHashCode() { - return deckHashCode; - } - public int getHandCount() { return this.handCount; } diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java index c00e05c5e61..6d36ed8444d 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java @@ -54,7 +54,7 @@ public class ComputerPlayer6 extends ComputerPlayer { private static final int MAX_SIMULATED_NODES_PER_CALC = 5000; // same params as Executors.newFixedThreadPool - // no needs erorrs check in afterExecute here cause that pool used for FutureTask with result check already + // no needs errors check in afterExecute here cause that pool used for FutureTask with result check already private static final ExecutorService threadPoolSimulations = new ThreadPoolExecutor( COMPUTER_MAX_THREADS_FOR_SIMULATIONS, COMPUTER_MAX_THREADS_FOR_SIMULATIONS, diff --git a/Mage.Server.Plugins/Mage.Tournament.BoosterDraft/src/mage/tournament/cubes/CubeFromDeck.java b/Mage.Server.Plugins/Mage.Tournament.BoosterDraft/src/mage/tournament/cubes/CubeFromDeck.java index 892efb3cdf5..796ad4557de 100644 --- a/Mage.Server.Plugins/Mage.Tournament.BoosterDraft/src/mage/tournament/cubes/CubeFromDeck.java +++ b/Mage.Server.Plugins/Mage.Tournament.BoosterDraft/src/mage/tournament/cubes/CubeFromDeck.java @@ -13,15 +13,13 @@ public class CubeFromDeck extends DraftCube { public CubeFromDeck(Deck cubeFromDeck) { super("Cube From Deck"); - DeckCardLists cards = null; - if (cubeFromDeck != null) { - cards = cubeFromDeck.getDeckCardLists(); + if (cubeFromDeck == null) { + return; } - if (cards != null) { - for (DeckCardInfo card : cards.getCards()) { - cubeCards.add(new CardIdentity(card.getCardName(), card.getSetCode(), card.getCardNum())); - } + DeckCardLists cards = cubeFromDeck.prepareCardsOnlyDeck(); + for (DeckCardInfo card : cards.getCards()) { + cubeCards.add(new CardIdentity(card.getCardName(), card.getSetCode(), card.getCardNumber())); } } } diff --git a/Mage.Server/src/main/java/mage/server/TableController.java b/Mage.Server/src/main/java/mage/server/TableController.java index 330988eeccc..37d5257080a 100644 --- a/Mage.Server/src/main/java/mage/server/TableController.java +++ b/Mage.Server/src/main/java/mage/server/TableController.java @@ -236,35 +236,51 @@ public class TableController { return true; } + /** + * Check and join real player + */ public synchronized boolean joinTable(UUID userId, String name, PlayerType playerType, int skill, DeckCardLists deckList, String password) throws MageException { + // user - must exist Optional _user = managerFactory.userManager().getUser(userId); if (!_user.isPresent()) { logger.error("Join Table: can't find user to join " + name + " Id = " + userId); return false; } + + // table - already joined User user = _user.get(); if (userPlayerMap.containsKey(userId) && playerType == PlayerType.HUMAN) { user.showUserMessage("Join Table", new StringBuilder("You can join a table only one time.").toString()); return false; } + + // table - already started if (table.getState() != TableState.WAITING) { user.showUserMessage("Join Table", "No available seats."); return false; } - // check password + + // table - wrong password if (!table.getMatch().getOptions().getPassword().isEmpty() && playerType == PlayerType.HUMAN) { if (!table.getMatch().getOptions().getPassword().equals(password)) { user.showUserMessage("Join Table", "Wrong password."); return false; } } + + // table - no more free seats Seat seat = table.getNextAvailableSeat(playerType); if (seat == null) { user.showUserMessage("Join Table", "No available seats."); return false; } + + // deck - try to load real deck (any unknown cards will raise exception error here) + // P.S. Quick start button can raise error here on memory problems, but it's ok + // (real table creating is unaffected and will not create empty table) Deck deck = Deck.load(deckList, false, false); + // deck - validate if (!Main.isTestMode() && !table.getValidator().validate(deck)) { StringBuilder sb = new StringBuilder("You (").append(name).append(") have an invalid deck for the selected ").append(table.getValidator().getName()).append(" Format. \n\n"); List errorsList = table.getValidator().getErrorsListSorted(); @@ -273,13 +289,16 @@ public class TableController { }); sb.append("\n\nSelect a deck that is appropriate for the selected format and try again!"); user.showUserMessage("Join Table", sb.toString()); + + // owner must create table with valid deck only if (isOwner(userId)) { - logger.debug("New table removed because owner submitted invalid deck tableId " + table.getId()); + logger.error("New table removed because owner submitted invalid deck tableId " + table.getId()); managerFactory.tableManager().removeTable(table.getId()); } return false; } - // Check quit ratio. + + // user - restrict by quit ratio int quitRatio = table.getMatch().getOptions().getQuitRatio(); if (quitRatio < user.getMatchQuitRatio()) { String message = new StringBuilder("Your quit ratio ").append(user.getMatchQuitRatio()) @@ -288,7 +307,7 @@ public class TableController { return false; } - // Check minimum rating. + // user - restrict by rating int minimumRating = table.getMatch().getOptions().getMinimumRating(); int userRating; if (table.getMatch().getOptions().isLimited()) { @@ -303,7 +322,7 @@ public class TableController { return false; } - // Check power level for table (currently only used for EDH/Commander table) + // user - restrict by deck power level and cards colors (see edh power level for details) int edhPowerLevel = table.getMatch().getOptions().getEdhPowerLevel(); if (edhPowerLevel > 0 && table.getValidator().getName().toLowerCase(Locale.ENGLISH).equals("commander")) { int deckEdhPowerLevel = table.getValidator().getEdhPowerLevel(deck); @@ -349,6 +368,7 @@ public class TableController { } } + // player - try to create (human, ai, etc) Optional playerOpt = createPlayer(name, seat.getPlayerType(), skill); if (!playerOpt.isPresent()) { String message = "Could not create player " + name + " of type " + seat.getPlayerType(); @@ -356,14 +376,18 @@ public class TableController { user.showUserMessage("Join Table", message); return false; } + + // player - restrict by player type, e.g. draft bot can join to tourney only Player player = playerOpt.get(); if (!player.canJoinTable(table)) { user.showUserMessage("Join Table", "A " + seat.getPlayerType() + " player can't join this table."); return false; } + + // all fine, player can be added match.addPlayer(player, deck); table.joinTable(player, seat); - logger.trace(player.getName() + " joined tableId: " + table.getId()); + //only inform human players and add them to sessionPlayerMap if (seat.getPlayer().isHuman()) { seat.getPlayer().setUserData(user.getUserData()); @@ -373,6 +397,7 @@ public class TableController { user.ccJoinedTable(table.getRoomId(), table.getId(), false); userPlayerMap.put(userId, player.getId()); } + return true; } @@ -391,8 +416,22 @@ public class TableController { } } + /** + * Submit deck on sideboarding/construction (final deck) + */ public synchronized boolean submitDeck(UUID userId, DeckCardLists deckList) throws MageException { UUID playerId = userPlayerMap.get(userId); + + // player - update tourney's player status + // TODO: need research, why it here instead real time check? + if (table.isTournamentSubTable()) { + TournamentPlayer tournamentPlayer = table.getTournament().getPlayer(playerId); + if (tournamentPlayer != null) { + tournamentPlayer.setStateInfo(""); // reset sideboarding state + } + } + + // player - already quit if (table.isTournament()) { TournamentPlayer player = tournament.getPlayer(playerId); if (player == null || player.hasQuit()) { @@ -403,23 +442,26 @@ public class TableController { if (mPlayer == null || mPlayer.hasQuit()) { return true; // so the construct panel closes after submit } - if (table.isTournamentSubTable()) { - TournamentPlayer tournamentPlayer = table.getTournament().getPlayer(mPlayer.getPlayer().getId()); - if (tournamentPlayer != null) { - tournamentPlayer.setStateInfo(""); // reset sideboarding state - } - } } - if (table.getState() != TableState.SIDEBOARDING && table.getState() != TableState.CONSTRUCTING) { + + // tourney - too late for submit + if (table.getState() != TableState.SIDEBOARDING + && table.getState() != TableState.CONSTRUCTING) { return false; } + + // deck - try to load real deck (any unknown cards will raise exception error here) Deck deck = Deck.load(deckList, false, false); + + // workaround to keep deck name for Tiny Leaders because it must be hidden for players if (table.getState() == TableState.SIDEBOARDING && table.getMatch() != null) { MatchPlayer mPlayer = table.getMatch().getPlayer(playerId); if (mPlayer != null) { deck.setName(mPlayer.getDeck().getName()); } } + + // deck - validate if (!Main.isTestMode() && !table.getValidator().validate(deck)) { Optional _user = managerFactory.userManager().getUser(userId); if (!_user.isPresent()) { @@ -434,21 +476,20 @@ public class TableController { _user.get().showUserMessage("Submit deck", sb.toString()); return false; } + submitDeck(userId, playerId, deck); return true; } public void updateDeck(UUID userId, DeckCardLists deckList) throws MageException { - boolean validDeck; UUID playerId = userPlayerMap.get(userId); - if (table.getState() != TableState.SIDEBOARDING && table.getState() != TableState.CONSTRUCTING) { + // TODO: need refactor, many duplicated code like state check + if (table.getState() != TableState.SIDEBOARDING + && table.getState() != TableState.CONSTRUCTING) { return; } Deck deck = Deck.load(deckList, false, false); - validDeck = updateDeck(userId, playerId, deck); - if (!validDeck && getTableState() == TableState.SIDEBOARDING) { - logger.warn(" userId: " + userId + " - Modified deck card list!"); - } + updateDeck(userId, playerId, deck); } private void submitDeck(UUID userId, UUID playerId, Deck deck) { @@ -461,20 +502,21 @@ public class TableController { } } - private boolean updateDeck(UUID userId, UUID playerId, Deck deck) { - boolean validDeck = true; + private void updateDeck(UUID userId, UUID playerId, Deck deck) { if (table.isTournament()) { if (tournament != null) { - validDeck = managerFactory.tournamentManager().updateDeck(tournament.getId(), playerId, deck); + // TODO: is it possible to update from direct call command in game?! + managerFactory.tournamentManager().updateDeck(tournament.getId(), playerId, deck); } else { logger.fatal("Tournament == null table: " + table.getId() + " userId: " + userId); } - } else if (TableState.SIDEBOARDING == table.getState()) { - validDeck = match.updateDeck(playerId, deck); + } else if (table.getState() == TableState.SIDEBOARDING) { + match.updateDeck(playerId, deck); } else { // deck was meanwhile submitted so the autoupdate can be ignored + // TODO: need research + logger.warn("wtf, why it submitting?!"); } - return validDeck; } public boolean watchTable(UUID userId) { @@ -538,6 +580,7 @@ public class TableController { } else { UUID playerId = userPlayerMap.get(userId); if (playerId != null) { + // TODO: wtf, need research and document all that checks, can be never used code if (table.getState() == TableState.WAITING || table.getState() == TableState.READY_TO_START) { table.leaveNotStartedTable(playerId); if (table.isTournament()) { @@ -884,7 +927,7 @@ public class TableController { private void autoSideboard() { for (MatchPlayer player : match.getPlayers()) { if (!player.isDoneSideboarding()) { - match.submitDeck(player.getPlayer().getId(), player.generateDeck(table.getValidator())); + match.submitDeck(player.getPlayer().getId(), player.autoCompleteDeck(table.getValidator())); } } } diff --git a/Mage.Server/src/main/java/mage/server/managers/TournamentManager.java b/Mage.Server/src/main/java/mage/server/managers/TournamentManager.java index 852588c2643..d5574011ae1 100644 --- a/Mage.Server/src/main/java/mage/server/managers/TournamentManager.java +++ b/Mage.Server/src/main/java/mage/server/managers/TournamentManager.java @@ -22,7 +22,7 @@ public interface TournamentManager { void submitDeck(UUID tournamentId, UUID playerId, Deck deck); - boolean updateDeck(UUID tournamentId, UUID playerId, Deck deck); + void updateDeck(UUID tournamentId, UUID playerId, Deck deck); TournamentView getTournamentView(UUID tournamentId); diff --git a/Mage.Server/src/main/java/mage/server/tournament/TournamentController.java b/Mage.Server/src/main/java/mage/server/tournament/TournamentController.java index 9e65fbe40c9..4069b81c4b2 100644 --- a/Mage.Server/src/main/java/mage/server/tournament/TournamentController.java +++ b/Mage.Server/src/main/java/mage/server/tournament/TournamentController.java @@ -324,11 +324,13 @@ public class TournamentController { } } - public boolean updateDeck(UUID playerId, Deck deck) { - if (tournamentSessions.containsKey(playerId)) { - return tournamentSessions.get(playerId).updateDeck(deck); + public void updateDeck(UUID playerId, Deck deck) { + TournamentSession session = tournamentSessions.getOrDefault(playerId, null); + if (session == null) { + return; } - return false; + + session.updateDeck(deck); } public void timeout(UUID userId) { diff --git a/Mage.Server/src/main/java/mage/server/tournament/TournamentManagerImpl.java b/Mage.Server/src/main/java/mage/server/tournament/TournamentManagerImpl.java index f84ec3fcd5e..18fb4bd4094 100644 --- a/Mage.Server/src/main/java/mage/server/tournament/TournamentManagerImpl.java +++ b/Mage.Server/src/main/java/mage/server/tournament/TournamentManagerImpl.java @@ -17,6 +17,7 @@ import java.util.concurrent.ConcurrentMap; */ public class TournamentManagerImpl implements TournamentManager { + // TODO: must check all usage of controllers.get and insert null check and error log private final ManagerFactory managerFactory; private final ConcurrentMap controllers = new ConcurrentHashMap<>(); @@ -61,8 +62,8 @@ public class TournamentManagerImpl implements TournamentManager { } @Override - public boolean updateDeck(UUID tournamentId, UUID playerId, Deck deck) { - return controllers.get(tournamentId).updateDeck(playerId, deck); + public void updateDeck(UUID tournamentId, UUID playerId, Deck deck) { + controllers.get(tournamentId).updateDeck(playerId, deck); } @Override diff --git a/Mage.Server/src/main/java/mage/server/tournament/TournamentSession.java b/Mage.Server/src/main/java/mage/server/tournament/TournamentSession.java index e04c9062e2f..8496fb1cf57 100644 --- a/Mage.Server/src/main/java/mage/server/tournament/TournamentSession.java +++ b/Mage.Server/src/main/java/mage/server/tournament/TournamentSession.java @@ -83,8 +83,8 @@ public class TournamentSession { tournament.submitDeck(playerId, deck); } - public boolean updateDeck(Deck deck) { - return tournament.updateDeck(playerId, deck); + public void updateDeck(Deck deck) { + tournament.updateDeck(playerId, deck); } public void setKilled() { diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/deck/DeckHashTest.java b/Mage.Tests/src/test/java/org/mage/test/serverside/deck/DeckHashTest.java new file mode 100644 index 00000000000..5abd1069b82 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/deck/DeckHashTest.java @@ -0,0 +1,241 @@ +package org.mage.test.serverside.deck; + +import mage.cards.decks.Deck; +import mage.cards.decks.DeckCardInfo; +import mage.cards.decks.DeckCardLayout; +import mage.cards.decks.DeckCardLists; +import mage.game.GameException; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.MageTestPlayerBase; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author JayDi85 + */ +public class DeckHashTest extends MageTestPlayerBase { + + private static final DeckCardInfo BAD_CARD = new DeckCardInfo("unknown card", "123", "xxx"); + private static final DeckCardInfo GOOD_CARD_1 = new DeckCardInfo("Lightning Bolt", "141", "CLU"); + private static final DeckCardInfo GOOD_CARD_1_ANOTHER_SET = new DeckCardInfo("Lightning Bolt", "146", "M10"); + private static final DeckCardInfo GOOD_CARD_1_x3 = new DeckCardInfo("Lightning Bolt", "141", "CLU", 3); + private static final DeckCardInfo GOOD_CARD_2 = new DeckCardInfo("Bear's Companion", "182", "2x2"); + private static final DeckCardInfo GOOD_CARD_3 = new DeckCardInfo("Amplifire", "92", "RNA"); + + private void assertDecks(String check, boolean mustBeSame, Deck deck1, Deck deck2) { + Assert.assertEquals( + check + " - " + (mustBeSame ? "hash code must be same" : "hash code must be different"), + mustBeSame, + deck1.getDeckHash() == deck2.getDeckHash() + ); + } + + private Deck prepareCardsDeck(List mainCards, List sideboardCards) { + DeckCardLists deckSource = new DeckCardLists(); + deckSource.getCards().addAll(mainCards); + deckSource.getSideboard().addAll(sideboardCards); + try { + return Deck.load(deckSource, true); + } catch (GameException e) { + throw new IllegalArgumentException("Must not catch exception, but found " + e, e); + } catch (Throwable e) { + Assert.fail("wtf"); + } + return null; + } + + private Deck prepareEmptyDeck(String name, String author, boolean addMainLayout, boolean addSideboardLayout) { + DeckCardLists deckSource = new DeckCardLists(); + deckSource.setName(name); + deckSource.setAuthor(author); + if (addMainLayout) { + deckSource.setCardLayout(new DeckCardLayout(new ArrayList<>(), "xxx")); + } + if (addSideboardLayout) { + deckSource.setCardLayout(new DeckCardLayout(new ArrayList<>(), "xxx")); + } + try { + return Deck.load(deckSource, true); + } catch (GameException e) { + throw new IllegalArgumentException("Must not catch exception, but found " + e, e); + } + } + + @Test + public void test_MustIgnoreNonCardsData() { + // additional info must be ignored for deck hash + Deck emptyDeck = new Deck(); + assertDecks("empty with empty", true, emptyDeck, emptyDeck); + assertDecks("empty with new empty", true, emptyDeck, new Deck()); + + assertDecks("empty with name", true, emptyDeck, prepareEmptyDeck("name", "", false, false)); + assertDecks("empty with author", true, emptyDeck, prepareEmptyDeck("", "author", false, false)); + assertDecks("empty with layout main", true, emptyDeck, prepareEmptyDeck("", "", true, false)); + assertDecks("empty with layout sideboard", true, emptyDeck, prepareEmptyDeck("", "", false, true)); + assertDecks("empty with all", true, emptyDeck, prepareEmptyDeck("name", "author", true, true)); + + assertDecks("empty with non empty", false, emptyDeck, prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList())); + } + + @Test + public void test_MustUseCardNameAndAmountOnly() { + // card names and amount must be important + // set and card number must be ignored + + assertDecks( + "empty", + true, + prepareCardsDeck(Arrays.asList(), Arrays.asList()), + prepareCardsDeck(Arrays.asList(), Arrays.asList()) + ); + + assertDecks( + "same names", + true, + prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList()), + prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList()) + ); + + assertDecks( + "same names but diff sets", + true, + prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList()), + prepareCardsDeck(Arrays.asList(GOOD_CARD_1_ANOTHER_SET), Arrays.asList()) + ); + + assertDecks( + "diff names", + false, + prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList()), + prepareCardsDeck(Arrays.asList(GOOD_CARD_2), Arrays.asList()) + ); + + assertDecks( + "same main, same side", + true, + prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList(GOOD_CARD_3)), + prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList(GOOD_CARD_3)) + ); + + assertDecks( + "diff main, same side", + false, + prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList(GOOD_CARD_3)), + prepareCardsDeck(Arrays.asList(GOOD_CARD_2), Arrays.asList(GOOD_CARD_3)) + ); + + assertDecks( + "same main, diff side", + false, + prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList(GOOD_CARD_2)), + prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList(GOOD_CARD_3)) + ); + + assertDecks( + "same names, but diff amount - main", + false, + prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList()), + prepareCardsDeck(Arrays.asList(GOOD_CARD_1_x3), Arrays.asList()) + ); + + assertDecks( + "same names, but diff amount - side", + false, + prepareCardsDeck(Arrays.asList(), Arrays.asList(GOOD_CARD_1)), + prepareCardsDeck(Arrays.asList(), Arrays.asList(GOOD_CARD_1_x3)) + ); + } + + @Test + public void test_MustIgnoreCardsOrder() { + // deck hash must use sorted list, so deck order must be ignored + + assertDecks( + "diff order - main", + true, + prepareCardsDeck(Arrays.asList(GOOD_CARD_1, GOOD_CARD_1, GOOD_CARD_1), Arrays.asList()), + prepareCardsDeck(Arrays.asList(GOOD_CARD_1_x3), Arrays.asList()) + ); + + assertDecks( + "diff order - side", + true, + prepareCardsDeck(Arrays.asList(), Arrays.asList(GOOD_CARD_1, GOOD_CARD_1, GOOD_CARD_1)), + prepareCardsDeck(Arrays.asList(), Arrays.asList(GOOD_CARD_1_x3)) + ); + + assertDecks( + "diff order - diff names", + true, + prepareCardsDeck(Arrays.asList(GOOD_CARD_1, GOOD_CARD_2, GOOD_CARD_3), Arrays.asList()), + prepareCardsDeck(Arrays.asList(GOOD_CARD_3, GOOD_CARD_1, GOOD_CARD_2), Arrays.asList()) + ); + } + + @Test + public void test_MustIgnoreUnknownCards() { + // hash must use only real cards and ignore missing + + assertDecks( + "empty and unknown", + true, + prepareCardsDeck(Arrays.asList(), Arrays.asList()), + prepareCardsDeck(Arrays.asList(BAD_CARD), Arrays.asList()) + ); + + assertDecks( + "unknown and unknown", + true, + prepareCardsDeck(Arrays.asList(BAD_CARD), Arrays.asList()), + prepareCardsDeck(Arrays.asList(BAD_CARD), Arrays.asList()) + ); + + assertDecks( + "unknown and x2 unknown", + true, + prepareCardsDeck(Arrays.asList(BAD_CARD), Arrays.asList()), + prepareCardsDeck(Arrays.asList(BAD_CARD, BAD_CARD), Arrays.asList()) + ); + + assertDecks( + "card and unknown", + false, + prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList()), + prepareCardsDeck(Arrays.asList(BAD_CARD), Arrays.asList()) + ); + + assertDecks( + "card + unknown and unknown", + false, + prepareCardsDeck(Arrays.asList(BAD_CARD, GOOD_CARD_1), Arrays.asList()), + prepareCardsDeck(Arrays.asList(BAD_CARD), Arrays.asList()) + ); + + assertDecks( + "card + unknown and card + unknown", + true, + prepareCardsDeck(Arrays.asList(BAD_CARD, GOOD_CARD_1), Arrays.asList()), + prepareCardsDeck(Arrays.asList(GOOD_CARD_1, BAD_CARD), Arrays.asList()) + ); + } + + @Test + public void test_MustRaiseErrorOnTooManyCards() { + // good amount + Assert.assertEquals(90, new DeckCardInfo(GOOD_CARD_1.getCardName(), GOOD_CARD_1.getCardNumber(), GOOD_CARD_1.getSetCode(), 90).getAmount()); + + // bad amount + try { + new DeckCardInfo(GOOD_CARD_1.getCardName(), GOOD_CARD_1.getCardNumber(), GOOD_CARD_1.getSetCode(), 100); + } catch (Exception e) { + if (e.getMessage().contains("Found too big amount")) { + // good + return; + } + } + Assert.fail("must raise exception on too big amount"); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/stub/TournamentStub.java b/Mage.Tests/src/test/java/org/mage/test/stub/TournamentStub.java index 4ae9d7a69a0..572f3d37a85 100644 --- a/Mage.Tests/src/test/java/org/mage/test/stub/TournamentStub.java +++ b/Mage.Tests/src/test/java/org/mage/test/stub/TournamentStub.java @@ -85,8 +85,8 @@ public class TournamentStub implements Tournament { } @Override - public boolean updateDeck(UUID playerId, Deck deck) { - return true; + public void updateDeck(UUID playerId, Deck deck) { + } @Override diff --git a/Mage/src/main/java/mage/cards/decks/Deck.java b/Mage/src/main/java/mage/cards/decks/Deck.java index a68343a35d6..b4c755f6f0e 100644 --- a/Mage/src/main/java/mage/cards/decks/Deck.java +++ b/Mage/src/main/java/mage/cards/decks/Deck.java @@ -1,28 +1,34 @@ package mage.cards.decks; +import mage.MageObject; import mage.cards.Card; import mage.cards.repository.CardInfo; import mage.cards.repository.CardRepository; import mage.game.GameException; import mage.util.Copyable; import mage.util.DeckUtil; -import org.apache.log4j.Logger; import java.io.Serializable; -import java.util.*; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; +/** + * Server side deck with workable cards, also can be used in GUI + *

+ * If you need text only deck then look at DeckCardLists + */ public class Deck implements Serializable, Copyable { static final int MAX_CARDS_PER_DECK = 2000; - private String name; + private String name; // TODO: must rework somehow - tiny leaders use deck name to find commander card and hide it for a user private final Set cards = new LinkedHashSet<>(); private final Set sideboard = new LinkedHashSet<>(); - private DeckCardLayout cardsLayout; - private DeckCardLayout sideboardLayout; - private long deckHashCode = 0; - private long deckCompleteHashCode = 0; + private DeckCardLayout cardsLayout; // client side only + private DeckCardLayout sideboardLayout; // client side only public Deck() { super(); @@ -34,8 +40,6 @@ public class Deck implements Serializable, Copyable { this.sideboard.addAll(deck.sideboard.stream().map(Card::copy).collect(Collectors.toList())); this.cardsLayout = deck.cardsLayout == null ? null : deck.cardsLayout.copy(); this.sideboardLayout = deck.sideboardLayout == null ? null : deck.sideboardLayout.copy(); - this.deckHashCode = deck.deckHashCode; - this.deckCompleteHashCode = deck.deckCompleteHashCode; } public static Deck load(DeckCardLists deckCardLists) throws GameException { @@ -46,27 +50,15 @@ public class Deck implements Serializable, Copyable { return Deck.load(deckCardLists, ignoreErrors, true); } - public static Deck append(Deck deckToAppend, Deck currentDeck) throws GameException { - List deckCardNames = new ArrayList<>(); - - for (Card card : deckToAppend.getCards()) { - if (card != null) { - currentDeck.cards.add(card); - deckCardNames.add(card.getName()); - } - } - List sbCardNames = new ArrayList<>(); - for (Card card : deckToAppend.getSideboard()) { - if (card != null) { - currentDeck.sideboard.add(card); - deckCardNames.add(card.getName()); - } - } - Collections.sort(deckCardNames); - Collections.sort(sbCardNames); - String deckString = deckCardNames.toString() + sbCardNames.toString(); - currentDeck.setDeckHashCode(DeckUtil.fixedHash(deckString)); - return currentDeck; + public static Deck append(Deck sourceDeck, Deck currentDeck) throws GameException { + Deck newDeck = currentDeck.copy(); + sourceDeck.getCards().forEach(card -> { + newDeck.cards.add(card.copy()); + }); + sourceDeck.getSideboard().forEach(card -> { + newDeck.sideboard.add(card.copy()); + }); + return newDeck; } public static Deck load(DeckCardLists deckCardLists, boolean ignoreErrors, boolean mockCards) throws GameException { @@ -87,49 +79,45 @@ public class Deck implements Serializable, Copyable { deck.setName(deckCardLists.getName()); deck.cardsLayout = deckCardLists.getCardLayout() == null ? null : deckCardLists.getCardLayout().copy(); deck.sideboardLayout = deckCardLists.getSideboardLayout() == null ? null : deckCardLists.getSideboardLayout().copy(); - List deckCardNames = new ArrayList<>(); - int totalCards = 0; - for (DeckCardInfo deckCardInfo : deckCardLists.getCards()) { - Card card = createCard(deckCardInfo, mockCards, cardInfoCache); - if (card != null) { - if (totalCards > MAX_CARDS_PER_DECK) { - break; - } - deck.cards.add(card); - deckCardNames.add(card.getName()); - totalCards++; - } else if (!ignoreErrors) { - throw createCardNotFoundGameException(deckCardInfo, deckCardLists.getName()); - } - } - List sbCardNames = new ArrayList<>(); - for (DeckCardInfo deckCardInfo : deckCardLists.getSideboard()) { - Card card = createCard(deckCardInfo, mockCards, cardInfoCache); - if (card != null) { + // load only real cards + int totalCards = 0; + + // main + main: + for (DeckCardInfo deckCardInfo : deckCardLists.getCards()) { + for (int i = 1; i <= deckCardInfo.getAmount(); i++) { if (totalCards > MAX_CARDS_PER_DECK) { - break; + break main; + } + + Card card = createCard(deckCardInfo, mockCards, cardInfoCache); + if (card != null) { + deck.cards.add(card); + totalCards++; + } else if (!ignoreErrors) { + throw createCardNotFoundGameException(deckCardInfo, deckCardLists.getName()); } - deck.sideboard.add(card); - sbCardNames.add(card.getName()); - totalCards++; - } else if (!ignoreErrors) { - throw createCardNotFoundGameException(deckCardInfo, deckCardLists.getName()); } } - Collections.sort(deckCardNames); - Collections.sort(sbCardNames); - String deckString = deckCardNames.toString() + sbCardNames.toString(); - deck.setDeckHashCode(DeckUtil.fixedHash(deckString)); - if (sbCardNames.isEmpty()) { - deck.setDeckCompleteHashCode(deck.getDeckHashCode()); - } else { - List deckAllCardNames = new ArrayList<>(); - deckAllCardNames.addAll(deckCardNames); - deckAllCardNames.addAll(sbCardNames); - Collections.sort(deckAllCardNames); - deck.setDeckCompleteHashCode(DeckUtil.fixedHash(deckAllCardNames.toString())); + + // sideboard + side: + for (DeckCardInfo deckCardInfo : deckCardLists.getSideboard()) { + for (int i = 1; i <= deckCardInfo.getAmount(); i++) { + if (totalCards > MAX_CARDS_PER_DECK) { + break side; + } + Card card = createCard(deckCardInfo, mockCards, cardInfoCache); + if (card != null) { + deck.sideboard.add(card); + totalCards++; + } else if (!ignoreErrors) { + throw createCardNotFoundGameException(deckCardInfo, deckCardLists.getName()); + } + } } + return deck; } @@ -149,7 +137,7 @@ public class Deck implements Serializable, Copyable { String cardError = String.format("Card not found - %s - %s - %s in deck %s.", deckCardInfo.getCardName(), deckCardInfo.getSetCode(), - deckCardInfo.getCardNum(), + deckCardInfo.getCardNumber(), deckName ); cardError += "\n\nPossible reasons:"; @@ -168,15 +156,15 @@ public class Deck implements Serializable, Copyable { CardInfo cardInfo; if (cardInfoCache != null) { // from cache - String key = String.format("%s_%s", deckCardInfo.getSetCode(), deckCardInfo.getCardNum()); + String key = String.format("%s_%s", deckCardInfo.getSetCode(), deckCardInfo.getCardNumber()); cardInfo = cardInfoCache.getOrDefault(key, null); if (cardInfo == null) { - cardInfo = CardRepository.instance.findCard(deckCardInfo.getSetCode(), deckCardInfo.getCardNum()); + cardInfo = CardRepository.instance.findCard(deckCardInfo.getSetCode(), deckCardInfo.getCardNumber()); cardInfoCache.put(key, cardInfo); } } else { // from db - cardInfo = CardRepository.instance.findCard(deckCardInfo.getSetCode(), deckCardInfo.getCardNum()); + cardInfo = CardRepository.instance.findCard(deckCardInfo.getSetCode(), deckCardInfo.getCardNumber()); } if (cardInfo == null) { @@ -190,13 +178,18 @@ public class Deck implements Serializable, Copyable { } } - public DeckCardLists getDeckCardLists() { + /** + * Prepare new deck with cards only without layout and other client side settings. + * Use it for any export and server's deck update/submit + */ + public DeckCardLists prepareCardsOnlyDeck() { DeckCardLists deckCardLists = new DeckCardLists(); - deckCardLists.setName(name); + for (Card card : cards) { deckCardLists.getCards().add(new DeckCardInfo(card.getName(), card.getCardNumber(), card.getExpansionSetCode())); } + for (Card card : sideboard) { deckCardLists.getSideboard().add(new DeckCardInfo(card.getName(), card.getCardNumber(), card.getExpansionSetCode())); } @@ -227,6 +220,7 @@ public class Deck implements Serializable, Copyable { return cards; } + // TODO: delete and replace by getCards() public Set getMaindeckCards() { return cards .stream() @@ -262,22 +256,6 @@ public class Deck implements Serializable, Copyable { return sideboardLayout; } - public long getDeckHashCode() { - return deckHashCode; - } - - public void setDeckHashCode(long deckHashCode) { - this.deckHashCode = deckHashCode; - } - - public long getDeckCompleteHashCode() { - return deckCompleteHashCode; - } - - public void setDeckCompleteHashCode(long deckHashCode) { - this.deckCompleteHashCode = deckHashCode; - } - public void clearLayouts() { this.cardsLayout = null; this.sideboardLayout = null; @@ -287,4 +265,11 @@ public class Deck implements Serializable, Copyable { public Deck copy() { return new Deck(this); } + + public long getDeckHash() { + return DeckUtil.getDeckHash( + this.cards.stream().map(MageObject::getName).collect(Collectors.toList()), + this.sideboard.stream().map(MageObject::getName).collect(Collectors.toList()) + ); + } } diff --git a/Mage/src/main/java/mage/cards/decks/DeckCardInfo.java b/Mage/src/main/java/mage/cards/decks/DeckCardInfo.java index 466eb8e853f..19327614f23 100644 --- a/Mage/src/main/java/mage/cards/decks/DeckCardInfo.java +++ b/Mage/src/main/java/mage/cards/decks/DeckCardInfo.java @@ -1,5 +1,3 @@ - - package mage.cards.decks; import mage.util.Copyable; @@ -7,14 +5,18 @@ import mage.util.Copyable; import java.io.Serializable; /** + * Client side: card record in deck file + * * @author LevelX2 */ public class DeckCardInfo implements Serializable, Copyable { + static final int MAX_AMOUNT_PER_CARD = 99; + private String cardName; private String setCode; - private String cardNum; - private int quantity; + private String cardNumber; + private int amount; public DeckCardInfo() { super(); @@ -23,19 +25,29 @@ public class DeckCardInfo implements Serializable, Copyable { protected DeckCardInfo(final DeckCardInfo info) { this.cardName = info.cardName; this.setCode = info.setCode; - this.cardNum = info.cardNum; - this.quantity = info.quantity; + this.cardNumber = info.cardNumber; + this.amount = info.amount; } - public DeckCardInfo(String cardName, String cardNum, String setCode) { - this(cardName, cardNum, setCode, 1); + public DeckCardInfo(String cardName, String cardNumber, String setCode) { + this(cardName, cardNumber, setCode, 1); } - public DeckCardInfo(String cardName, String cardNum, String setCode, int quantity) { + public static void makeSureCardAmountFine(int amount, String cardName) { + // runtime check + if (amount > MAX_AMOUNT_PER_CARD) { + // xmage uses 1 for amount all around, but keep that protection anyway + throw new IllegalArgumentException("Found too big amount for a deck's card: " + cardName + " - " + amount); + } + } + + public DeckCardInfo(String cardName, String cardNumber, String setCode, int amount) { + makeSureCardAmountFine(amount, cardName); + this.cardName = cardName; - this.cardNum = cardNum; + this.cardNumber = cardNumber; this.setCode = setCode; - this.quantity = quantity; + this.amount = amount; } public String getCardName() { @@ -46,21 +58,16 @@ public class DeckCardInfo implements Serializable, Copyable { return setCode; } - public String getCardNum() { - return cardNum; + public String getCardNumber() { + return cardNumber; } - public int getQuantity() { - return quantity; - } - - public DeckCardInfo increaseQuantity() { - quantity++; - return this; + public int getAmount() { + return amount; } public String getCardKey() { - return setCode + cardNum; + return setCode + cardNumber; } @Override diff --git a/Mage/src/main/java/mage/cards/decks/DeckCardLists.java b/Mage/src/main/java/mage/cards/decks/DeckCardLists.java index d9021e322e2..4ae17b70512 100644 --- a/Mage/src/main/java/mage/cards/decks/DeckCardLists.java +++ b/Mage/src/main/java/mage/cards/decks/DeckCardLists.java @@ -6,9 +6,11 @@ import mage.util.Copyable; import java.io.Serializable; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; /** + * Client side deck with text only. + *

+ * Can contain restricted, un-implemented or unknown cards * * @author BetaSteward_at_googlemail.com, JayDi85 */ @@ -46,12 +48,15 @@ public class DeckCardLists implements Serializable, Copyable { public DeckCardLayout getCardLayout() { return cardLayout; } + public void setCardLayout(DeckCardLayout layout) { this.cardLayout = layout; } + public DeckCardLayout getSideboardLayout() { return sideboardLayout; } + public void setSideboardLayout(DeckCardLayout layout) { this.sideboardLayout = layout; } diff --git a/Mage/src/main/java/mage/cards/decks/DeckFormats.java b/Mage/src/main/java/mage/cards/decks/DeckFormats.java index 1677fe79bde..db671699b59 100644 --- a/Mage/src/main/java/mage/cards/decks/DeckFormats.java +++ b/Mage/src/main/java/mage/cards/decks/DeckFormats.java @@ -72,6 +72,8 @@ public enum DeckFormats { return res; } + // TODO: need refactor and remove all that methods to 1-3 versions only + public static void writeDeck(String file, DeckCardLists deck) throws IOException { writeDeck(new File(file), deck); } diff --git a/Mage/src/main/java/mage/cards/repository/ExpansionRepository.java b/Mage/src/main/java/mage/cards/repository/ExpansionRepository.java index 53321c6f08b..23088f04e98 100644 --- a/Mage/src/main/java/mage/cards/repository/ExpansionRepository.java +++ b/Mage/src/main/java/mage/cards/repository/ExpansionRepository.java @@ -56,8 +56,9 @@ public enum ExpansionRepository { instanceInitialized = true; eventSource.fireRepositoryDbLoaded(); - } catch (SQLException ex) { - ex.printStackTrace(); + } catch (SQLException e) { + // TODO: add app close? + e.printStackTrace(); } } diff --git a/Mage/src/main/java/mage/game/command/emblems/EmblemOfCard.java b/Mage/src/main/java/mage/game/command/emblems/EmblemOfCard.java index d889e7278dd..efd91aeeaf8 100644 --- a/Mage/src/main/java/mage/game/command/emblems/EmblemOfCard.java +++ b/Mage/src/main/java/mage/game/command/emblems/EmblemOfCard.java @@ -11,7 +11,6 @@ import mage.cards.repository.CardRepository; import mage.constants.Zone; import mage.game.command.Emblem; import mage.util.CardUtil; -import org.apache.log4j.Logger; import java.util.List; import java.util.stream.Collectors; @@ -50,7 +49,7 @@ public final class EmblemOfCard extends Emblem { public static Card cardFromDeckInfo(DeckCardInfo info) { return lookupCard( info.getCardName(), - info.getCardNum(), + info.getCardNumber(), info.getSetCode(), "DeckCardInfo" ); diff --git a/Mage/src/main/java/mage/game/match/Match.java b/Mage/src/main/java/mage/game/match/Match.java index 0ab9150dd36..bf3fe7d64e8 100644 --- a/Mage/src/main/java/mage/game/match/Match.java +++ b/Mage/src/main/java/mage/game/match/Match.java @@ -41,7 +41,7 @@ public interface Match { void submitDeck(UUID playerId, Deck deck); - boolean updateDeck(UUID playerId, Deck deck); + void updateDeck(UUID playerId, Deck deck); void startMatch(); diff --git a/Mage/src/main/java/mage/game/match/MatchImpl.java b/Mage/src/main/java/mage/game/match/MatchImpl.java index 98ae8b21dd9..fcd3f1e337d 100644 --- a/Mage/src/main/java/mage/game/match/MatchImpl.java +++ b/Mage/src/main/java/mage/game/match/MatchImpl.java @@ -379,9 +379,8 @@ public abstract class MatchImpl implements Match { public void submitDeck(UUID playerId, Deck deck) { MatchPlayer player = getPlayer(playerId); if (player != null) { - // make sure the deck name (needed for Tiny Leaders) won't get lost by sideboarding + // workaround to keep deck name for Tiny Leaders because it must be hidden for players deck.setName(player.getDeck().getName()); - deck.setDeckHashCode(player.getDeck().getDeckHashCode()); player.submitDeck(deck); } synchronized (this) { @@ -390,20 +389,14 @@ public abstract class MatchImpl implements Match { } @Override - public boolean updateDeck(UUID playerId, Deck deck) { - boolean validDeck = true; + public void updateDeck(UUID playerId, Deck deck) { + // used for auto-save deck MatchPlayer player = getPlayer(playerId); - if (player != null) { - // Check if the cards included in the deck are the same as in the original deck - validDeck = (player.getDeck().getDeckCompleteHashCode() == deck.getDeckCompleteHashCode()); - if (validDeck == false) { - // clear the deck so the player cheating looses the game - deck.getCards().clear(); - deck.getSideboard().clear(); - } - player.updateDeck(deck); + if (player == null) { + return; } - return validDeck; + + player.updateDeck(deck); } protected String createGameStartMessage() { @@ -416,9 +409,6 @@ public abstract class MatchImpl implements Match { sb.append(" QUITTED"); } sb.append("
"); - if (mp.getDeck() != null) { - sb.append("DeckHash: ").append(mp.getDeck().getDeckHashCode()).append("
"); - } } if (getDraws() > 0) { sb.append(" Draws: ").append(getDraws()).append("
"); diff --git a/Mage/src/main/java/mage/game/match/MatchPlayer.java b/Mage/src/main/java/mage/game/match/MatchPlayer.java index 18ec3bfd64c..aa8b0945898 100644 --- a/Mage/src/main/java/mage/game/match/MatchPlayer.java +++ b/Mage/src/main/java/mage/game/match/MatchPlayer.java @@ -4,6 +4,7 @@ import mage.cards.Card; import mage.cards.decks.Deck; import mage.cards.decks.DeckValidator; import mage.players.Player; +import org.apache.log4j.Logger; import java.io.Serializable; @@ -12,13 +13,15 @@ import java.io.Serializable; */ public class MatchPlayer implements Serializable { + private static final Logger logger = Logger.getLogger(MatchPlayer.class); + private int wins; private int winsNeeded; private boolean matchWinner; private Deck deck; - private Player player; - private String name; + private final Player player; + private final String name; private boolean quit; private boolean doneSideboarding; @@ -89,22 +92,33 @@ public class MatchPlayer implements Serializable { return viewerDeck; } - public void submitDeck(Deck deck) { - this.deck = deck; + public void submitDeck(Deck newDeck) { + this.deck = newDeck; this.doneSideboarding = true; } - public void updateDeck(Deck deck) { + public boolean updateDeck(Deck newDeck) { + // used for auto-save by timeout from client side + + // workaround to keep deck name for Tiny Leaders because it must be hidden for players if (this.deck != null) { - // preserver deck name, important for Tiny Leaders format - deck.setName(this.getDeck().getName()); - // preserve the original deck hash code before sideboarding to give no information if cards were swapped - deck.setDeckHashCode(this.getDeck().getDeckHashCode()); + newDeck.setName(this.getDeck().getName()); } - this.deck = deck; + + // make sure it's the same deck (player do not add or remove something) + boolean isGood = (this.deck.getDeckHash() == newDeck.getDeckHash()); + if (!isGood) { + logger.error("Found cheating player " + player.getName() + + " with changed deck, main " + newDeck.getCards().size() + ", side " + newDeck.getSideboard().size()); + newDeck.getCards().clear(); + newDeck.getSideboard().clear(); + } + + this.deck = newDeck; + return isGood; } - public Deck generateDeck(DeckValidator deckValidator) { + public Deck autoCompleteDeck(DeckValidator deckValidator) { // auto complete deck while (deck.getMaindeckCards().size() < deckValidator.getDeckMinSize() && !deck.getSideboard().isEmpty()) { Card card = deck.getSideboard().iterator().next(); diff --git a/Mage/src/main/java/mage/game/tournament/Tournament.java b/Mage/src/main/java/mage/game/tournament/Tournament.java index a296709b791..6bdf6d372d5 100644 --- a/Mage/src/main/java/mage/game/tournament/Tournament.java +++ b/Mage/src/main/java/mage/game/tournament/Tournament.java @@ -49,7 +49,7 @@ public interface Tournament { void submitDeck(UUID playerId, Deck deck); - boolean updateDeck(UUID playerId, Deck deck); + void updateDeck(UUID playerId, Deck deck); void autoSubmit(UUID playerId, Deck deck); diff --git a/Mage/src/main/java/mage/game/tournament/TournamentImpl.java b/Mage/src/main/java/mage/game/tournament/TournamentImpl.java index aee81e73498..9fd1d41a9fa 100644 --- a/Mage/src/main/java/mage/game/tournament/TournamentImpl.java +++ b/Mage/src/main/java/mage/game/tournament/TournamentImpl.java @@ -139,11 +139,13 @@ public abstract class TournamentImpl implements Tournament { } @Override - public boolean updateDeck(UUID playerId, Deck deck) { - if (players.containsKey(playerId)) { - return players.get(playerId).updateDeck(deck); + public void updateDeck(UUID playerId, Deck deck) { + TournamentPlayer player = players.getOrDefault(playerId, null); + if (player == null) { + return; } - return false; + + player.updateDeck(deck); } protected Round createRoundRandom() { diff --git a/Mage/src/main/java/mage/game/tournament/TournamentPlayer.java b/Mage/src/main/java/mage/game/tournament/TournamentPlayer.java index f6bd6f6875c..1e1631d019c 100644 --- a/Mage/src/main/java/mage/game/tournament/TournamentPlayer.java +++ b/Mage/src/main/java/mage/game/tournament/TournamentPlayer.java @@ -93,18 +93,18 @@ public class TournamentPlayer { } public boolean updateDeck(Deck deck) { - // Check if the cards included in the deck are the same as in the original deck - 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() + // used for auto-save deck + + // make sure it's the same deck (player do not add or remove something) + boolean isGood = (this.getDeck().getDeckHash() == deck.getDeckHash()); + if (!isGood) { + logger.error("Found cheating tourney player " + player.getName() + " with changed deck, main " + deck.getCards().size() + ", side " + deck.getSideboard().size()); deck.getCards().clear(); deck.getSideboard().clear(); } this.deck = deck; - return validDeck; + return isGood; } /** diff --git a/Mage/src/main/java/mage/util/DeckUtil.java b/Mage/src/main/java/mage/util/DeckUtil.java index 38da6de811e..10f484d2d4d 100644 --- a/Mage/src/main/java/mage/util/DeckUtil.java +++ b/Mage/src/main/java/mage/util/DeckUtil.java @@ -1,20 +1,36 @@ package mage.util; +import mage.cards.decks.Deck; +import mage.players.Player; import org.apache.log4j.Logger; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** - * @author LevelX2 + * @author LevelX2, JayDi85 */ public final class DeckUtil { private static final Logger logger = Logger.getLogger(DeckUtil.class); - public static long fixedHash(String string) { + /** + * Find deck hash (main + sideboard) + * It must be same after sideboard (do not depend on main/sb structure) + */ + public static long getDeckHash(List mainCards, List sideboardCards) { + List all = new ArrayList<>(mainCards); + all.addAll(sideboardCards); + Collections.sort(all); + return getStringHash(all.toString()); + } + + private static long getStringHash(String string) { long h = 1125899906842597L; // prime int len = string.length(); @@ -42,4 +58,27 @@ public final class DeckUtil { } return null; } + + /** + * make sure it's the same deck (player do not add or remove something) + * + * @param newDeck will be clear on cheating + */ + public boolean checkDeckForModification(String checkInfo, Player player, Deck oldDeck, Deck newDeck) { + boolean isGood = (oldDeck.getDeckHash() == newDeck.getDeckHash()); + if (!isGood) { + String message = String.format("Found cheating player [%s] in [%s] with changed deck, main %d -> %d, side %d -> %d", + player.getName(), + checkInfo, + oldDeck.getCards().size(), + newDeck.getCards().size(), + oldDeck.getSideboard().size(), + newDeck.getSideboard().size() + ); + logger.error(message); + newDeck.getCards().clear(); + newDeck.getSideboard().clear(); + } + return isGood; + } }