diff --git a/Mage.Common/src/main/java/mage/utils/SystemUtil.java b/Mage.Common/src/main/java/mage/utils/SystemUtil.java index cda1bffda17..83f5a1c832a 100644 --- a/Mage.Common/src/main/java/mage/utils/SystemUtil.java +++ b/Mage.Common/src/main/java/mage/utils/SystemUtil.java @@ -798,7 +798,7 @@ public final class SystemUtil { // TODO: replace by player.move? switch (zone) { case BATTLEFIELD: - CardUtil.putCardOntoBattlefieldWithEffects(source, game, card, player); + CardUtil.putCardOntoBattlefieldWithEffects(source, game, card, player, false); break; case LIBRARY: card.setZone(Zone.LIBRARY, game); diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index 741c0d4e7ff..5c43777769e 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -1301,7 +1301,6 @@ public class ComputerPlayer extends PlayerImpl { @Override public boolean priority(Game game) { game.resumeTimer(getTurnControlledBy()); - log.debug("priority"); boolean result = priorityPlay(game); game.pauseTimer(getTurnControlledBy()); return result; diff --git a/Mage.Server/src/main/java/mage/server/game/GameController.java b/Mage.Server/src/main/java/mage/server/game/GameController.java index 1b9d83063a1..c7e83f06579 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameController.java +++ b/Mage.Server/src/main/java/mage/server/game/GameController.java @@ -24,7 +24,6 @@ import mage.players.Player; import mage.server.Main; import mage.server.User; import mage.server.managers.ManagerFactory; -import mage.server.util.Splitter; import mage.util.MultiAmountMessage; import mage.utils.StreamUtils; import mage.utils.timer.PriorityTimer; @@ -174,8 +173,8 @@ public class GameController implements GameCallback { timer.pause(); break; } - } catch (MageException ex) { - logger.fatal("Table event listener error ", ex); + } catch (MageException e) { + logger.fatal("Table event listener error: " + e, e); } } ); @@ -992,29 +991,24 @@ public class GameController implements GameCallback { } private void perform(UUID playerId, Command command, boolean informOthers) { - if (game.getPlayer(playerId).isGameUnderControl()) { // is the player controlling it's own turn - if (gameSessions.containsKey(playerId)) { - setupTimeout(playerId); - command.execute(playerId); - } - // TODO: if watcher disconnects then game freezes with active timer, must be fix for such use case - // same for another player (can be fixed by super-duper connection) - if (informOthers) { - informOthers(playerId); - } - } else { - List players = Splitter.split(game, playerId); - for (UUID uuid : players) { - if (gameSessions.containsKey(uuid)) { - setupTimeout(uuid); - command.execute(uuid); - } - } - // TODO: if watcher disconnects then game freeze with active timer, must be fix for such use case - // same for another player (can be fixed by super-duper connection) - if (informOthers) { - informOthers(players); - } + Player player = game.getPlayer(playerId); + if (player == null) { + throw new IllegalArgumentException("Can't perform command for unknown player id: " + playerId); + } + + Player realPlayerController = game.getPlayer(player.getTurnControlledBy()); + if (realPlayerController == null) { + throw new IllegalArgumentException("Can't find real turn controller for player id: " + playerId); + } + + if (gameSessions.containsKey(realPlayerController.getId())) { + setupTimeout(realPlayerController.getId()); + command.execute(realPlayerController.getId()); + } + // TODO: if watcher disconnects then game freezes with active timer, must be fix for such use case + // same for another player (can be fixed by super-duper connection) + if (informOthers) { + informOthers(playerId); } } diff --git a/Mage.Server/src/main/java/mage/server/util/Splitter.java b/Mage.Server/src/main/java/mage/server/util/Splitter.java deleted file mode 100644 index 6290a80d47a..00000000000 --- a/Mage.Server/src/main/java/mage/server/util/Splitter.java +++ /dev/null @@ -1,25 +0,0 @@ -package mage.server.util; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import mage.game.Game; -import mage.players.Player; - -/** - * @author nantuko - */ -public final class Splitter { - - private Splitter(){} - - public static List split(Game game, UUID playerId) { - List players = new ArrayList<>(); - //players.add(playerId); // add original player - Player player = game.getPlayer(playerId); - if (player != null && player.getTurnControlledBy() != null) { - players.add(player.getTurnControlledBy()); - } - return players; - } -} diff --git a/Mage.Sets/src/mage/cards/d/DeceiverOfForm.java b/Mage.Sets/src/mage/cards/d/DeceiverOfForm.java index 6252ebf18b1..056de1ede14 100644 --- a/Mage.Sets/src/mage/cards/d/DeceiverOfForm.java +++ b/Mage.Sets/src/mage/cards/d/DeceiverOfForm.java @@ -81,11 +81,8 @@ class DeceiverOfFormEffect extends OneShotEffect { && ((ModalDoubleFacedCard) cardFromTop).getLeftHalfCard().isCreature(game)) { copyFromCard = ((ModalDoubleFacedCard) cardFromTop).getLeftHalfCard(); } - Permanent newBluePrint = null; - newBluePrint = new PermanentCard(copyFromCard, source.getControllerId(), game); - newBluePrint.assignNewId(); + Permanent newBluePrint = new PermanentCard(copyFromCard, source.getControllerId(), game); CopyEffect copyEffect = new CopyEffect(Duration.EndOfTurn, newBluePrint, permanent.getId()); - copyEffect.newId(); Ability newAbility = source.copy(); copyEffect.init(newAbility, game); game.addEffect(copyEffect, newAbility); diff --git a/Mage.Sets/src/mage/cards/d/Dermotaxi.java b/Mage.Sets/src/mage/cards/d/Dermotaxi.java index 88971b76f50..6d058e662b7 100644 --- a/Mage.Sets/src/mage/cards/d/Dermotaxi.java +++ b/Mage.Sets/src/mage/cards/d/Dermotaxi.java @@ -131,7 +131,6 @@ class DermotaxiCopyEffect extends OneShotEffect { DermotaxiCopyApplier applier = new DermotaxiCopyApplier(); applier.apply(game, newBluePrint, source, sourcePermanent.getId()); CopyEffect copyEffect = new CopyEffect(Duration.EndOfTurn, newBluePrint, sourcePermanent.getId()); - copyEffect.newId(); copyEffect.setApplier(applier); game.addEffect(copyEffect, source); return true; diff --git a/Mage.Sets/src/mage/cards/d/DimirDoppelganger.java b/Mage.Sets/src/mage/cards/d/DimirDoppelganger.java index 64f9732a428..49694c07f27 100644 --- a/Mage.Sets/src/mage/cards/d/DimirDoppelganger.java +++ b/Mage.Sets/src/mage/cards/d/DimirDoppelganger.java @@ -86,9 +86,8 @@ class DimirDoppelgangerEffect extends OneShotEffect { CopyApplier applier = new DimirDoppelgangerCopyApplier(); applier.apply(game, newBluePrint, source, dimirDoppelganger.getId()); CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, dimirDoppelganger.getId()); - copyEffect.newId(); copyEffect.setApplier(applier); - Ability newAbility = source.copy(); + Ability newAbility = source.copy(); // TODO: why it copy new ability instead source? Some cards use it, some miss copyEffect.init(newAbility, game); game.addEffect(copyEffect, newAbility); } diff --git a/Mage.Sets/src/mage/cards/e/EchoingDeeps.java b/Mage.Sets/src/mage/cards/e/EchoingDeeps.java index 91db220de8e..82518e08484 100644 --- a/Mage.Sets/src/mage/cards/e/EchoingDeeps.java +++ b/Mage.Sets/src/mage/cards/e/EchoingDeeps.java @@ -99,7 +99,6 @@ class EchoingDeepsEffect extends OneShotEffect { CopyApplier applier = new EchoingDeepsApplier(); applier.apply(game, newBluePrint, source, source.getSourceId()); CopyEffect copyEffect = new CopyEffect(Duration.WhileOnBattlefield, newBluePrint, source.getSourceId()); - copyEffect.newId(); copyEffect.setApplier(applier); copyEffect.init(source, game); game.addEffect(copyEffect, source); diff --git a/Mage.Sets/src/mage/cards/i/IdentityThief.java b/Mage.Sets/src/mage/cards/i/IdentityThief.java index 68f6d09fd17..1a9ecce99c8 100644 --- a/Mage.Sets/src/mage/cards/i/IdentityThief.java +++ b/Mage.Sets/src/mage/cards/i/IdentityThief.java @@ -110,6 +110,7 @@ class IdentityThiefEffect extends OneShotEffect { ContinuousEffect copyEffect = new CopyEffect(Duration.EndOfTurn, targetPermanent, source.getSourceId()); copyEffect.setTargetPointer(new FixedTarget(sourcePermanent.getId(), game)); game.addEffect(copyEffect, source); + UUID exileZoneId = CardUtil.getExileZoneId(game, source.getSourceId(), source.getSourceObjectZoneChangeCounter()); if (controller.moveCardsToExile(targetPermanent, source, game, true, exileZoneId, sourcePermanent.getName())) { Effect effect = new ReturnToBattlefieldUnderOwnerControlTargetEffect(false, true); diff --git a/Mage.Sets/src/mage/cards/l/LazavDimirMastermind.java b/Mage.Sets/src/mage/cards/l/LazavDimirMastermind.java index dabcb69b2c8..40aa6f4d101 100644 --- a/Mage.Sets/src/mage/cards/l/LazavDimirMastermind.java +++ b/Mage.Sets/src/mage/cards/l/LazavDimirMastermind.java @@ -85,7 +85,6 @@ class LazavDimirMastermindEffect extends OneShotEffect { CopyApplier applier = new LazavDimirMastermindCopyApplier(); applier.apply(game, newBluePrint, source, lazavDimirMastermind.getId()); CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, lazavDimirMastermind.getId()); - copyEffect.newId(); copyEffect.setApplier(applier); Ability newAbility = source.copy(); copyEffect.init(newAbility, game); diff --git a/Mage.Sets/src/mage/cards/l/LazavTheMultifarious.java b/Mage.Sets/src/mage/cards/l/LazavTheMultifarious.java index 556b436c269..cc44a44c7e8 100644 --- a/Mage.Sets/src/mage/cards/l/LazavTheMultifarious.java +++ b/Mage.Sets/src/mage/cards/l/LazavTheMultifarious.java @@ -115,7 +115,6 @@ class LazavTheMultifariousEffect extends OneShotEffect { CopyApplier applier = new LazavTheMultifariousCopyApplier(); applier.apply(game, newBluePrint, source, lazavTheMultifarious.getId()); CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, lazavTheMultifarious.getId()); - copyEffect.newId(); copyEffect.setApplier(applier); Ability newAbility = source.copy(); copyEffect.init(newAbility, game); diff --git a/Mage.Sets/src/mage/cards/l/LazotepConvert.java b/Mage.Sets/src/mage/cards/l/LazotepConvert.java index cdc4853098d..6c6bf532331 100644 --- a/Mage.Sets/src/mage/cards/l/LazotepConvert.java +++ b/Mage.Sets/src/mage/cards/l/LazotepConvert.java @@ -95,6 +95,7 @@ class LazotepConvertCopyEffect extends OneShotEffect { } Card modifiedCopy = copyFromCard.copy(); //Appliers must be applied before CopyEffect, its applier setting is just for copies of copies + // TODO: research applier usage, why it here applier.apply(game, modifiedCopy, source, source.getSourceId()); game.addEffect(new CopyEffect( Duration.Custom, modifiedCopy, source.getSourceId() diff --git a/Mage.Sets/src/mage/cards/l/LikenessLooter.java b/Mage.Sets/src/mage/cards/l/LikenessLooter.java index 157a059e378..fd91545c71b 100644 --- a/Mage.Sets/src/mage/cards/l/LikenessLooter.java +++ b/Mage.Sets/src/mage/cards/l/LikenessLooter.java @@ -110,7 +110,6 @@ class LikenessLooterEffect extends OneShotEffect { CopyApplier applier = new LikenessLooterCopyApplier(); applier.apply(game, newBluePrint, source, permanent.getId()); CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, permanent.getId()); - copyEffect.newId(); copyEffect.setApplier(applier); Ability newAbility = source.copy(); copyEffect.init(newAbility, game); diff --git a/Mage.Sets/src/mage/cards/o/OlagLudevicsHubris.java b/Mage.Sets/src/mage/cards/o/OlagLudevicsHubris.java index d21012cbf8b..9b9a6abc671 100644 --- a/Mage.Sets/src/mage/cards/o/OlagLudevicsHubris.java +++ b/Mage.Sets/src/mage/cards/o/OlagLudevicsHubris.java @@ -88,7 +88,6 @@ class OlagLudevicsHubrisEffect extends ReplacementEffectImpl { CopyApplier applier = new OlagLudevicsHubrisCopyApplier(); applier.apply(game, newBluePrint, source, source.getSourceId()); CopyEffect copyEffect = new CopyEffect(Duration.Custom, newBluePrint, source.getSourceId()); - copyEffect.newId(); copyEffect.setApplier(applier); Ability newAbility = source.copy(); copyEffect.init(newAbility, game); diff --git a/Mage.Sets/src/mage/cards/s/ShadowKin.java b/Mage.Sets/src/mage/cards/s/ShadowKin.java index 0f4fc8fa159..95253a8f555 100644 --- a/Mage.Sets/src/mage/cards/s/ShadowKin.java +++ b/Mage.Sets/src/mage/cards/s/ShadowKin.java @@ -99,7 +99,6 @@ class ShadowKinEffect extends OneShotEffect { CopyApplier applier = new ShadowKinApplier(); applier.apply(game, blueprint, source, sourcePermanent.getId()); CopyEffect copyEffect = new CopyEffect(Duration.Custom, blueprint, sourcePermanent.getId()); - copyEffect.newId(); copyEffect.setApplier(applier); Ability newAbility = source.copy(); copyEffect.init(newAbility, game); diff --git a/Mage.Sets/src/mage/cards/t/TheMyriadPools.java b/Mage.Sets/src/mage/cards/t/TheMyriadPools.java index 8b2113b4eee..42f35dcc3d1 100644 --- a/Mage.Sets/src/mage/cards/t/TheMyriadPools.java +++ b/Mage.Sets/src/mage/cards/t/TheMyriadPools.java @@ -175,7 +175,6 @@ class TheMyriadPoolsCopyEffect extends OneShotEffect { newBluePrint = new PermanentCard(copyFromCardOnStack, source.getControllerId(), game); newBluePrint.assignNewId(); CopyEffect copyEffect = new CopyEffect(Duration.EndOfTurn, newBluePrint, targetPermanentToCopyTo.getId()); - copyEffect.newId(); Ability newAbility = source.copy(); copyEffect.init(newAbility, game); game.addEffect(copyEffect, newAbility); diff --git a/Mage.Sets/src/mage/cards/v/VolrathTheShapestealer.java b/Mage.Sets/src/mage/cards/v/VolrathTheShapestealer.java index 8a98f91aeaa..968c9e20b67 100644 --- a/Mage.Sets/src/mage/cards/v/VolrathTheShapestealer.java +++ b/Mage.Sets/src/mage/cards/v/VolrathTheShapestealer.java @@ -91,7 +91,6 @@ class VolrathTheShapestealerEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); Permanent volrathTheShapestealer = game.getPermanent(source.getSourceId()); - Permanent newBluePrint = null; if (controller == null || volrathTheShapestealer == null) { return false; @@ -100,12 +99,12 @@ class VolrathTheShapestealerEffect extends OneShotEffect { if (copyFromCard == null) { return true; } - newBluePrint = new PermanentCard(copyFromCard, source.getControllerId(), game); + //newBluePrint = new PermanentCard(copyFromCard, source.getControllerId(), game); + Card newBluePrint = copyFromCard.copy(); newBluePrint.assignNewId(); CopyApplier applier = new VolrathTheShapestealerCopyApplier(); applier.apply(game, newBluePrint, source, volrathTheShapestealer.getId()); CopyEffect copyEffect = new CopyEffect(Duration.UntilYourNextTurn, newBluePrint, volrathTheShapestealer.getId()); - copyEffect.newId(); copyEffect.setApplier(applier); Ability newAbility = source.copy(); copyEffect.init(newAbility, game); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/copy/CopySpellTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/copy/CopySpellTest.java index 1dfa057c598..e5174bfdfc6 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/copy/CopySpellTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/copy/CopySpellTest.java @@ -889,9 +889,10 @@ public class CopySpellTest extends CardTestPlayerBase { } private void prepareZoneAndZCC(Card originalCard) { - // prepare custom zcc and zone for copy testing + // prepare custom zcc and zone for copy testing (it's not real game) + // HAND zone is safest way (some card types require diff zones for stack/battlefield, e.g. MDFC) originalCard.setZoneChangeCounter(5, currentGame); - originalCard.setZone(Zone.STACK, currentGame); + originalCard.setZone(Zone.HAND, currentGame); } private void cardsMustHaveSameZoneAndZCC(Card originalCard, Card copiedCard, String infoPrefix) { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modaldoublefaced/ModalDoubleFacedCardsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modaldoublefaced/ModalDoubleFacedCardsTest.java index f6d7f704213..1eb571247d4 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modaldoublefaced/ModalDoubleFacedCardsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modaldoublefaced/ModalDoubleFacedCardsTest.java @@ -999,4 +999,33 @@ public class ModalDoubleFacedCardsTest extends CardTestPlayerBase { setStopAt(2, PhaseStep.END_TURN); execute(); } + + @Test + public void test_Battlefield_MustHaveAbilitiesFromOneSideOnly() { + // possible bug: test framework adds second side abilities + + // left side - Reidane, God of the Worthy: + // Snow lands your opponents control enter the battlefield tapped. + // right side - Valkmira, Protector's Shield: + // If a source an opponent controls would deal damage to you or a permanent you control, prevent 1 of that damage. + // Whenever you or another permanent you control becomes the target of a spell or ability an opponent controls, + // counter that spell or ability unless its controller pays {1}. + addCard(Zone.BATTLEFIELD, playerB, "Reidane, God of the Worthy", 1); + // + addCard(Zone.HAND, playerA, "Snow-Covered Forest", 1); + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + + // cast, second side effects must be ignored (e.g. counter trigger) + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Snow-Covered Forest"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt"); + addTarget(playerA, playerB); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertTappedCount("Snow-Covered Forest", true, 1); + assertLife(playerB, 20 - 3); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java index 564f621c30c..d7dfb446b9f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java @@ -22,6 +22,7 @@ import mage.cards.repository.CardRepository; import mage.constants.*; import mage.filter.StaticFilters; import mage.game.Game; +import mage.game.PutToBattlefieldInfo; import mage.game.match.Match; import mage.game.match.MatchType; import mage.game.permanent.PermanentCard; @@ -72,7 +73,7 @@ public abstract class MageTestPlayerBase { protected Pattern pattern = Pattern.compile("([a-zA-Z]*):([\\w]*):([a-zA-Z ,\\-.!'\\d]*):([\\d]*)(:\\{tapped\\})?"); protected Map> handCards = new HashMap<>(); - protected Map> battlefieldCards = new HashMap<>(); + protected Map> battlefieldCards = new HashMap<>(); // cards + additional status like tapped protected Map> graveyardCards = new HashMap<>(); protected Map> libraryCards = new HashMap<>(); protected Map> commandCards = new HashMap<>(); @@ -111,7 +112,7 @@ public abstract class MageTestPlayerBase { EXPECTED } - protected ParserState parserState; + protected ParserState parserState; // TODO: remove outdated and unsued code /** * Expected results of the test. Read from test case in {@link String} based @@ -216,6 +217,7 @@ public abstract class MageTestPlayerBase { } private void parseLine(String line) { + // TODO: delete unused code if (parserState == ParserState.EXPECTED) { expectedResults.add(line); // just remember for future use return; @@ -229,14 +231,14 @@ public abstract class MageTestPlayerBase { if (nickname.startsWith("Computer")) { List cards = null; - List perms = null; + //List perms = null; Zone gameZone; if ("hand".equalsIgnoreCase(zone)) { gameZone = Zone.HAND; cards = getHandCards(getPlayer(nickname)); } else if ("battlefield".equalsIgnoreCase(zone)) { gameZone = Zone.BATTLEFIELD; - perms = getBattlefieldCards(getPlayer(nickname)); + //perms = getBattlefieldCards(getPlayer(nickname)); } else if ("graveyard".equalsIgnoreCase(zone)) { gameZone = Zone.GRAVEYARD; cards = getGraveCards(getPlayer(nickname)); @@ -268,10 +270,10 @@ public abstract class MageTestPlayerBase { Card newCard = cardInfo != null ? cardInfo.getCard() : null; if (newCard != null) { if (gameZone == Zone.BATTLEFIELD) { - Card permCard = CardUtil.getDefaultCardSideForBattlefield(currentGame, newCard); - PermanentCard p = new PermanentCard(permCard, null, currentGame); - p.setTapped(tapped); - perms.add(p); + //Card permCard = CardUtil.getDefaultCardSideForBattlefield(currentGame, newCard); + //PermanentCard p = new PermanentCard(permCard, null, currentGame); + //p.setTapped(tapped); + //perms.add(p); } else { cards.add(newCard); } @@ -339,11 +341,11 @@ public abstract class MageTestPlayerBase { return res; } - protected List getBattlefieldCards(TestPlayer player) { + protected List getBattlefieldCards(TestPlayer player) { if (battlefieldCards.containsKey(player)) { return battlefieldCards.get(player); } - List res = new ArrayList<>(); + List res = new ArrayList<>(); battlefieldCards.put(player, res); return res; } @@ -437,12 +439,13 @@ public abstract class MageTestPlayerBase { CardSetInfo testSet = new CardSetInfo(needCardName, needSetCode, "123", Rarity.COMMON); Card newCard = new CustomTestCard(controllerPlayer.getId(), testSet, cardType, spellCost); - Card permCard = CardUtil.getDefaultCardSideForBattlefield(currentGame, newCard); - PermanentCard permanent = new PermanentCard(permCard, controllerPlayer.getId(), currentGame); switch (putAtZone) { case BATTLEFIELD: - getBattlefieldCards(controllerPlayer).add(permanent); + getBattlefieldCards(controllerPlayer).add(new PutToBattlefieldInfo( + newCard, + false + )); break; case GRAVEYARD: getGraveCards(controllerPlayer).add(newCard); @@ -565,7 +568,7 @@ public abstract class MageTestPlayerBase { } } -// custom card with global abilities list to init (can contains abilities per card name) +// custom card with global abilities list to init (can contain abilities per card name) class CustomTestCard extends CardImpl { static private final Map> abilitiesList = new HashMap<>(); // card name -> abilities diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index 1ee5304f785..0128bfa6256 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -631,7 +631,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement } /** - * Add any amount of cards to specified zone of specified player. + * Add any amount of cards to specified zone of specified player without resolve/ETB * * @param gameZone {@link mage.constants.Zone} to add cards to. * @param player {@link Player} to add cards for. Use either playerA or @@ -684,14 +684,13 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement if (gameZone == Zone.BATTLEFIELD) { for (int i = 0; i < count; i++) { Card newCard = cardInfo.getCard(); - Card permCard = CardUtil.getDefaultCardSideForBattlefield(currentGame, newCard); - - PermanentCard p = new PermanentCard(permCard, player.getId(), currentGame); - p.setTapped(tapped); - getBattlefieldCards(player).add(p); - + getBattlefieldCards(player).add(new PutToBattlefieldInfo( + newCard, + tapped + )); if (!aliasName.isEmpty()) { - player.addAlias(player.generateAliasName(aliasName, useAliasMultiNames, i + 1), p.getId()); + // TODO: is it bugged with double faced cards (wrong ref)? + player.addAlias(player.generateAliasName(aliasName, useAliasMultiNames, i + 1), newCard.getId()); } } } else { diff --git a/Mage.Tests/src/test/java/org/mage/test/testapi/AddCardApiTest.java b/Mage.Tests/src/test/java/org/mage/test/testapi/AddCardApiTest.java index b97d6030340..873e4a7f18a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/testapi/AddCardApiTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/testapi/AddCardApiTest.java @@ -78,14 +78,16 @@ public class AddCardApiTest extends CardTestPlayerBase { execute(); assertPermanentCount(playerA, "Memorial to Glory", 2); - getBattlefieldCards(playerA).stream() - .filter(card -> card.getName().equals("Memorial to Glory")) - .forEach(card -> Assert.assertEquals("40K", card.getExpansionSetCode())); + getBattlefieldCards(playerA) + .stream() + .filter(info -> info.getCard().getName().equals("Memorial to Glory")) + .forEach(info -> Assert.assertEquals("40K", info.getCard().getExpansionSetCode())); assertPermanentCount(playerA, "Plains", 2); - getBattlefieldCards(playerA).stream() - .filter(card -> card.getName().equals("Plains")) - .forEach(card -> Assert.assertEquals("PANA", card.getExpansionSetCode())); + getBattlefieldCards(playerA) + .stream() + .filter(info -> info.getCard().getName().equals("Plains")) + .forEach(info -> Assert.assertEquals("PANA", info.getCard().getExpansionSetCode())); } @Test(expected = org.junit.ComparisonFailure.class) diff --git a/Mage.Tests/src/test/java/org/mage/test/turnmod/ExtraTurnsTest.java b/Mage.Tests/src/test/java/org/mage/test/turnmod/ExtraTurnsTest.java index def70e36960..38c45bf3f5c 100644 --- a/Mage.Tests/src/test/java/org/mage/test/turnmod/ExtraTurnsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/turnmod/ExtraTurnsTest.java @@ -16,7 +16,7 @@ public class ExtraTurnsTest extends CardTestPlayerBase { private void checkTurnControl(int turn, TestPlayer needTurnController, boolean isExtraTurn) { runCode("checking turn " + turn, turn, PhaseStep.POSTCOMBAT_MAIN, playerA, (info, player, game) -> { Player defaultTurnController = game.getPlayer(game.getActivePlayerId()); - Player realTurnController = defaultTurnController.getTurnControlledBy() == null ? defaultTurnController : game.getPlayer(defaultTurnController.getTurnControlledBy()); + Player realTurnController = game.getPlayer(defaultTurnController.getTurnControlledBy()); Assert.assertEquals(String.format("turn %d must be controlled by %s", turn, needTurnController.getName()), needTurnController.getName(), realTurnController.getName()); Assert.assertEquals(String.format("turn %d must be %s", turn, (isExtraTurn ? "extra turn" : "normal turn")), diff --git a/Mage/src/main/java/mage/abilities/effects/common/LoseControlOnOtherPlayersControllerEffect.java b/Mage/src/main/java/mage/abilities/effects/common/LoseControlOnOtherPlayersControllerEffect.java index 1e1b9bbc81e..691553c7f6a 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/LoseControlOnOtherPlayersControllerEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/LoseControlOnOtherPlayersControllerEffect.java @@ -9,6 +9,7 @@ import mage.game.Game; import mage.players.Player; /** + * TODO: delete, there are already end turn code with control reset * @author nantuko */ public class LoseControlOnOtherPlayersControllerEffect extends OneShotEffect { diff --git a/Mage/src/main/java/mage/cards/ModalDoubleFacedCard.java b/Mage/src/main/java/mage/cards/ModalDoubleFacedCard.java index dafe544bfb8..7e637f5635a 100644 --- a/Mage/src/main/java/mage/cards/ModalDoubleFacedCard.java +++ b/Mage/src/main/java/mage/cards/ModalDoubleFacedCard.java @@ -94,15 +94,29 @@ public abstract class ModalDoubleFacedCard extends CardImpl implements CardWithH @Override public void setCopy(boolean isCopy, MageObject copiedFrom) { super.setCopy(isCopy, copiedFrom); - leftHalfCard.setCopy(isCopy, copiedFrom); + leftHalfCard.setCopy(isCopy, copiedFrom); // TODO: must check copiedFrom and assign sides? (??? related to #8476 ???) rightHalfCard.setCopy(isCopy, copiedFrom); } + private void setSideZones(Zone mainZone, Game game) { + switch (mainZone) { + case BATTLEFIELD: + case STACK: + throw new IllegalArgumentException("Wrong code usage: you must put to battlefield/stack only real side card (half), not main"); + default: + // must keep both sides in same zone cause xmage need access to cost reduction, spell + // and other abilities before put it to stack (in playable calcs) + game.setZone(leftHalfCard.getId(), mainZone); + game.setZone(rightHalfCard.getId(), mainZone); + break; + } + checkGoodZones(game, this); + } + @Override public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List appliedEffects) { if (super.moveToZone(toZone, source, game, flag, appliedEffects)) { - game.getState().setZone(leftHalfCard.getId(), toZone); - game.getState().setZone(rightHalfCard.getId(), toZone); + setSideZones(toZone, game); return true; } return false; @@ -111,21 +125,69 @@ public abstract class ModalDoubleFacedCard extends CardImpl implements CardWithH @Override public void setZone(Zone zone, Game game) { super.setZone(zone, game); - game.setZone(leftHalfCard.getId(), zone); - game.setZone(rightHalfCard.getId(), zone); + setSideZones(zone, game); } @Override public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List appliedEffects) { if (super.moveToExile(exileId, name, source, game, appliedEffects)) { - Zone currentZone = game.getState().getZone(getId()); - game.getState().setZone(leftHalfCard.getId(), currentZone); - game.getState().setZone(rightHalfCard.getId(), currentZone); + setSideZones(Zone.EXILED, game); return true; } return false; } + /** + * Runtime check for good zones and other MDF data + */ + public static void checkGoodZones(Game game, ModalDoubleFacedCard card) { + Card leftPart = card.getLeftHalfCard(); + Card rightPart = card.getRightHalfCard(); + + Zone zoneMain = game.getState().getZone(card.getId()); + Zone zoneLeft = game.getState().getZone(leftPart.getId()); + Zone zoneRight = game.getState().getZone(rightPart.getId()); + + // runtime check: + // * in battlefield and stack - card + one of the sides (another side in outside zone) + // * in other zones - card + both sides (need both sides due cost reductions, spell and other access before put to stack) + // + // 712.8a While a double-faced card is outside the game or in a zone other than the battlefield or stack, + // it has only the characteristics of its front face. + // + // 712.8f While a modal double-faced spell is on the stack or a modal double-faced permanent is on the battlefield, + // it has only the characteristics of the face that’s up. + Zone needZoneLeft; + Zone needZoneRight; + switch (zoneMain) { + case BATTLEFIELD: + case STACK: + if (zoneMain == zoneLeft) { + needZoneLeft = zoneMain; + needZoneRight = Zone.OUTSIDE; + } else if (zoneMain == zoneRight) { + needZoneLeft = Zone.OUTSIDE; + needZoneRight = zoneMain; + } else { + // impossible + needZoneLeft = zoneMain; + needZoneRight = Zone.OUTSIDE; + } + break; + default: + needZoneLeft = zoneMain; + needZoneRight = zoneMain; + break; + } + + if (zoneLeft != needZoneLeft || zoneRight != needZoneRight) { + throw new IllegalStateException("Wrong code usage: MDF card uses wrong zones - " + card + + "\r\n" + String.format("* main zone: %s", zoneMain) + + "\r\n" + String.format("* left side: need %s, actual %s", needZoneLeft, zoneLeft) + + "\r\n" + String.format("* right side: need %s, actual %s", needZoneRight, zoneRight)); + } + } + @Override public boolean removeFromZone(Game game, Zone fromZone, Ability source) { // zone contains only one main card diff --git a/Mage/src/main/java/mage/cards/ModalDoubleFacedCardHalfImpl.java b/Mage/src/main/java/mage/cards/ModalDoubleFacedCardHalfImpl.java index 917d2ecd590..2521b78aee8 100644 --- a/Mage/src/main/java/mage/cards/ModalDoubleFacedCardHalfImpl.java +++ b/Mage/src/main/java/mage/cards/ModalDoubleFacedCardHalfImpl.java @@ -71,9 +71,32 @@ public class ModalDoubleFacedCardHalfImpl extends CardImpl implements ModalDoubl @Override public void setZone(Zone zone, Game game) { + // see ModalDoubleFacedCard.checkGoodZones for details game.setZone(parentCard.getId(), zone); - game.setZone(parentCard.getLeftHalfCard().getId(), zone); - game.setZone(parentCard.getRightHalfCard().getId(), zone); + game.setZone(this.getId(), zone); + + // find another side to sync + ModalDoubleFacedCardHalf otherSide; + if (!parentCard.getLeftHalfCard().getId().equals(this.getId())) { + otherSide = parentCard.getLeftHalfCard(); + } else if (!parentCard.getRightHalfCard().getId().equals(this.getId())) { + otherSide = parentCard.getRightHalfCard(); + } else { + throw new IllegalStateException("Wrong code usage: MDF halves must use different ids"); + } + + switch (zone) { + case STACK: + case BATTLEFIELD: + // stack and battlefield must have only one side + game.setZone(otherSide.getId(), Zone.OUTSIDE); + break; + default: + game.setZone(otherSide.getId(), zone); + break; + } + + ModalDoubleFacedCard.checkGoodZones(game, parentCard); } @Override diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index c700478852b..51a9dbcd3b1 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -555,7 +555,7 @@ public interface Game extends MageItem, Serializable, Copyable { // game cheats (for tests only) void cheat(UUID ownerId, Map commands); - void cheat(UUID ownerId, List library, List hand, List battlefield, List graveyard, List command, List exiled); + void cheat(UUID ownerId, List library, List hand, List battlefield, List graveyard, List command, List exiled); // controlling the behaviour of replacement effects while permanents entering the battlefield void setScopeRelevant(boolean scopeRelevant); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index eb440aff64c..02e24289503 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -288,6 +288,8 @@ public abstract class GameImpl implements Game { public void loadCards(Set cards, UUID ownerId) { for (Card card : cards) { if (card instanceof PermanentCard) { + // TODO: impossible use case, can be deleted? + // trying to put permanent card to battlefield card = ((PermanentCard) card).getCard(); } @@ -2019,11 +2021,10 @@ public abstract class GameImpl implements Game { // save original copy link (handle copy of copies too) newBluePrint.setCopy(true, (copyFromPermanent.getCopyFrom() != null ? copyFromPermanent.getCopyFrom() : copyFromPermanent)); - CopyEffect newEffect = new CopyEffect(duration, newBluePrint, copyToPermanentId); - newEffect.newId(); - newEffect.setApplier(applier); + CopyEffect newCopyEffect = new CopyEffect(duration, newBluePrint, copyToPermanentId); + newCopyEffect.setApplier(applier); Ability newAbility = source.copy(); - newEffect.init(newAbility, this); + newCopyEffect.init(newAbility, this); // If there are already copy effects with duration = Custom to the same object, remove the existing effects because they no longer have any effect if (duration == Duration.Custom) { @@ -2037,7 +2038,7 @@ public abstract class GameImpl implements Game { } } } - state.addEffect(newEffect, newAbility); + state.addEffect(newCopyEffect, newAbility); return newBluePrint; } @@ -3567,20 +3568,27 @@ public abstract class GameImpl implements Game { } @Override - public void cheat(UUID ownerId, List library, List hand, List battlefield, List graveyard, List command, List exiled) { + public void cheat(UUID ownerId, List library, List hand, List battlefield, List graveyard, List command, List exiled) { // fake test ability for triggers and events Ability fakeSourceAbilityTemplate = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("adding testing cards")); fakeSourceAbilityTemplate.setControllerId(ownerId); Player player = getPlayer(ownerId); if (player != null) { + // init cards loadCards(ownerId, library); loadCards(ownerId, hand); - loadCards(ownerId, battlefield); + loadCards(ownerId, battlefield + .stream() + .map(PutToBattlefieldInfo::getCard) + .collect(Collectors.toList()) + ); loadCards(ownerId, graveyard); loadCards(ownerId, command); loadCards(ownerId, exiled); + // move cards to zones + for (Card card : library) { player.getLibrary().putOnTop(card, this); } @@ -3610,10 +3618,10 @@ public abstract class GameImpl implements Game { getExile().add(card); } - for (PermanentCard permanentCard : battlefield) { + for (PutToBattlefieldInfo info : battlefield) { Ability fakeSourceAbility = fakeSourceAbilityTemplate.copy(); - fakeSourceAbility.setSourceId(permanentCard.getId()); - CardUtil.putCardOntoBattlefieldWithEffects(fakeSourceAbility, this, permanentCard, player); + fakeSourceAbility.setSourceId(info.getCard().getId()); + CardUtil.putCardOntoBattlefieldWithEffects(fakeSourceAbility, this, info.getCard(), player, info.isTapped()); } applyEffects(); diff --git a/Mage/src/main/java/mage/game/PutToBattlefieldInfo.java b/Mage/src/main/java/mage/game/PutToBattlefieldInfo.java new file mode 100644 index 00000000000..404cde602e6 --- /dev/null +++ b/Mage/src/main/java/mage/game/PutToBattlefieldInfo.java @@ -0,0 +1,27 @@ +package mage.game; + +import mage.cards.Card; + +/** + * For tests only: put to battlefield with additional settings like tapped + * + * @author JayDi85 + */ +public class PutToBattlefieldInfo { + + private final Card card; + private final boolean tapped; + + public PutToBattlefieldInfo(Card card, boolean tapped) { + this.card = card; + this.tapped = tapped; + } + + public Card getCard() { + return card; + } + + public boolean isTapped() { + return tapped; + } +} diff --git a/Mage/src/main/java/mage/game/ZonesHandler.java b/Mage/src/main/java/mage/game/ZonesHandler.java index 96c78f0d90c..ee318df9fa1 100644 --- a/Mage/src/main/java/mage/game/ZonesHandler.java +++ b/Mage/src/main/java/mage/game/ZonesHandler.java @@ -439,6 +439,7 @@ public final class ZonesHandler { if (Zone.STACK == event.getFromZone()) { Spell spell = game.getStack().getSpell(event.getTargetId()); if (spell != null && !spell.isFaceDown(game)) { + // TODO: wtf, why only colors!? Must research and remove colors workaround if (!card.getColor(game).equals(spell.getColor(game))) { // the card that is referenced to in the permanent is copied and the spell attributes are set to this copied card card.getColor(game).setColor(spell.getColor(game)); diff --git a/Mage/src/main/java/mage/game/permanent/PermanentCard.java b/Mage/src/main/java/mage/game/permanent/PermanentCard.java index 9946f8cdd55..ef2716b5e45 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentCard.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentCard.java @@ -14,6 +14,7 @@ import mage.cards.SplitCard; import mage.constants.SpellAbilityType; import mage.game.Game; import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.token.Token; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; @@ -25,18 +26,23 @@ import java.util.UUID; * * @author BetaSteward_at_googlemail.com */ -@SupportedSourceVersion(SourceVersion.RELEASE_8) public class PermanentCard extends PermanentImpl { protected int maxLevelCounters; - // A copy of the origin card that was cast (this is not the original card, so it's possible to change some attribute to this blueprint to change attributes to the permanent if it enters the battlefield with e.g. a subtype) - protected Card card; + // A copy of the original card that was cast (this is not the original card, so it's possible to change some attribute to this blueprint to change attributes to the permanent if it enters the battlefield with e.g. a subtype) + protected Card card; // TODO: wtf, it modified on getCard and other places, e.g. on bestow -- must be fixed! // the number this permanent instance had protected int zoneChangeCounter; public PermanentCard(Card card, UUID controllerId, Game game) { super(card.getId(), card.getOwnerId(), controllerId, card.getName()); + // runtime check: must use real card only inside + if (card instanceof PermanentCard) { + // TODO: allow? + throw new IllegalArgumentException("Wrong code usage: can't use PermanentCard inside another PermanentCard"); + } + // usage check: you must put to play only real card's part // if you use it in test code then call CardUtil.getDefaultCardSideForBattlefield for default side // it's a basic check and still allows to create permanent from instant or sorcery @@ -101,6 +107,7 @@ public class PermanentCard extends PermanentImpl { } protected void copyFromCard(final Card card, final Game game) { + // TODO: must research - is it copy all fields or something miss this.name = card.getName(); this.abilities.clear(); if (this.faceDown) { @@ -224,6 +231,10 @@ public class PermanentCard extends PermanentImpl { @Override public String toString() { - return card.toString(); + return card.toString() + + ", " + ((this instanceof Token) ? "T" : "C") + + (this.isCopy() ? ", copy" : "") + + ", " + this.getPower() + "/" + this.getToughness() + + (this.isTapped() ? ", tapped" : ""); } } diff --git a/Mage/src/main/java/mage/game/stack/SpellStack.java b/Mage/src/main/java/mage/game/stack/SpellStack.java index 14c4c0efb1c..389495d22e4 100644 --- a/Mage/src/main/java/mage/game/stack/SpellStack.java +++ b/Mage/src/main/java/mage/game/stack/SpellStack.java @@ -143,6 +143,6 @@ public class SpellStack extends ArrayDeque { @Override public String toString() { - return this.size() + (this.isEmpty() ? "" : " (top: " + CardUtil.substring(this.getFirst().toString(), 100) + ")"); + return this.size() + (this.isEmpty() ? "" : " (top: " + CardUtil.substring(this.getFirst().toString(), 100, "...") + ")"); } } diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 43c8db88362..51ef25d4480 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -294,10 +294,8 @@ public interface Player extends MageItem, Copyable { void setTopCardRevealed(boolean topCardRevealed); /** - * Get data from the client Preferences (e.g. avatarId or - * showAbilityPickerForce) - * - * @return + * User's settings like avatar or skip buttons. + * WARNING, game related code must use controlling player settings only, e.g. getControllingPlayersUserData */ UserData getUserData(); @@ -333,6 +331,9 @@ public interface Player extends MageItem, Copyable { List getTurnControllers(); + /** + * Current turn controller for a player (return own id for own control) + */ UUID getTurnControlledBy(); /** @@ -360,6 +361,11 @@ public interface Player extends MageItem, Copyable { */ void setGameUnderYourControl(boolean value); + /** + * Return player's turn control to prev player + * @param value + * @param fullRestore return turn control to own + */ void setGameUnderYourControl(boolean value, boolean fullRestore); void setTestMode(boolean value); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index d0d917f5a8e..481032d918a 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -159,10 +159,12 @@ public abstract class PlayerImpl implements Player, Serializable { protected FilterPermanent sacrificeCostFilter; protected final List alternativeSourceCosts = new ArrayList<>(); - protected boolean isGameUnderControl = true; - protected UUID turnController; - protected List turnControllers = new ArrayList<>(); - protected Set playersUnderYourControl = new HashSet<>(); + // TODO: rework turn controller to use single list (see other todos) + //protected Stack allTurnControllers = new Stack<>(); + protected boolean isGameUnderControl = true; // TODO: replace with allTurnControllers.isEmpty + protected UUID turnController; // null on own control TODO: replace with allTurnControllers.last + protected List turnControllers = new ArrayList<>(); // TODO: remove + protected Set playersUnderYourControl = new HashSet<>(); // TODO: replace with game method and search in allTurnControllers protected Set usersAllowedToSeeHandCards = new HashSet<>(); protected List attachments = new ArrayList<>(); @@ -263,14 +265,14 @@ public abstract class PlayerImpl implements Player, Serializable { this.storedBookmark = player.storedBookmark; this.topCardRevealed = player.topCardRevealed; - this.playersUnderYourControl.addAll(player.playersUnderYourControl); this.usersAllowedToSeeHandCards.addAll(player.usersAllowedToSeeHandCards); this.isTestMode = player.isTestMode; - this.isGameUnderControl = player.isGameUnderControl; + this.isGameUnderControl = player.isGameUnderControl; this.turnController = player.turnController; this.turnControllers.addAll(player.turnControllers); + this.playersUnderYourControl.addAll(player.playersUnderYourControl); this.passed = player.passed; this.passedTurn = player.passedTurn; @@ -363,13 +365,14 @@ public abstract class PlayerImpl implements Player, Serializable { this.alternativeSourceCosts.addAll(player.getAlternativeSourceCosts()); this.topCardRevealed = player.isTopCardRevealed(); - this.playersUnderYourControl.clear(); - this.playersUnderYourControl.addAll(player.getPlayersUnderYourControl()); - this.isGameUnderControl = player.isGameUnderControl(); - this.turnController = player.getTurnControlledBy(); + this.isGameUnderControl = player.isGameUnderControl(); + this.turnController = this.getId().equals(player.getTurnControlledBy()) ? null : player.getTurnControlledBy(); this.turnControllers.clear(); this.turnControllers.addAll(player.getTurnControllers()); + this.playersUnderYourControl.clear(); + this.playersUnderYourControl.addAll(player.getPlayersUnderYourControl()); + this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving(); this.clearCastSourceIdManaCosts(); @@ -607,6 +610,9 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public void setTurnControlledBy(UUID playerId) { + if (playerId == null) { + throw new IllegalArgumentException("Can't add unknown player to turn controllers: " + playerId); + } this.turnController = playerId; this.turnControllers.add(playerId); } @@ -618,7 +624,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public UUID getTurnControlledBy() { - return this.turnController; + return this.turnController == null ? this.getId() : this.turnController; } @Override @@ -647,14 +653,18 @@ public abstract class PlayerImpl implements Player, Serializable { this.isGameUnderControl = value; if (isGameUnderControl) { if (fullRestore) { + // to own this.turnControllers.clear(); - this.turnController = getId(); + this.turnController = null; + this.isGameUnderControl = true; } else { + // to prev player if (!turnControllers.isEmpty()) { this.turnControllers.remove(turnControllers.size() - 1); } if (turnControllers.isEmpty()) { - this.turnController = getId(); + this.turnController = null; + this.isGameUnderControl = true; } else { this.turnController = turnControllers.get(turnControllers.size() - 1); isGameUnderControl = false; @@ -2515,7 +2525,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public void concede(Game game) { game.setConcedingPlayer(playerId); - lost(game); + + lost(game); // it's ok to be ignored by "can't lose abilities" here (setConcedingPlayer done all work above) } @Override diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 8298764ec4b..514dbb41937 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -1082,16 +1082,26 @@ public final class CardUtil { } /** - * Put card to battlefield without resolve (for cheats and tests only) + * Put card to battlefield without resolve/ETB (for cheats and tests only) * - * @param source must be non null (if you need it empty then use fakeSourceAbility) + * @param source must be non-null (if you need it empty then use fakeSourceAbility) * @param game * @param newCard * @param player */ - public static void putCardOntoBattlefieldWithEffects(Ability source, Game game, Card newCard, Player player) { + public static void putCardOntoBattlefieldWithEffects(Ability source, Game game, Card newCard, Player player, boolean tapped) { // same logic as ZonesHandler->maybeRemoveFromSourceZone + // runtime check: must have source + if (source == null) { + throw new IllegalArgumentException("Wrong code usage: must use source ability or fakeSourceAbility"); + } + + // runtime check: must use only real cards + if (newCard instanceof PermanentCard) { + throw new IllegalArgumentException("Wrong code usage: must put to battlefield only real cards, not PermanentCard"); + } + // workaround to put real permanent from one side (example: you call mdf card by cheats) Card permCard = getDefaultCardSideForBattlefield(game, newCard); @@ -1105,15 +1115,16 @@ public final class CardUtil { permanent = new PermanentCard(permCard, player.getId(), game); } - // put onto battlefield with possible counters + // put onto battlefield with possible counters without ETB game.getPermanentsEntering().put(permanent.getId(), permanent); permCard.checkForCountersToAdd(permanent, source, game); permanent.entersBattlefield(source, game, Zone.OUTSIDE, false); game.addPermanent(permanent, game.getState().getNextPermanentOrderNumber()); game.getPermanentsEntering().remove(permanent.getId()); - // workaround for special tapped status from test framework's command (addCard) - if (permCard instanceof PermanentCard && ((PermanentCard) permCard).isTapped()) { + // tapped status + // warning, "enters the battlefield tapped" abilities will be executed before, so don't set to false here + if (tapped) { permanent.setTapped(true); }