From 621d8c188df907a5778e149d5130baf524aba222 Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Fri, 17 Jul 2020 17:42:49 +0200 Subject: [PATCH] * Reworked rollback handling - possible fix for #2072 #5383, #4309 and fixes #5883, fixes #1983, fixes #5917. --- Mage/src/main/java/mage/game/Game.java | 21 ++-- Mage/src/main/java/mage/game/GameImpl.java | 135 ++++++++++++--------- 2 files changed, 90 insertions(+), 66 deletions(-) diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index ebf4dad93b..106a68ece5 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -1,5 +1,8 @@ package mage.game; +import java.io.Serializable; +import java.util.*; +import java.util.stream.Collectors; import mage.MageItem; import mage.MageObject; import mage.abilities.Ability; @@ -42,10 +45,6 @@ import mage.players.Players; import mage.util.MessageToClient; import mage.util.functions.ApplyToPermanent; -import java.io.Serializable; -import java.util.*; -import java.util.stream.Collectors; - public interface Game extends MageItem, Serializable { MatchType getGameType(); @@ -301,8 +300,8 @@ public interface Game extends MageItem, Serializable { /** * Creates and fires an damage prevention event * - * @param damageEvent damage event that will be replaced (instanceof check - * will be done) + * @param damageEvent damage event that will be replaced (instanceof + * check will be done) * @param source ability that's the source of the prevention effect * @param game * @param amountToPrevent max preventable amount @@ -313,9 +312,10 @@ public interface Game extends MageItem, Serializable { /** * Creates and fires an damage prevention event * - * @param event damage event that will be replaced (instanceof check will be - * done) - * @param source ability that's the source of the prevention effect + * @param event damage event that will be replaced (instanceof + * check will be done) + * @param source ability that's the source of the prevention + * effect * @param game * @param preventAllDamage true if there is no limit to the damage that can * be prevented @@ -489,4 +489,7 @@ public interface Game extends MageItem, Serializable { return getCommandersIds(player, CommanderCardType.ANY); } + void setGameStopped(boolean gameStopped); + + boolean isGameStopped(); } diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 990499c74f..05341b64b4 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -1,5 +1,9 @@ package mage.game; +import java.io.IOException; +import java.io.Serializable; +import java.util.*; +import java.util.Map.Entry; import mage.MageException; import mage.MageObject; import mage.abilities.*; @@ -67,11 +71,6 @@ import mage.util.functions.ApplyToPermanent; import mage.watchers.common.*; import org.apache.log4j.Logger; -import java.io.IOException; -import java.io.Serializable; -import java.util.*; -import java.util.Map.Entry; - public abstract class GameImpl implements Game, Serializable { private static final int ROLLBACK_TURNS_MAX = 4; @@ -107,11 +106,13 @@ public abstract class GameImpl implements Game, Serializable { // game states to allow player rollback protected transient Map gameStatesRollBack = new HashMap<>(); protected boolean executingRollback; + protected int turnToGoToForRollback; protected Date startTime; protected Date endTime; protected UUID startingPlayerId; protected UUID winnerId; + protected boolean gameStopped = false; protected RangeOfInfluence range; protected Mulligan mulligan; @@ -767,27 +768,6 @@ public abstract class GameImpl implements Game, Serializable { } } - @Override - public void resume() { - playerList = state.getPlayerList(state.getActivePlayerId()); - Player player = getPlayer(playerList.get()); - boolean wasPaused = state.isPaused(); - state.resume(); - if (!checkIfGameIsOver()) { - fireInformEvent("Turn " + state.getTurnNum()); - if (checkStopOnTurnOption()) { - return; - } - state.getTurn().resumePlay(this, wasPaused); - if (!isPaused() && !checkIfGameIsOver()) { - endOfTurn(); - player = playerList.getNext(this, true); - state.setTurnNum(state.getTurnNum() + 1); - } - } - play(player.getId()); - } - protected void play(UUID nextPlayerId) { if (!isPaused() && !checkIfGameIsOver()) { playerList = state.getPlayerList(nextPlayerId); @@ -876,13 +856,8 @@ public abstract class GameImpl implements Game, Serializable { boolean skipTurn = false; do { if (executingRollback) { - executingRollback = false; + rollbackTurnsExecution(turnToGoToForRollback); player = getPlayer(state.getActivePlayerId()); - for (Player playerObject : getPlayers().values()) { - if (playerObject.isInGame()) { - playerObject.abortReset(); - } - } } else { state.setActivePlayerId(player.getId()); saveRollBackGameState(); @@ -904,6 +879,27 @@ public abstract class GameImpl implements Game, Serializable { return true; } + @Override + public void resume() { + playerList = state.getPlayerList(state.getActivePlayerId()); + Player player = getPlayer(playerList.get()); + boolean wasPaused = state.isPaused(); + state.resume(); + if (!checkIfGameIsOver()) { + fireInformEvent("Turn " + state.getTurnNum()); + if (checkStopOnTurnOption()) { + return; + } + state.getTurn().resumePlay(this, wasPaused); + if (!isPaused() && !checkIfGameIsOver()) { + endOfTurn(); + player = playerList.getNext(this, true); + state.setTurnNum(state.getTurnNum() + 1); + } + } + play(player.getId()); + } + private boolean checkStopOnTurnOption() { if (gameOptions.stopOnTurn != null && gameOptions.stopAtStep == PhaseStep.UNTAP) { if (gameOptions.stopOnTurn.equals(state.getTurnNum())) { @@ -1805,7 +1801,7 @@ public abstract class GameImpl implements Game, Serializable { break; } // triggered abilities that don't use the stack have to be executed first (e.g. Banisher Priest Return exiled creature - for (Iterator it = abilities.iterator(); it.hasNext(); ) { + for (Iterator it = abilities.iterator(); it.hasNext();) { TriggeredAbility triggeredAbility = it.next(); if (!triggeredAbility.isUsesStack()) { state.removeTriggeredAbility(triggeredAbility); @@ -1880,7 +1876,7 @@ public abstract class GameImpl implements Game, Serializable { Zone currentZone = this.getState().getZone(card.getId()); String currentZoneInfo = (currentZone == null ? "(error)" : "(" + currentZone.name() + ")"); if (player.chooseUse(Outcome.Benefit, "Move " + card.getIdName() - + " to the command zone or leave it in current zone " + currentZoneInfo + "?", "You can only make this choice once per object", + + " to the command zone or leave it in current zone " + currentZoneInfo + "?", "You can only make this choice once per object", "Move to command", "Leave in current zone " + currentZoneInfo, null, this)) { toMove.add(card); } else { @@ -2594,7 +2590,7 @@ public abstract class GameImpl implements Game, Serializable { } //20100423 - 800.4a Set toOutside = new HashSet<>(); - for (Iterator it = getBattlefield().getAllPermanents().iterator(); it.hasNext(); ) { + for (Iterator it = getBattlefield().getAllPermanents().iterator(); it.hasNext();) { Permanent perm = it.next(); if (perm.isOwnedBy(playerId)) { if (perm.getAttachedTo() != null) { @@ -2644,7 +2640,7 @@ public abstract class GameImpl implements Game, Serializable { player.moveCards(toOutside, Zone.OUTSIDE, null, this); // triggered abilities that don't use the stack have to be executed List abilities = state.getTriggered(player.getId()); - for (Iterator it = abilities.iterator(); it.hasNext(); ) { + for (Iterator it = abilities.iterator(); it.hasNext();) { TriggeredAbility triggeredAbility = it.next(); if (!triggeredAbility.isUsesStack()) { state.removeTriggeredAbility(triggeredAbility); @@ -2664,7 +2660,7 @@ public abstract class GameImpl implements Game, Serializable { // Remove cards from the player in all exile zones for (ExileZone exile : this.getExile().getExileZones()) { - for (Iterator it = exile.iterator(); it.hasNext(); ) { + for (Iterator it = exile.iterator(); it.hasNext();) { Card card = this.getCard(it.next()); if (card != null && card.isOwnedBy(playerId)) { it.remove(); @@ -2674,7 +2670,7 @@ public abstract class GameImpl implements Game, Serializable { //Remove all commander/emblems/plane the player controls boolean addPlaneAgain = false; - for (Iterator it = this.getState().getCommand().iterator(); it.hasNext(); ) { + for (Iterator it = this.getState().getCommand().iterator(); it.hasNext();) { CommandObject obj = it.next(); if (obj.isControlledBy(playerId)) { if (obj instanceof Emblem) { @@ -3260,32 +3256,46 @@ public abstract class GameImpl implements Game, Serializable { return turnToGoTo > 0 && gameStatesRollBack.containsKey(turnToGoTo); } + private void rollbackTurnsExecution(int turnToGoToForRollback) { + GameState restore = gameStatesRollBack.get(turnToGoToForRollback); + if (restore != null) { + informPlayers(GameLog.getPlayerRequestColoredText("Player request: Rolling back to start of turn " + restore.getTurnNum())); + state.restoreForRollBack(restore); + playerList.setCurrent(state.getPlayerByOrderId()); + // Reset temporary created bookmarks because no longer valid after rollback + savedStates.clear(); + gameStates.clear(); + // because restore uses the objects without copy each copy the state again + gameStatesRollBack.put(getTurnNum(), state.copy()); + + for (Player playerObject : getPlayers().values()) { + if (playerObject.isInGame()) { + playerObject.abortReset(); + } + } + } + executingRollback = false; + } + @Override public synchronized void rollbackTurns(int turnsToRollback) { - if (gameOptions.rollbackTurnsAllowed) { + if (gameOptions.rollbackTurnsAllowed && !executingRollback) { int turnToGoTo = getTurnNum() - turnsToRollback; if (turnToGoTo < 1 || !gameStatesRollBack.containsKey(turnToGoTo)) { informPlayers(GameLog.getPlayerRequestColoredText("Player request: It's not possible to rollback " + turnsToRollback + " turn(s)")); } else { - GameState restore = gameStatesRollBack.get(turnToGoTo); - if (restore != null) { - informPlayers(GameLog.getPlayerRequestColoredText("Player request: Rolling back to start of turn " + restore.getTurnNum())); - state.restoreForRollBack(restore); - playerList.setCurrent(state.getPlayerByOrderId()); - // Reset temporary created bookmarks because no longer valid after rollback - savedStates.clear(); - gameStates.clear(); - // because restore uses the objects without copy each copy the state again - gameStatesRollBack.put(getTurnNum(), state.copy()); - executingRollback = true; - for (Player playerObject : getPlayers().values()) { - if (playerObject.isHuman() && playerObject.canRespond()) { - playerObject.resetStoredBookmark(this); - playerObject.abort(); - playerObject.resetPlayerPassedActions(); - } + executingRollback = true; + turnToGoToForRollback = turnToGoTo; + for (Player playerObject : getPlayers().values()) { + if (playerObject.isHuman() && playerObject.canRespond()) { + playerObject.resetStoredBookmark(this); + playerObject.abort(); + playerObject.resetPlayerPassedActions(); } - fireUpdatePlayersEvent(); + } + fireUpdatePlayersEvent(); + if (gameOptions.testMode && gameStopped) { // in test mode execute rollback directly + rollbackTurnsExecution(turnToGoToForRollback); } } } @@ -3374,4 +3384,15 @@ public abstract class GameImpl implements Game, Serializable { public Set getCommandersIds(Player player, CommanderCardType commanderCardType) { return player.getCommandersIds(); } + + @Override + public void setGameStopped(boolean gameStopped) { + this.gameStopped = gameStopped; + } + + @Override + public boolean isGameStopped() { + return gameStopped; + } + }