* Reworked rollback handling - possible fix for #2072 #5383, #4309 and fixes #5883, fixes #1983, fixes #5917.

This commit is contained in:
LevelX2 2020-07-17 17:42:49 +02:00
parent fe9deec071
commit 621d8c188d
2 changed files with 90 additions and 66 deletions

View file

@ -1,5 +1,8 @@
package mage.game; package mage.game;
import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;
import mage.MageItem; import mage.MageItem;
import mage.MageObject; import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
@ -42,10 +45,6 @@ import mage.players.Players;
import mage.util.MessageToClient; import mage.util.MessageToClient;
import mage.util.functions.ApplyToPermanent; import mage.util.functions.ApplyToPermanent;
import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;
public interface Game extends MageItem, Serializable { public interface Game extends MageItem, Serializable {
MatchType getGameType(); MatchType getGameType();
@ -301,8 +300,8 @@ public interface Game extends MageItem, Serializable {
/** /**
* Creates and fires an damage prevention event * Creates and fires an damage prevention event
* *
* @param damageEvent damage event that will be replaced (instanceof check * @param damageEvent damage event that will be replaced (instanceof
* will be done) * check will be done)
* @param source ability that's the source of the prevention effect * @param source ability that's the source of the prevention effect
* @param game * @param game
* @param amountToPrevent max preventable amount * @param amountToPrevent max preventable amount
@ -313,9 +312,10 @@ public interface Game extends MageItem, Serializable {
/** /**
* Creates and fires an damage prevention event * Creates and fires an damage prevention event
* *
* @param event damage event that will be replaced (instanceof check will be * @param event damage event that will be replaced (instanceof
* done) * check will be done)
* @param source ability that's the source of the prevention effect * @param source ability that's the source of the prevention
* effect
* @param game * @param game
* @param preventAllDamage true if there is no limit to the damage that can * @param preventAllDamage true if there is no limit to the damage that can
* be prevented * be prevented
@ -489,4 +489,7 @@ public interface Game extends MageItem, Serializable {
return getCommandersIds(player, CommanderCardType.ANY); return getCommandersIds(player, CommanderCardType.ANY);
} }
void setGameStopped(boolean gameStopped);
boolean isGameStopped();
} }

View file

@ -1,5 +1,9 @@
package mage.game; package mage.game;
import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import mage.MageException; import mage.MageException;
import mage.MageObject; import mage.MageObject;
import mage.abilities.*; import mage.abilities.*;
@ -67,11 +71,6 @@ import mage.util.functions.ApplyToPermanent;
import mage.watchers.common.*; import mage.watchers.common.*;
import org.apache.log4j.Logger; 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 { public abstract class GameImpl implements Game, Serializable {
private static final int ROLLBACK_TURNS_MAX = 4; 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 // game states to allow player rollback
protected transient Map<Integer, GameState> gameStatesRollBack = new HashMap<>(); protected transient Map<Integer, GameState> gameStatesRollBack = new HashMap<>();
protected boolean executingRollback; protected boolean executingRollback;
protected int turnToGoToForRollback;
protected Date startTime; protected Date startTime;
protected Date endTime; protected Date endTime;
protected UUID startingPlayerId; protected UUID startingPlayerId;
protected UUID winnerId; protected UUID winnerId;
protected boolean gameStopped = false;
protected RangeOfInfluence range; protected RangeOfInfluence range;
protected Mulligan mulligan; 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) { protected void play(UUID nextPlayerId) {
if (!isPaused() && !checkIfGameIsOver()) { if (!isPaused() && !checkIfGameIsOver()) {
playerList = state.getPlayerList(nextPlayerId); playerList = state.getPlayerList(nextPlayerId);
@ -876,13 +856,8 @@ public abstract class GameImpl implements Game, Serializable {
boolean skipTurn = false; boolean skipTurn = false;
do { do {
if (executingRollback) { if (executingRollback) {
executingRollback = false; rollbackTurnsExecution(turnToGoToForRollback);
player = getPlayer(state.getActivePlayerId()); player = getPlayer(state.getActivePlayerId());
for (Player playerObject : getPlayers().values()) {
if (playerObject.isInGame()) {
playerObject.abortReset();
}
}
} else { } else {
state.setActivePlayerId(player.getId()); state.setActivePlayerId(player.getId());
saveRollBackGameState(); saveRollBackGameState();
@ -904,6 +879,27 @@ public abstract class GameImpl implements Game, Serializable {
return true; 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() { private boolean checkStopOnTurnOption() {
if (gameOptions.stopOnTurn != null && gameOptions.stopAtStep == PhaseStep.UNTAP) { if (gameOptions.stopOnTurn != null && gameOptions.stopAtStep == PhaseStep.UNTAP) {
if (gameOptions.stopOnTurn.equals(state.getTurnNum())) { if (gameOptions.stopOnTurn.equals(state.getTurnNum())) {
@ -1805,7 +1801,7 @@ public abstract class GameImpl implements Game, Serializable {
break; break;
} }
// triggered abilities that don't use the stack have to be executed first (e.g. Banisher Priest Return exiled creature // triggered abilities that don't use the stack have to be executed first (e.g. Banisher Priest Return exiled creature
for (Iterator<TriggeredAbility> it = abilities.iterator(); it.hasNext(); ) { for (Iterator<TriggeredAbility> it = abilities.iterator(); it.hasNext();) {
TriggeredAbility triggeredAbility = it.next(); TriggeredAbility triggeredAbility = it.next();
if (!triggeredAbility.isUsesStack()) { if (!triggeredAbility.isUsesStack()) {
state.removeTriggeredAbility(triggeredAbility); state.removeTriggeredAbility(triggeredAbility);
@ -1880,7 +1876,7 @@ public abstract class GameImpl implements Game, Serializable {
Zone currentZone = this.getState().getZone(card.getId()); Zone currentZone = this.getState().getZone(card.getId());
String currentZoneInfo = (currentZone == null ? "(error)" : "(" + currentZone.name() + ")"); String currentZoneInfo = (currentZone == null ? "(error)" : "(" + currentZone.name() + ")");
if (player.chooseUse(Outcome.Benefit, "Move " + card.getIdName() 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)) { "Move to command", "Leave in current zone " + currentZoneInfo, null, this)) {
toMove.add(card); toMove.add(card);
} else { } else {
@ -2594,7 +2590,7 @@ public abstract class GameImpl implements Game, Serializable {
} }
//20100423 - 800.4a //20100423 - 800.4a
Set<Card> toOutside = new HashSet<>(); Set<Card> toOutside = new HashSet<>();
for (Iterator<Permanent> it = getBattlefield().getAllPermanents().iterator(); it.hasNext(); ) { for (Iterator<Permanent> it = getBattlefield().getAllPermanents().iterator(); it.hasNext();) {
Permanent perm = it.next(); Permanent perm = it.next();
if (perm.isOwnedBy(playerId)) { if (perm.isOwnedBy(playerId)) {
if (perm.getAttachedTo() != null) { if (perm.getAttachedTo() != null) {
@ -2644,7 +2640,7 @@ public abstract class GameImpl implements Game, Serializable {
player.moveCards(toOutside, Zone.OUTSIDE, null, this); player.moveCards(toOutside, Zone.OUTSIDE, null, this);
// triggered abilities that don't use the stack have to be executed // triggered abilities that don't use the stack have to be executed
List<TriggeredAbility> abilities = state.getTriggered(player.getId()); List<TriggeredAbility> abilities = state.getTriggered(player.getId());
for (Iterator<TriggeredAbility> it = abilities.iterator(); it.hasNext(); ) { for (Iterator<TriggeredAbility> it = abilities.iterator(); it.hasNext();) {
TriggeredAbility triggeredAbility = it.next(); TriggeredAbility triggeredAbility = it.next();
if (!triggeredAbility.isUsesStack()) { if (!triggeredAbility.isUsesStack()) {
state.removeTriggeredAbility(triggeredAbility); state.removeTriggeredAbility(triggeredAbility);
@ -2664,7 +2660,7 @@ public abstract class GameImpl implements Game, Serializable {
// Remove cards from the player in all exile zones // Remove cards from the player in all exile zones
for (ExileZone exile : this.getExile().getExileZones()) { for (ExileZone exile : this.getExile().getExileZones()) {
for (Iterator<UUID> it = exile.iterator(); it.hasNext(); ) { for (Iterator<UUID> it = exile.iterator(); it.hasNext();) {
Card card = this.getCard(it.next()); Card card = this.getCard(it.next());
if (card != null && card.isOwnedBy(playerId)) { if (card != null && card.isOwnedBy(playerId)) {
it.remove(); it.remove();
@ -2674,7 +2670,7 @@ public abstract class GameImpl implements Game, Serializable {
//Remove all commander/emblems/plane the player controls //Remove all commander/emblems/plane the player controls
boolean addPlaneAgain = false; boolean addPlaneAgain = false;
for (Iterator<CommandObject> it = this.getState().getCommand().iterator(); it.hasNext(); ) { for (Iterator<CommandObject> it = this.getState().getCommand().iterator(); it.hasNext();) {
CommandObject obj = it.next(); CommandObject obj = it.next();
if (obj.isControlledBy(playerId)) { if (obj.isControlledBy(playerId)) {
if (obj instanceof Emblem) { if (obj instanceof Emblem) {
@ -3260,32 +3256,46 @@ public abstract class GameImpl implements Game, Serializable {
return turnToGoTo > 0 && gameStatesRollBack.containsKey(turnToGoTo); 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 @Override
public synchronized void rollbackTurns(int turnsToRollback) { public synchronized void rollbackTurns(int turnsToRollback) {
if (gameOptions.rollbackTurnsAllowed) { if (gameOptions.rollbackTurnsAllowed && !executingRollback) {
int turnToGoTo = getTurnNum() - turnsToRollback; int turnToGoTo = getTurnNum() - turnsToRollback;
if (turnToGoTo < 1 || !gameStatesRollBack.containsKey(turnToGoTo)) { if (turnToGoTo < 1 || !gameStatesRollBack.containsKey(turnToGoTo)) {
informPlayers(GameLog.getPlayerRequestColoredText("Player request: It's not possible to rollback " + turnsToRollback + " turn(s)")); informPlayers(GameLog.getPlayerRequestColoredText("Player request: It's not possible to rollback " + turnsToRollback + " turn(s)"));
} else { } else {
GameState restore = gameStatesRollBack.get(turnToGoTo); executingRollback = true;
if (restore != null) { turnToGoToForRollback = turnToGoTo;
informPlayers(GameLog.getPlayerRequestColoredText("Player request: Rolling back to start of turn " + restore.getTurnNum())); for (Player playerObject : getPlayers().values()) {
state.restoreForRollBack(restore); if (playerObject.isHuman() && playerObject.canRespond()) {
playerList.setCurrent(state.getPlayerByOrderId()); playerObject.resetStoredBookmark(this);
// Reset temporary created bookmarks because no longer valid after rollback playerObject.abort();
savedStates.clear(); playerObject.resetPlayerPassedActions();
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();
}
} }
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<UUID> getCommandersIds(Player player, CommanderCardType commanderCardType) { public Set<UUID> getCommandersIds(Player player, CommanderCardType commanderCardType) {
return player.getCommandersIds(); return player.getCommandersIds();
} }
@Override
public void setGameStopped(boolean gameStopped) {
this.gameStopped = gameStopped;
}
@Override
public boolean isGameStopped() {
return gameStopped;
}
} }