1
0
Fork 0
mirror of https://github.com/correl/mage.git synced 2025-04-13 01:01:11 -09:00

Merge pull request from magefree/master

merge
This commit is contained in:
theelk801 2017-08-26 19:59:51 -04:00 committed by GitHub
commit 22d253acd7
16 changed files with 420 additions and 206 deletions

View file

@ -705,7 +705,7 @@ public class HumanPlayer extends PlayerImpl {
// it's a main phase // it's a main phase
if (!skippedAtLeastOnce if (!skippedAtLeastOnce
|| (!playerId.equals(game.getActivePlayerId()) || (!playerId.equals(game.getActivePlayerId())
&& !this.getUserData().getUserSkipPrioritySteps().isStopOnAllMainPhases())) { && !controllingPlayer.getUserData().getUserSkipPrioritySteps().isStopOnAllMainPhases())) {
skippedAtLeastOnce = true; skippedAtLeastOnce = true;
if (passWithManaPoolCheck(game)) { if (passWithManaPoolCheck(game)) {
return false; return false;
@ -726,7 +726,7 @@ public class HumanPlayer extends PlayerImpl {
// It's end of turn phase // It's end of turn phase
if (!skippedAtLeastOnce if (!skippedAtLeastOnce
|| (playerId.equals(game.getActivePlayerId()) || (playerId.equals(game.getActivePlayerId())
&& !this.getUserData().getUserSkipPrioritySteps().isStopOnAllEndPhases())) { && !controllingPlayer.getUserData().getUserSkipPrioritySteps().isStopOnAllEndPhases())) {
skippedAtLeastOnce = true; skippedAtLeastOnce = true;
if (passWithManaPoolCheck(game)) { if (passWithManaPoolCheck(game)) {
return false; return false;
@ -834,7 +834,7 @@ public class HumanPlayer extends PlayerImpl {
return !controllingPlayer.getUserData().getUserSkipPrioritySteps().getOpponentTurn().isPhaseStepSet(game.getStep().getType()); return !controllingPlayer.getUserData().getUserSkipPrioritySteps().getOpponentTurn().isPhaseStepSet(game.getStep().getType());
} }
} catch (NullPointerException ex) { } catch (NullPointerException ex) {
String isNull = userData == null ? "null" : "not null"; String isNull = controllingPlayer.getUserData() == null ? "null" : "not null";
logger.error("null pointer exception UserData = " + isNull); logger.error("null pointer exception UserData = " + isNull);
} }
return true; return true;
@ -843,7 +843,7 @@ public class HumanPlayer extends PlayerImpl {
@Override @Override
public TriggeredAbility chooseTriggeredAbility(List<TriggeredAbility> abilities, Game game) { public TriggeredAbility chooseTriggeredAbility(List<TriggeredAbility> abilities, Game game) {
String autoOrderRuleText = null; String autoOrderRuleText = null;
boolean autoOrderUse = getUserData().isAutoOrderTrigger(); boolean autoOrderUse = getControllingPlayersUserData(game).isAutoOrderTrigger();
while (!abort) { while (!abort) {
// try to set trigger auto order // try to set trigger auto order
List<TriggeredAbility> abilitiesWithNoOrderSet = new ArrayList<>(); List<TriggeredAbility> abilitiesWithNoOrderSet = new ArrayList<>();
@ -1045,7 +1045,7 @@ public class HumanPlayer extends PlayerImpl {
while (!abort) { while (!abort) {
if (passedAllTurns if (passedAllTurns
|| passedUntilEndStepBeforeMyTurn || passedUntilEndStepBeforeMyTurn
|| (!getUserData().getUserSkipPrioritySteps().isStopOnDeclareAttackersDuringSkipAction() || (!getControllingPlayersUserData(game).getUserSkipPrioritySteps().isStopOnDeclareAttackersDuringSkipAction()
&& (passedTurn && (passedTurn
|| passedTurnSkipStack || passedTurnSkipStack
|| passedUntilEndOfTurn || passedUntilEndOfTurn
@ -1223,7 +1223,7 @@ public class HumanPlayer extends PlayerImpl {
FilterCreatureForCombatBlock filter = filterCreatureForCombatBlock.copy(); FilterCreatureForCombatBlock filter = filterCreatureForCombatBlock.copy();
filter.add(new ControllerIdPredicate(defendingPlayerId)); filter.add(new ControllerIdPredicate(defendingPlayerId));
if (game.getBattlefield().count(filter, null, playerId, game) == 0 if (game.getBattlefield().count(filter, null, playerId, game) == 0
&& !getUserData().getUserSkipPrioritySteps().isStopOnDeclareBlockerIfNoneAvailable()) { && !getControllingPlayersUserData(game).getUserSkipPrioritySteps().isStopOnDeclareBlockerIfNoneAvailable()) {
return; return;
} }
while (!abort) { while (!abort) {
@ -1420,7 +1420,7 @@ public class HumanPlayer extends PlayerImpl {
protected void activateAbility(LinkedHashMap<UUID, ? extends ActivatedAbility> abilities, MageObject object, Game game) { protected void activateAbility(LinkedHashMap<UUID, ? extends ActivatedAbility> abilities, MageObject object, Game game) {
updateGameStatePriority("activateAbility", game); updateGameStatePriority("activateAbility", game);
if (abilities.size() == 1 if (abilities.size() == 1
&& suppressAbilityPicker(abilities.values().iterator().next())) { && suppressAbilityPicker(abilities.values().iterator().next(), game)) {
ActivatedAbility ability = abilities.values().iterator().next(); ActivatedAbility ability = abilities.values().iterator().next();
if (!ability.getTargets().isEmpty() if (!ability.getTargets().isEmpty()
|| !(ability.getCosts().size() == 1 || !(ability.getCosts().size() == 1
@ -1452,8 +1452,8 @@ public class HumanPlayer extends PlayerImpl {
} }
} }
private boolean suppressAbilityPicker(ActivatedAbility ability) { private boolean suppressAbilityPicker(ActivatedAbility ability, Game game) {
if (this.getUserData().isShowAbilityPickerForced()) { if (getControllingPlayersUserData(game).isShowAbilityPickerForced()) {
if (ability instanceof PlayLandAbility) { if (ability instanceof PlayLandAbility) {
return true; return true;
} }

View file

@ -27,6 +27,13 @@
*/ */
package mage.server; package mage.server;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import mage.cards.repository.CardInfo; import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository; import mage.cards.repository.CardRepository;
import mage.server.exceptions.UserNotFoundException; import mage.server.exceptions.UserNotFoundException;
@ -36,11 +43,6 @@ import mage.view.ChatMessage.MessageType;
import mage.view.ChatMessage.SoundToPlay; import mage.view.ChatMessage.SoundToPlay;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
@ -51,6 +53,7 @@ public enum ChatManager {
private static final HashMap<String, String> userMessages = new HashMap<>(); private static final HashMap<String, String> userMessages = new HashMap<>();
private final ConcurrentHashMap<UUID, ChatSession> chatSessions = new ConcurrentHashMap<>(); private final ConcurrentHashMap<UUID, ChatSession> chatSessions = new ConcurrentHashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public UUID createChatSession(String info) { public UUID createChatSession(String info) {
ChatSession chatSession = new ChatSession(info); ChatSession chatSession = new ChatSession(info);
@ -68,6 +71,10 @@ public enum ChatManager {
} }
public void clearUserMessageStorage() {
userMessages.clear();
}
public void leaveChat(UUID chatId, UUID userId) { public void leaveChat(UUID chatId, UUID userId) {
ChatSession chatSession = chatSessions.get(chatId); ChatSession chatSession = chatSessions.get(chatId);
if (chatSession != null && chatSession.hasUser(userId)) { if (chatSession != null && chatSession.hasUser(userId)) {
@ -81,7 +88,13 @@ public enum ChatManager {
if (chatSession != null) { if (chatSession != null) {
synchronized (chatSession) { synchronized (chatSession) {
if (chatSessions.containsKey(chatId)) { if (chatSessions.containsKey(chatId)) {
chatSessions.remove(chatId); final Lock w = lock.writeLock();
w.lock();
try {
chatSessions.remove(chatId);
} finally {
w.unlock();
}
logger.trace("Chat removed - chatId: " + chatId); logger.trace("Chat removed - chatId: " + chatId);
} else { } else {
logger.trace("Chat to destroy does not exist - chatId: " + chatId); logger.trace("Chat to destroy does not exist - chatId: " + chatId);
@ -263,10 +276,11 @@ public enum ChatManager {
* @param userId * @param userId
* @param message * @param message
* @param color * @param color
* @throws mage.server.exceptions.UserNotFoundException
*/ */
public void broadcast(UUID userId, String message, MessageColor color) throws UserNotFoundException { public void broadcast(UUID userId, String message, MessageColor color) throws UserNotFoundException {
UserManager.instance.getUser(userId).ifPresent(user -> { UserManager.instance.getUser(userId).ifPresent(user -> {
chatSessions.values() getChatSessions()
.stream() .stream()
.filter(chat -> chat.hasUser(userId)) .filter(chat -> chat.hasUser(userId))
.forEach(session -> session.broadcast(user.getName(), message, color, true, MessageType.TALK, null)); .forEach(session -> session.broadcast(user.getName(), message, color, true, MessageType.TALK, null));
@ -276,15 +290,15 @@ public enum ChatManager {
public void sendReconnectMessage(UUID userId) { public void sendReconnectMessage(UUID userId) {
UserManager.instance.getUser(userId).ifPresent(user UserManager.instance.getUser(userId).ifPresent(user
-> chatSessions.values() -> getChatSessions()
.stream() .stream()
.filter(chat -> chat.hasUser(userId)) .filter(chat -> chat.hasUser(userId))
.forEach(chatSession -> chatSession.broadcast(null, user.getName() + " has reconnected", MessageColor.BLUE, true, MessageType.STATUS, null))); .forEach(chatSession -> chatSession.broadcast(null, user.getName() + " has reconnected", MessageColor.BLUE, true, MessageType.STATUS, null)));
} }
public void removeUser(UUID userId, DisconnectReason reason) { public void removeUser(UUID userId, DisconnectReason reason) {
for (ChatSession chatSession : chatSessions.values()) { for (ChatSession chatSession : getChatSessions()) {
if (chatSession.hasUser(userId)) { if (chatSession.hasUser(userId)) {
chatSession.kill(userId, reason); chatSession.kill(userId, reason);
} }
@ -292,7 +306,13 @@ public enum ChatManager {
} }
public List<ChatSession> getChatSessions() { public List<ChatSession> getChatSessions() {
return new ArrayList<>(chatSessions.values()); final Lock r = lock.readLock();
r.lock();
try {
return new ArrayList<>(chatSessions.values());
} finally {
r.unlock();
}
} }
} }

View file

@ -30,6 +30,9 @@ package mage.server;
import java.text.DateFormat; import java.text.DateFormat;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import mage.interfaces.callback.ClientCallback; import mage.interfaces.callback.ClientCallback;
import mage.interfaces.callback.ClientCallbackMethod; import mage.interfaces.callback.ClientCallbackMethod;
import mage.view.ChatMessage; import mage.view.ChatMessage;
@ -46,6 +49,8 @@ public class ChatSession {
private static final Logger logger = Logger.getLogger(ChatSession.class); private static final Logger logger = Logger.getLogger(ChatSession.class);
private static final DateFormat timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT); private static final DateFormat timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT);
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final ConcurrentHashMap<UUID, String> clients = new ConcurrentHashMap<>(); private final ConcurrentHashMap<UUID, String> clients = new ConcurrentHashMap<>();
private final UUID chatId; private final UUID chatId;
private final Date createTime; private final Date createTime;
@ -61,7 +66,13 @@ public class ChatSession {
UserManager.instance.getUser(userId).ifPresent(user -> { UserManager.instance.getUser(userId).ifPresent(user -> {
if (!clients.containsKey(userId)) { if (!clients.containsKey(userId)) {
String userName = user.getName(); String userName = user.getName();
clients.put(userId, userName); final Lock w = lock.writeLock();
w.lock();
try {
clients.put(userId, userName);
} finally {
w.unlock();
}
broadcast(null, userName + " has joined (" + user.getClientVersion() + ')', MessageColor.BLUE, true, MessageType.STATUS, null); broadcast(null, userName + " has joined (" + user.getClientVersion() + ')', MessageColor.BLUE, true, MessageType.STATUS, null);
logger.trace(userName + " joined chat " + chatId); logger.trace(userName + " joined chat " + chatId);
} }
@ -77,8 +88,14 @@ public class ChatSession {
} }
if (userId != null && clients.containsKey(userId)) { if (userId != null && clients.containsKey(userId)) {
String userName = clients.get(userId); String userName = clients.get(userId);
if (reason != DisconnectReason.LostConnection) { // for lost connection the user will be reconnected or session expire so no removeUserFromAllTables of chat yet if (reason != DisconnectReason.LostConnection) { // for lost connection the user will be reconnected or session expire so no removeUserFromAllTablesAndChat of chat yet
clients.remove(userId); final Lock w = lock.writeLock();
w.lock();
try {
clients.remove(userId);
} finally {
w.unlock();
}
logger.debug(userName + '(' + reason.toString() + ')' + " removed from chatId " + chatId); logger.debug(userName + '(' + reason.toString() + ')' + " removed from chatId " + chatId);
} }
String message = reason.getMessage(); String message = reason.getMessage();
@ -117,7 +134,15 @@ public class ChatSession {
if (!message.isEmpty()) { if (!message.isEmpty()) {
Set<UUID> clientsToRemove = new HashSet<>(); Set<UUID> clientsToRemove = new HashSet<>();
ClientCallback clientCallback = new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId, new ChatMessage(userName, message, (withTime ? timeFormatter.format(new Date()) : ""), color, messageType, soundToPlay)); ClientCallback clientCallback = new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId, new ChatMessage(userName, message, (withTime ? timeFormatter.format(new Date()) : ""), color, messageType, soundToPlay));
for (UUID userId : clients.keySet()) { List<UUID> chatUserIds = new ArrayList<>();
final Lock r = lock.readLock();
r.lock();
try {
chatUserIds.addAll(clients.keySet());
} finally {
r.unlock();
}
for (UUID userId : chatUserIds) {
Optional<User> user = UserManager.instance.getUser(userId); Optional<User> user = UserManager.instance.getUser(userId);
if (user.isPresent()) { if (user.isPresent()) {
user.get().fireCallback(clientCallback); user.get().fireCallback(clientCallback);
@ -125,7 +150,15 @@ public class ChatSession {
clientsToRemove.add(userId); clientsToRemove.add(userId);
} }
} }
clients.keySet().removeAll(clientsToRemove); if (!clientsToRemove.isEmpty()) {
final Lock w = lock.readLock();
w.lock();
try {
clients.keySet().removeAll(clientsToRemove);
} finally {
w.unlock();
}
}
} }
} }

View file

@ -356,7 +356,7 @@ public class Session {
} else { } else {
logger.error("SESSION LOCK - kill: userId " + userId); logger.error("SESSION LOCK - kill: userId " + userId);
} }
UserManager.instance.removeUserFromAllTables(userId, reason); UserManager.instance.removeUserFromAllTablesAndChat(userId, reason);
} catch (InterruptedException ex) { } catch (InterruptedException ex) {
logger.error("SESSION LOCK - kill: userId " + userId, ex); logger.error("SESSION LOCK - kill: userId " + userId, ex);
} finally { } finally {

View file

@ -511,7 +511,7 @@ public class TableController {
if (this.userId != null && this.userId.equals(userId) // tourn. sub tables have no creator user if (this.userId != null && this.userId.equals(userId) // tourn. sub tables have no creator user
&& (table.getState() == TableState.WAITING && (table.getState() == TableState.WAITING
|| table.getState() == TableState.READY_TO_START)) { || table.getState() == TableState.READY_TO_START)) {
// table not started yet and user is the owner, removeUserFromAllTables the table // table not started yet and user is the owner, removeUserFromAllTablesAndChat the table
TableManager.instance.removeTable(table.getId()); TableManager.instance.removeTable(table.getId());
} else { } else {
UUID playerId = userPlayerMap.get(userId); UUID playerId = userPlayerMap.get(userId);
@ -826,7 +826,7 @@ public class TableController {
} }
user.showUserMessage("Match info", sb.toString()); user.showUserMessage("Match info", sb.toString());
} }
// removeUserFromAllTables table from user - table manager holds table for display of finished matches // removeUserFromAllTablesAndChat table from user - table manager holds table for display of finished matches
if (!table.isTournamentSubTable()) { if (!table.isTournamentSubTable()) {
user.removeTable(entry.getValue()); user.removeTable(entry.getValue());
} }
@ -913,7 +913,7 @@ public class TableController {
return false; return false;
} }
} else { } else {
// check if table creator is still a valid user, if not removeUserFromAllTables table // check if table creator is still a valid user, if not removeUserFromAllTablesAndChat table
return UserManager.instance.getUser(userId).isPresent(); return UserManager.instance.getUser(userId).isPresent();
} }
} }

View file

@ -78,6 +78,7 @@ public enum TableManager {
TableManager() { TableManager() {
expireExecutor.scheduleAtFixedRate(() -> { expireExecutor.scheduleAtFixedRate(() -> {
try { try {
ChatManager.instance.clearUserMessageStorage();
checkTableHealthState(); checkTableHealthState();
} catch (Exception ex) { } catch (Exception ex) {
logger.fatal("Check table health state job error:", ex); logger.fatal("Check table health state job error:", ex);
@ -161,7 +162,7 @@ public enum TableManager {
} }
} }
// removeUserFromAllTables user from all tournament sub tables // removeUserFromAllTablesAndChat user from all tournament sub tables
public void userQuitTournamentSubTables(UUID userId) { public void userQuitTournamentSubTables(UUID userId) {
for (TableController controller : controllers.values()) { for (TableController controller : controllers.values()) {
if (controller.getTable() != null) { if (controller.getTable() != null) {
@ -174,7 +175,7 @@ public enum TableManager {
} }
} }
// removeUserFromAllTables user from all sub tables of a tournament // removeUserFromAllTablesAndChat user from all sub tables of a tournament
public void userQuitTournamentSubTables(UUID tournamentId, UUID userId) { public void userQuitTournamentSubTables(UUID tournamentId, UUID userId) {
for (TableController controller : controllers.values()) { for (TableController controller : controllers.values()) {
if (controller.getTable().isTournamentSubTable() && controller.getTable().getTournament().getId().equals(tournamentId)) { if (controller.getTable().isTournamentSubTable() && controller.getTable().getTournament().getId().equals(tournamentId)) {
@ -386,8 +387,8 @@ public enum TableManager {
for (Table table : tableCopy) { for (Table table : tableCopy) {
try { try {
if (table.getState() != TableState.FINISHED if (table.getState() != TableState.FINISHED
&& ((System.currentTimeMillis() - table.getStartTime().getTime()) / 1000) > 30) { // removeUserFromAllTables only if table started longer than 30 seconds ago && ((System.currentTimeMillis() - table.getStartTime().getTime()) / 1000) > 30) { // removeUserFromAllTablesAndChat only if table started longer than 30 seconds ago
// removeUserFromAllTables tables and games not valid anymore // removeUserFromAllTablesAndChat tables and games not valid anymore
logger.debug(table.getId() + " [" + table.getName() + "] " + formatter.format(table.getStartTime() == null ? table.getCreateTime() : table.getCreateTime()) + " (" + table.getState().toString() + ") " + (table.isTournament() ? "- Tournament" : "")); logger.debug(table.getId() + " [" + table.getName() + "] " + formatter.format(table.getStartTime() == null ? table.getCreateTime() : table.getCreateTime()) + " (" + table.getState().toString() + ") " + (table.isTournament() ? "- Tournament" : ""));
getController(table.getId()).ifPresent(tableController -> { getController(table.getId()).ifPresent(tableController -> {
if ((table.isTournament() && !tableController.isTournamentStillValid()) if ((table.isTournament() && !tableController.isTournamentStillValid())

View file

@ -29,6 +29,9 @@ package mage.server;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import mage.server.User.UserState; import mage.server.User.UserState;
import mage.server.record.UserStats; import mage.server.record.UserStats;
import mage.server.record.UserStatsRepository; import mage.server.record.UserStatsRepository;
@ -54,6 +57,7 @@ public enum UserManager {
private static final Logger LOGGER = Logger.getLogger(UserManager.class); private static final Logger LOGGER = Logger.getLogger(UserManager.class);
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final ConcurrentHashMap<UUID, User> users = new ConcurrentHashMap<>(); private final ConcurrentHashMap<UUID, User> users = new ConcurrentHashMap<>();
private static final ExecutorService USER_EXECUTOR = ThreadExecutor.instance.getCallExecutor(); private static final ExecutorService USER_EXECUTOR = ThreadExecutor.instance.getCallExecutor();
@ -69,7 +73,13 @@ public enum UserManager {
return Optional.empty(); //user already exists return Optional.empty(); //user already exists
} }
User user = new User(userName, host, authorizedUser); User user = new User(userName, host, authorizedUser);
users.put(user.getId(), user); final Lock w = lock.writeLock();
w.lock();
try {
users.put(user.getId(), user);
} finally {
w.unlock();
}
return Optional.of(user); return Optional.of(user);
} }
@ -83,17 +93,32 @@ public enum UserManager {
} }
public Optional<User> getUserByName(String userName) { public Optional<User> getUserByName(String userName) {
Optional<User> u = users.values().stream().filter(user -> user.getName().equals(userName)) final Lock r = lock.readLock();
.findFirst(); r.lock();
if (u.isPresent()) { try {
return u; Optional<User> u = users.values().stream().filter(user -> user.getName().equals(userName))
} else { .findFirst();
return Optional.empty(); if (u.isPresent()) {
return u;
} else {
return Optional.empty();
}
} finally {
r.unlock();
} }
} }
public Collection<User> getUsers() { public Collection<User> getUsers() {
return users.values(); ArrayList<User> userList = new ArrayList<>();
final Lock r = lock.readLock();
r.lock();
try {
userList.addAll(users.values());
} finally {
r.unlock();
}
return userList;
} }
public boolean connectToSession(String sessionId, UUID userId) { public boolean connectToSession(String sessionId, UUID userId) {
@ -112,13 +137,10 @@ public enum UserManager {
if (user.isPresent()) { if (user.isPresent()) {
user.get().setSessionId(""); user.get().setSessionId("");
if (reason == DisconnectReason.Disconnected) { if (reason == DisconnectReason.Disconnected) {
removeUserFromAllTables(userId, reason); removeUserFromAllTablesAndChat(userId, reason);
user.get().setUserState(UserState.Offline); user.get().setUserState(UserState.Offline);
} }
} }
if (userId != null) {
ChatManager.instance.removeUser(userId, reason);
}
} }
public boolean isAdmin(UUID userId) { public boolean isAdmin(UUID userId) {
@ -131,7 +153,7 @@ public enum UserManager {
return false; return false;
} }
public void removeUserFromAllTables(final UUID userId, final DisconnectReason reason) { public void removeUserFromAllTablesAndChat(final UUID userId, final DisconnectReason reason) {
if (userId != null) { if (userId != null) {
getUser(userId).ifPresent(user getUser(userId).ifPresent(user
-> USER_EXECUTOR.execute( -> USER_EXECUTOR.execute(
@ -139,6 +161,7 @@ public enum UserManager {
try { try {
LOGGER.info("USER REMOVE - " + user.getName() + " (" + reason.toString() + ") userId: " + userId + " [" + user.getGameInfo() + ']'); LOGGER.info("USER REMOVE - " + user.getName() + " (" + reason.toString() + ") userId: " + userId + " [" + user.getGameInfo() + ']');
user.removeUserFromAllTables(reason); user.removeUserFromAllTables(reason);
ChatManager.instance.removeUser(user.getId(), reason);
LOGGER.debug("USER REMOVE END - " + user.getName()); LOGGER.debug("USER REMOVE END - " + user.getName());
} catch (Exception ex) { } catch (Exception ex) {
handleException(ex); handleException(ex);
@ -173,7 +196,16 @@ public enum UserManager {
Calendar calendarRemove = Calendar.getInstance(); Calendar calendarRemove = Calendar.getInstance();
calendarRemove.add(Calendar.MINUTE, -8); calendarRemove.add(Calendar.MINUTE, -8);
List<User> toRemove = new ArrayList<>(); List<User> toRemove = new ArrayList<>();
for (User user : users.values()) { logger.info("Start Check Expired");
ArrayList<User> userList = new ArrayList<>();
final Lock r = lock.readLock();
r.lock();
try {
userList.addAll(users.values());
} finally {
r.unlock();
}
for (User user : userList) {
try { try {
if (user.getUserState() == UserState.Offline) { if (user.getUserState() == UserState.Offline) {
if (user.isExpired(calendarRemove.getTime())) { if (user.isExpired(calendarRemove.getTime())) {
@ -185,7 +217,7 @@ public enum UserManager {
user.lostConnection(); user.lostConnection();
disconnect(user.getId(), DisconnectReason.BecameInactive); disconnect(user.getId(), DisconnectReason.BecameInactive);
} }
removeUserFromAllTables(user.getId(), DisconnectReason.SessionExpired); removeUserFromAllTablesAndChat(user.getId(), DisconnectReason.SessionExpired);
user.setUserState(UserState.Offline); user.setUserState(UserState.Offline);
// Remove the user from all tournaments // Remove the user from all tournaments
@ -195,9 +227,17 @@ public enum UserManager {
handleException(ex); handleException(ex);
} }
} }
for (User user : toRemove) { logger.info("Users to remove " + toRemove.size());
users.remove(user.getId()); final Lock w = lock.readLock();
w.lock();
try {
for (User user : toRemove) {
users.remove(user.getId());
}
} finally {
w.unlock();
} }
logger.info("End Check Expired");
} catch (Exception ex) { } catch (Exception ex) {
handleException(ex); handleException(ex);
} }

View file

@ -95,7 +95,7 @@ public class GamesRoomImpl extends RoomImpl implements GamesRoom, Serializable {
} else if (matchList.size() < 50) { } else if (matchList.size() < 50) {
matchList.add(new MatchView(table)); matchList.add(new MatchView(table));
} else { } else {
// more since 50 matches finished since this match so removeUserFromAllTables it // more since 50 matches finished since this match so removeUserFromAllTablesAndChat it
if (table.isTournament()) { if (table.isTournament()) {
TournamentManager.instance.removeTournament(table.getTournament().getId()); TournamentManager.instance.removeTournament(table.getTournament().getId());
} }

View file

@ -51,6 +51,7 @@ public class JeeringHomunculus extends CardImpl {
this.toughness = new MageInt(4); this.toughness = new MageInt(4);
// When Jeering Homunculus enters the battlefield, you may goad target creature. // When Jeering Homunculus enters the battlefield, you may goad target creature.
// (Until your next turn, that creature attacks each combat if able and attacks a player other than you if able.)
Ability ability = new EntersBattlefieldTriggeredAbility(new GoadTargetEffect(), true); Ability ability = new EntersBattlefieldTriggeredAbility(new GoadTargetEffect(), true);
ability.addTarget(new TargetCreaturePermanent()); ability.addTarget(new TargetCreaturePermanent());
this.addAbility(ability); this.addAbility(ability);

View file

@ -28,11 +28,10 @@
package mage.cards.t; package mage.cards.t;
import java.util.UUID; import java.util.UUID;
import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.ExileSpellEffect; import mage.abilities.effects.common.ExileSpellEffect;
import mage.abilities.effects.common.NameACardEffect;
import mage.abilities.effects.common.continuous.GainAbilityControllerEffect; import mage.abilities.effects.common.continuous.GainAbilityControllerEffect;
import mage.abilities.effects.common.continuous.LifeTotalCantChangeControllerEffect; import mage.abilities.effects.common.continuous.LifeTotalCantChangeControllerEffect;
import mage.abilities.keyword.ProtectionAbility; import mage.abilities.keyword.ProtectionAbility;
@ -41,9 +40,8 @@ import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.Duration; import mage.constants.Duration;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.filter.FilterObject; import mage.filter.FilterCard;
import mage.filter.common.FilterControlledPermanent; import mage.filter.StaticFilters;
import mage.filter.predicate.mageobject.NamePredicate;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.players.Player; import mage.players.Player;
@ -58,7 +56,8 @@ public class TeferisProtection extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{W}"); super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{W}");
// Until your next turn, your life total can't change, and you have protection from everything. All permanents you control phase out. (While they're phased out, they're treated as though they don't exist. They phase in before you untap during your untap step.) // Until your next turn, your life total can't change, and you have protection from everything. All permanents you control phase out. (While they're phased out, they're treated as though they don't exist. They phase in before you untap during your untap step.)
this.getSpellAbility().addEffect(new LifeTotalCantChangeControllerEffect(Duration.UntilYourNextTurn)); this.getSpellAbility().addEffect(new LifeTotalCantChangeControllerEffect(Duration.UntilYourNextTurn)
.setText("Until your next turn, your life total can't change"));
this.getSpellAbility().addEffect(new TeferisProtectionEffect()); this.getSpellAbility().addEffect(new TeferisProtectionEffect());
this.getSpellAbility().addEffect(new TeferisProtectionPhaseOutEffect()); this.getSpellAbility().addEffect(new TeferisProtectionPhaseOutEffect());
@ -78,9 +77,31 @@ public class TeferisProtection extends CardImpl {
class TeferisProtectionEffect extends OneShotEffect { class TeferisProtectionEffect extends OneShotEffect {
/**
* 25.08.2017 The following rulings focus on the protection from keyword
*
* 25.08.2017 If a player has protection from everything, it means three
* things: 1) All damage that would be dealt to that player is prevented. 2)
* Auras cant be attached to that player. 3) That player cant be the
* target of spells or abilities.
*
* 25.08.2017 Nothing other than the specified events are prevented or
* illegal. An effect that doesnt target you could still cause you to
* discard cards, for example. Creatures can still attack you while you have
* protection from everything, although combat damage that they would deal
* to you will be prevented.
*
* 25.08.2017 Gaining protection from everything causes a spell or ability
* on the stack to have an illegal target if it targets you. As a spell or
* ability tries to resolve, if all its targets are illegal, that spell or
* ability is countered and none of its effects happen, including effects
* unrelated to the target. If at least one target is still legal, the spell
* or ability does as much as it can to the remaining legal targets, and its
* other effects still happen.
*/
public TeferisProtectionEffect() { public TeferisProtectionEffect() {
super(Outcome.Protect); super(Outcome.Protect);
staticText = "<br/><br/>You have protection from everything<i>(You can't be targeted, dealt damage, or enchanted by anything.)</i>"; staticText = ", and you have protection from everything";
} }
public TeferisProtectionEffect(final TeferisProtectionEffect effect) { public TeferisProtectionEffect(final TeferisProtectionEffect effect) {
@ -90,12 +111,8 @@ class TeferisProtectionEffect extends OneShotEffect {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
String cardName = (String) game.getState().getValue(source.getSourceId().toString() + NameACardEffect.INFO_KEY);
if (controller != null) { if (controller != null) {
FilterObject filter = new FilterObject("the name [everything]"); game.addEffect(new GainAbilityControllerEffect(new TeferisProtectionAbility(), Duration.UntilYourNextTurn), source);
filter.add(new NamePredicate("everything"));
ContinuousEffect effect = new GainAbilityControllerEffect(new ProtectionAbility(filter), Duration.Custom);
game.addEffect(effect, source);
return true; return true;
} }
return false; return false;
@ -107,9 +124,33 @@ class TeferisProtectionEffect extends OneShotEffect {
} }
} }
class TeferisProtectionPhaseOutEffect extends OneShotEffect { class TeferisProtectionAbility extends ProtectionAbility {
private FilterControlledPermanent permanentsYouControl = new FilterControlledPermanent("all permanents you control"); public TeferisProtectionAbility() {
super(new FilterCard("everything"));
}
public TeferisProtectionAbility(final TeferisProtectionAbility ability) {
super(ability);
}
@Override
public TeferisProtectionAbility copy() {
return new TeferisProtectionAbility(this);
}
@Override
public String getRule() {
return "Protection from everything";
}
@Override
public boolean canTarget(MageObject source, Game game) {
return false;
}
}
class TeferisProtectionPhaseOutEffect extends OneShotEffect {
public TeferisProtectionPhaseOutEffect() { public TeferisProtectionPhaseOutEffect() {
super(Outcome.Benefit); super(Outcome.Benefit);
@ -129,7 +170,7 @@ class TeferisProtectionPhaseOutEffect extends OneShotEffect {
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (controller != null) { if (controller != null) {
for (Permanent permanent : game.getBattlefield().getActivePermanents(permanentsYouControl, controller.getId(), game)) { for (Permanent permanent : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_CONTROLLED_PERMANENT, controller.getId(), game)) {
permanent.phaseOut(game); permanent.phaseOut(game);
} }
return true; return true;

View file

@ -29,12 +29,12 @@ package mage.cards.t;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.UUID; import java.util.UUID;
import mage.MageInt; import mage.MageInt;
import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.BeginningOfCombatTriggeredAbility; import mage.abilities.common.BeginningOfCombatTriggeredAbility;
import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.ContinuousEffect;
@ -46,10 +46,12 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.constants.SubType;
import mage.constants.TargetController; import mage.constants.TargetController;
import mage.constants.WatcherScope; import mage.constants.WatcherScope;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.players.Player; import mage.players.Player;
import mage.target.targetpointer.FixedTarget; import mage.target.targetpointer.FixedTarget;
import mage.util.RandomUtil; import mage.util.RandomUtil;
@ -64,7 +66,7 @@ public class TerritorialHellkite extends CardImpl {
public TerritorialHellkite(UUID ownerId, CardSetInfo setInfo) { public TerritorialHellkite(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}{R}"); super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}{R}");
this.subtype.add("Dragon"); this.subtype.add(SubType.DRAGON);
this.power = new MageInt(6); this.power = new MageInt(6);
this.toughness = new MageInt(5); this.toughness = new MageInt(5);
@ -90,7 +92,8 @@ public class TerritorialHellkite extends CardImpl {
class AttackedLastCombatWatcher extends Watcher { class AttackedLastCombatWatcher extends Watcher {
public final Map<UUID, UUID> attackedLastCombatPlayers = new HashMap<>(); // Map<lastCombatOfPlayerId, Map<attackingCreature, attackedPlayerId>>
public final Map<UUID, Map<MageObjectReference, UUID>> attackedLastCombatPlayers = new HashMap<>();
public AttackedLastCombatWatcher() { public AttackedLastCombatWatcher() {
super(AttackedLastCombatWatcher.class.getSimpleName(), WatcherScope.GAME); super(AttackedLastCombatWatcher.class.getSimpleName(), WatcherScope.GAME);
@ -98,32 +101,34 @@ class AttackedLastCombatWatcher extends Watcher {
public AttackedLastCombatWatcher(final AttackedLastCombatWatcher watcher) { public AttackedLastCombatWatcher(final AttackedLastCombatWatcher watcher) {
super(watcher); super(watcher);
for (Entry<UUID, UUID> entry : watcher.attackedLastCombatPlayers.entrySet()) { for (Entry<UUID, Map<MageObjectReference, UUID>> entry : watcher.attackedLastCombatPlayers.entrySet()) {
attackedLastCombatPlayers.put(entry.getKey(), entry.getValue()); Map<MageObjectReference, UUID> allAttackersCopy = new HashMap<>();
allAttackersCopy.putAll(entry.getValue());
attackedLastCombatPlayers.put(entry.getKey(), allAttackersCopy);
} }
} }
@Override @Override
public void watch(GameEvent event, Game game) { public void watch(GameEvent event, Game game) {
//TODO: this will have problems if the creature is stolen and then given back before the original controller's next combat
if (event.getType() == GameEvent.EventType.DECLARE_ATTACKERS_STEP_PRE) { if (event.getType() == GameEvent.EventType.DECLARE_ATTACKERS_STEP_PRE) {
if (!attackedLastCombatPlayers.keySet().isEmpty()) { // Remove previous attacking creatures of the current combat's player if info exists
Iterator<Map.Entry<UUID, UUID>> attackers = attackedLastCombatPlayers.entrySet().iterator(); attackedLastCombatPlayers.remove(game.getCombat().getAttackingPlayerId());
while (attackers.hasNext()) {
Map.Entry<UUID, UUID> attacker = attackers.next();
if (game.getPermanent(attacker.getKey()).getControllerId().equals(game.getActivePlayerId())) {
attackers.remove();
}
}
}
} }
if (event.getType() == GameEvent.EventType.ATTACKER_DECLARED) { if (event.getType() == GameEvent.EventType.ATTACKER_DECLARED) {
attackedLastCombatPlayers.put(event.getSourceId(), game.getCombat().getDefenderId(event.getSourceId())); // remember which attacker attacked which player
Map<MageObjectReference, UUID> attackedPlayers = new HashMap<>();
for (UUID attackerId : game.getCombat().getAttackers()) {
Permanent attacker = game.getPermanent(attackerId);
if (attacker != null) {
attackedPlayers.put(new MageObjectReference(attacker, game), game.getCombat().getDefenderId(attackerId));
}
}
attackedLastCombatPlayers.put(game.getCombat().getAttackingPlayerId(), attackedPlayers);
} }
} }
public Map<UUID, UUID> getAttackedLastCombatPlayers() { public Map<MageObjectReference, UUID> getAttackedLastCombatPlayers(UUID combatPlayerId) {
return this.attackedLastCombatPlayers; return attackedLastCombatPlayers.get(combatPlayerId);
} }
@Override @Override
@ -152,22 +157,20 @@ class AttackIfAbleTargetRandoOpponentSourceEffect extends OneShotEffect {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (controller != null) { Permanent sourcePermanent = game.getPermanent(source.getSourceId());
AttackedLastCombatWatcher watcher = (AttackedLastCombatWatcher) game.getState().getWatchers().get(AttackedLastCombatWatcher.class.getSimpleName());
if (controller != null && sourcePermanent != null && watcher != null) {
List<UUID> opponents = new ArrayList<>(); List<UUID> opponents = new ArrayList<>();
AttackedLastCombatWatcher watcher = (AttackedLastCombatWatcher) game.getState().getWatchers().get(AttackedLastCombatWatcher.class.getSimpleName()); Map<MageObjectReference, UUID> attackedPlayers = watcher.getAttackedLastCombatPlayers(source.getControllerId());
if (watcher != null) { MageObjectReference mor = new MageObjectReference(sourcePermanent, game);
boolean ignoreMe; if (attackedPlayers == null) {
opponents.addAll(game.getOpponents(controller.getId()));
} else {
for (UUID opp : game.getOpponents(controller.getId())) { for (UUID opp : game.getOpponents(controller.getId())) {
ignoreMe = false; if (!opp.equals(attackedPlayers.getOrDefault(mor, null))) {
if (watcher.getAttackedLastCombatPlayers().getOrDefault(source.getSourceId(), source.getControllerId()).equals(opp)) {
ignoreMe = true;
}
if (!ignoreMe) {
opponents.add(opp); opponents.add(opp);
} }
} }
} else {
opponents.addAll(game.getOpponents(controller.getId()));
} }
if (!opponents.isEmpty()) { if (!opponents.isEmpty()) {
Player opponent = game.getPlayer(opponents.get(RandomUtil.nextInt(opponents.size()))); Player opponent = game.getPlayer(opponents.get(RandomUtil.nextInt(opponents.size())));
@ -175,12 +178,15 @@ class AttackIfAbleTargetRandoOpponentSourceEffect extends OneShotEffect {
ContinuousEffect effect = new AttacksIfAbleTargetPlayerSourceEffect(); ContinuousEffect effect = new AttacksIfAbleTargetPlayerSourceEffect();
effect.setTargetPointer(new FixedTarget(opponent.getId())); effect.setTargetPointer(new FixedTarget(opponent.getId()));
game.addEffect(effect, source); game.addEffect(effect, source);
return true; game.informPlayers(sourcePermanent.getLogName() + " has to attack " + opponent.getLogName() + ".");
} }
} else { } else {
game.getPermanent(source.getSourceId()).tap(game); game.informPlayers(sourcePermanent.getLogName() + " can't attack an opponent it didn't attack last combat.");
sourcePermanent.tap(game);
} }
return true;
} }
return false; return false;
} }
} }

View file

@ -51,7 +51,7 @@ import mage.target.TargetPermanent;
public class VampireHexmage extends CardImpl { public class VampireHexmage extends CardImpl {
public VampireHexmage(UUID ownerId, CardSetInfo setInfo) { public VampireHexmage(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{B}{B}"); super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{B}{B}");
this.subtype.add("Vampire"); this.subtype.add("Vampire");
this.subtype.add("Shaman"); this.subtype.add("Shaman");
@ -77,12 +77,12 @@ public class VampireHexmage extends CardImpl {
class VampireHexmageEffect extends OneShotEffect { class VampireHexmageEffect extends OneShotEffect {
VampireHexmageEffect ( ) { VampireHexmageEffect() {
super(Outcome.Benefit); super(Outcome.Benefit);
staticText = "Remove all counters from target permanent"; staticText = "Remove all counters from target permanent";
} }
VampireHexmageEffect ( VampireHexmageEffect effect ) { VampireHexmageEffect(VampireHexmageEffect effect) {
super(effect); super(effect);
} }
@ -93,18 +93,13 @@ class VampireHexmageEffect extends OneShotEffect {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
TargetPermanent target = (TargetPermanent)source.getTargets().get(0); Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source));
Permanent permanent = game.getPermanent(target.getFirstTarget());
if (permanent != null) { if (permanent != null) {
for(Counter counter : permanent.getCounters(game).values()){ for (Counter counter : permanent.getCounters(game).copy().values()) { // copy to prevent ConcurrentModificationException
permanent.removeCounters(counter, game); permanent.removeCounters(counter, game);
} }
return true; return true;
} }
return false; return false;
} }

View file

@ -53,16 +53,16 @@ public class PutPermanentOnBattlefieldEffect extends OneShotEffect {
} else { } else {
player = game.getPlayer(source.getControllerId()); player = game.getPlayer(source.getControllerId());
} }
String choiceText = "Put " + filter.getMessage() + " from your hand onto the battlefield?"; if (player == null) {
if (player == null || !player.chooseUse(Outcome.PutCardInPlay, choiceText, source, game)) {
return false; return false;
} }
if (player.chooseUse(Outcome.PutCardInPlay, "Put " + filter.getMessage() + " from your hand onto the battlefield?", source, game)) {
TargetCardInHand target = new TargetCardInHand(filter); TargetCardInHand target = new TargetCardInHand(filter);
if (player.choose(Outcome.PutCreatureInPlay, target, source.getSourceId(), game)) { if (player.choose(Outcome.PutCreatureInPlay, target, source.getSourceId(), game)) {
Card card = game.getCard(target.getFirstTarget()); Card card = game.getCard(target.getFirstTarget());
if (card != null) { if (card != null) {
return player.moveCards(card, Zone.BATTLEFIELD, source, game); return player.moveCards(card, Zone.BATTLEFIELD, source, game);
}
} }
} }
return true; return true;

View file

@ -66,12 +66,8 @@ public class TributeAbility extends EntersBattlefieldAbility {
@Override @Override
public String getRule() { public String getRule() {
StringBuilder sb = new StringBuilder("Tribute "); return "Tribute " + tributeValue + " <i>(As this creature enters the battlefield, an opponent of your choice may put "
sb.append(tributeValue); + tributeValue + " +1/+1 counter on it.)</i>";
sb.append(" <i>(As this creature enters the battlefield, an opponent of your choice may put ");
sb.append(tributeValue);
sb.append(" +1/+1 counter on it.)</i>");
return sb.toString();
} }
} }

View file

@ -23,7 +23,6 @@ import mage.filter.predicate.permanent.TokenPredicate;
*/ */
public final class StaticFilters { public final class StaticFilters {
public static final FilterSpiritOrArcaneCard SPIRIT_OR_ARCANE_CARD = new FilterSpiritOrArcaneCard(); public static final FilterSpiritOrArcaneCard SPIRIT_OR_ARCANE_CARD = new FilterSpiritOrArcaneCard();
public static final FilterArtifactOrEnchantmentPermanent ARTIFACT_OR_ENCHANTMENT_PERMANENT = new FilterArtifactOrEnchantmentPermanent(); public static final FilterArtifactOrEnchantmentPermanent ARTIFACT_OR_ENCHANTMENT_PERMANENT = new FilterArtifactOrEnchantmentPermanent();
public static final FilterEnchantmentPermanent FILTER_ENCHANTMENT_PERMANENT = new FilterEnchantmentPermanent(); public static final FilterEnchantmentPermanent FILTER_ENCHANTMENT_PERMANENT = new FilterEnchantmentPermanent();
@ -37,6 +36,8 @@ public final class StaticFilters {
public static final FilterPermanent FILTER_PERMANENT_ARTIFACT_OR_CREATURE = new FilterPermanent("artifact or creature"); public static final FilterPermanent FILTER_PERMANENT_ARTIFACT_OR_CREATURE = new FilterPermanent("artifact or creature");
public static final FilterPermanent FILTER_PERMANENT_ARTIFACT_CREATURE_OR_ENCHANTMENT = new FilterPermanent("artifact, creature, or enchantment"); public static final FilterPermanent FILTER_PERMANENT_ARTIFACT_CREATURE_OR_ENCHANTMENT = new FilterPermanent("artifact, creature, or enchantment");
public static final FilterPermanent FILTER_PERMANENT_ARTIFACT_CREATURE_ENCHANTMENT_OR_LAND = new FilterPermanent("artifact, creature, enchantment, or land"); public static final FilterPermanent FILTER_PERMANENT_ARTIFACT_CREATURE_ENCHANTMENT_OR_LAND = new FilterPermanent("artifact, creature, enchantment, or land");
public static final FilterControlledPermanent FILTER_CONTROLLED_PERMANENT = new FilterControlledPermanent();
public static final FilterControlledPermanent FILTER_CONTROLLED_PERMANENT_ARTIFACT = new FilterControlledArtifactPermanent(); public static final FilterControlledPermanent FILTER_CONTROLLED_PERMANENT_ARTIFACT = new FilterControlledArtifactPermanent();
public static final FilterControlledPermanent FILTER_CONTROLLED_PERMANENT_ARTIFACT_OR_CREATURE = new FilterControlledPermanent("artifact or creature you control"); public static final FilterControlledPermanent FILTER_CONTROLLED_PERMANENT_ARTIFACT_OR_CREATURE = new FilterControlledPermanent("artifact or creature you control");
@ -67,8 +68,6 @@ public final class StaticFilters {
public static final FilterPermanent FILTER_ATTACKING_CREATURES = new FilterCreaturePermanent("attacking creatures"); public static final FilterPermanent FILTER_ATTACKING_CREATURES = new FilterCreaturePermanent("attacking creatures");
public static final FilterPermanent FILTER_AURA = new FilterPermanent(); public static final FilterPermanent FILTER_AURA = new FilterPermanent();
public static final FilterPermanent FILTER_EQUIPMENT = new FilterPermanent(); public static final FilterPermanent FILTER_EQUIPMENT = new FilterPermanent();
public static final FilterPermanent FILTER_FORTIFICATION = new FilterPermanent(); public static final FilterPermanent FILTER_FORTIFICATION = new FilterPermanent();
@ -91,7 +90,6 @@ public final class StaticFilters {
FILTER_CONTROLLED_PERMANENT_NON_LAND.add( FILTER_CONTROLLED_PERMANENT_NON_LAND.add(
Predicates.not(new CardTypePredicate(CardType.LAND))); Predicates.not(new CardTypePredicate(CardType.LAND)));
FILTER_CREATURE_TOKENS.add(new TokenPredicate()); FILTER_CREATURE_TOKENS.add(new TokenPredicate());
FILTER_ATTACKING_CREATURES.add(new AttackingPredicate()); FILTER_ATTACKING_CREATURES.add(new AttackingPredicate());

View file

@ -27,6 +27,10 @@
*/ */
package mage.players; package mage.players;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Map.Entry;
import mage.ConditionalMana; import mage.ConditionalMana;
import mage.MageObject; import mage.MageObject;
import mage.Mana; import mage.Mana;
@ -50,6 +54,11 @@ import mage.cards.CardsImpl;
import mage.cards.SplitCard; import mage.cards.SplitCard;
import mage.cards.decks.Deck; import mage.cards.decks.Deck;
import mage.constants.*; import mage.constants.*;
import static mage.constants.Zone.BATTLEFIELD;
import static mage.constants.Zone.EXILED;
import static mage.constants.Zone.GRAVEYARD;
import static mage.constants.Zone.HAND;
import static mage.constants.Zone.LIBRARY;
import mage.counters.Counter; import mage.counters.Counter;
import mage.counters.CounterType; import mage.counters.CounterType;
import mage.counters.Counters; import mage.counters.Counters;
@ -87,11 +96,6 @@ import mage.util.GameLog;
import mage.util.RandomUtil; import mage.util.RandomUtil;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Map.Entry;
public abstract class PlayerImpl implements Player, Serializable { public abstract class PlayerImpl implements Player, Serializable {
private static final Logger logger = Logger.getLogger(PlayerImpl.class); private static final Logger logger = Logger.getLogger(PlayerImpl.class);
@ -966,6 +970,10 @@ public abstract class PlayerImpl implements Player, Serializable {
ability = chooseSpellAbilityForCast(ability, game, noMana); ability = chooseSpellAbilityForCast(ability, game, noMana);
} }
//20091005 - 601.2a //20091005 - 601.2a
if (ability.getSourceId() == null) {
logger.error("Ability without sourceId turn " + game.getTurnNum() + ". Ability: " + ability.getRule());
return false;
}
Card card = game.getCard(ability.getSourceId()); Card card = game.getCard(ability.getSourceId());
if (card != null) { if (card != null) {
if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, ability.getId(), ability.getSourceId(), playerId), ability)) { if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, ability.getId(), ability.getSourceId(), playerId), ability)) {
@ -1770,51 +1778,59 @@ public abstract class PlayerImpl implements Player, Serializable {
@SuppressWarnings({"null", "ConstantConditions"}) @SuppressWarnings({"null", "ConstantConditions"})
private int doDamage(int damage, UUID sourceId, Game game, boolean combatDamage, boolean preventable, List<UUID> appliedEffects) { private int doDamage(int damage, UUID sourceId, Game game, boolean combatDamage, boolean preventable, List<UUID> appliedEffects) {
if (damage > 0 && canDamage(game.getObject(sourceId), game)) { if (damage > 0) {
GameEvent event = new DamagePlayerEvent(playerId, sourceId, playerId, damage, preventable, combatDamage); if (canDamage(game.getObject(sourceId), game)) {
event.setAppliedEffects(appliedEffects); GameEvent event = new DamagePlayerEvent(playerId, sourceId, playerId, damage, preventable, combatDamage);
if (!game.replaceEvent(event)) { event.setAppliedEffects(appliedEffects);
int actualDamage = event.getAmount(); if (!game.replaceEvent(event)) {
if (actualDamage > 0) { int actualDamage = event.getAmount();
UUID sourceControllerId = null; if (actualDamage > 0) {
Abilities sourceAbilities = null; UUID sourceControllerId = null;
MageObject source = game.getPermanentOrLKIBattlefield(sourceId); Abilities sourceAbilities = null;
if (source == null) { MageObject source = game.getPermanentOrLKIBattlefield(sourceId);
StackObject stackObject = game.getStack().getStackObject(sourceId); if (source == null) {
if (stackObject != null) { StackObject stackObject = game.getStack().getStackObject(sourceId);
source = stackObject.getStackAbility().getSourceObject(game); if (stackObject != null) {
source = stackObject.getStackAbility().getSourceObject(game);
} else {
source = game.getObject(sourceId);
}
if (source instanceof Spell) {
sourceAbilities = ((Spell) source).getAbilities(game);
sourceControllerId = ((Spell) source).getControllerId();
} else if (source instanceof Card) {
sourceAbilities = ((Card) source).getAbilities(game);
sourceControllerId = ((Card) source).getOwnerId();
} else if (source instanceof CommandObject) {
sourceControllerId = ((CommandObject) source).getControllerId();
sourceAbilities = ((CommandObject) source).getAbilities();
}
} else { } else {
source = game.getObject(sourceId); sourceAbilities = ((Permanent) source).getAbilities(game);
sourceControllerId = ((Permanent) source).getControllerId();
} }
if (source instanceof Spell) { if (sourceAbilities != null && sourceAbilities.containsKey(InfectAbility.getInstance().getId())) {
sourceAbilities = ((Spell) source).getAbilities(game); addCounters(CounterType.POISON.createInstance(actualDamage), game);
sourceControllerId = ((Spell) source).getControllerId(); } else {
} else if (source instanceof Card) { GameEvent damageToLifeLossEvent = new GameEvent(EventType.DAMAGE_CAUSES_LIFE_LOSS, playerId, sourceId, playerId, actualDamage, combatDamage);
sourceAbilities = ((Card) source).getAbilities(game); if (!game.replaceEvent(damageToLifeLossEvent)) {
sourceControllerId = ((Card) source).getOwnerId(); this.loseLife(damageToLifeLossEvent.getAmount(), game, combatDamage);
} else if (source instanceof CommandObject) { }
sourceControllerId = ((CommandObject) source).getControllerId();
sourceAbilities = ((CommandObject) source).getAbilities();
} }
} else { if (sourceAbilities != null && sourceAbilities.containsKey(LifelinkAbility.getInstance().getId())) {
sourceAbilities = ((Permanent) source).getAbilities(game); Player player = game.getPlayer(sourceControllerId);
sourceControllerId = ((Permanent) source).getControllerId(); player.gainLife(actualDamage, game);
}
if (sourceAbilities != null && sourceAbilities.containsKey(InfectAbility.getInstance().getId())) {
addCounters(CounterType.POISON.createInstance(actualDamage), game);
} else {
GameEvent damageToLifeLossEvent = new GameEvent(EventType.DAMAGE_CAUSES_LIFE_LOSS, playerId, sourceId, playerId, actualDamage, combatDamage);
if (!game.replaceEvent(damageToLifeLossEvent)) {
this.loseLife(damageToLifeLossEvent.getAmount(), game, combatDamage);
} }
game.fireEvent(new DamagedPlayerEvent(playerId, sourceId, playerId, actualDamage, combatDamage));
return actualDamage;
} }
if (sourceAbilities != null && sourceAbilities.containsKey(LifelinkAbility.getInstance().getId())) {
Player player = game.getPlayer(sourceControllerId);
player.gainLife(actualDamage, game);
}
game.fireEvent(new DamagedPlayerEvent(playerId, sourceId, playerId, actualDamage, combatDamage));
return actualDamage;
} }
} else {
MageObject sourceObject = game.getObject(sourceId);
game.informPlayers(damage + " damage "
+ (sourceObject == null ? "" : "from " + sourceObject.getLogName())
+ "to " + getLogName()
+ (damage > 1 ? " were" : "was") + " prevented because of protection.");
} }
} }
return 0; return 0;
@ -2924,15 +2940,27 @@ public abstract class PlayerImpl implements Player, Serializable {
return this.userData; return this.userData;
} }
public UserData getControllingPlayersUserData(Game game) {
if (isGameUnderControl()) {
Player player = game.getPlayer(getTurnControlledBy());
if (player.isHuman()) {
return player.getUserData();
}
}
return this.userData;
}
@Override @Override
public void setUserData(UserData userData) { public void setUserData(UserData userData
) {
this.userData = userData; this.userData = userData;
getManaPool().setAutoPayment(userData.isManaPoolAutomatic()); getManaPool().setAutoPayment(userData.isManaPoolAutomatic());
getManaPool().setAutoPaymentRestricted(userData.isManaPoolAutomaticRestricted()); getManaPool().setAutoPaymentRestricted(userData.isManaPoolAutomaticRestricted());
} }
@Override @Override
public void addAction(String action) { public void addAction(String action
) {
// do nothing // do nothing
} }
@ -2942,7 +2970,8 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public void setAllowBadMoves(boolean allowBadMoves) { public void setAllowBadMoves(boolean allowBadMoves
) {
// do nothing // do nothing
} }
@ -2952,17 +2981,21 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public void setCanPayLifeCost(boolean canPayLifeCost) { public void setCanPayLifeCost(boolean canPayLifeCost
) {
this.canPayLifeCost = canPayLifeCost; this.canPayLifeCost = canPayLifeCost;
} }
@Override @Override
public boolean canPaySacrificeCost(Permanent permanent, UUID sourceId, UUID controllerId, Game game) { public boolean canPaySacrificeCost(Permanent permanent, UUID sourceId,
UUID controllerId, Game game
) {
return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, sourceId, controllerId, game); return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, sourceId, controllerId, game);
} }
@Override @Override
public void setCanPaySacrificeCostFilter(FilterPermanent filter) { public void setCanPaySacrificeCostFilter(FilterPermanent filter
) {
this.sacrificeCostFilter = filter; this.sacrificeCostFilter = filter;
} }
@ -2977,7 +3010,8 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public void setLoseByZeroOrLessLife(boolean loseByZeroOrLessLife) { public void setLoseByZeroOrLessLife(boolean loseByZeroOrLessLife
) {
this.loseByZeroOrLessLife = loseByZeroOrLessLife; this.loseByZeroOrLessLife = loseByZeroOrLessLife;
} }
@ -2987,7 +3021,8 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public void setPlayCardsFromGraveyard(boolean playCardsFromGraveyard) { public void setPlayCardsFromGraveyard(boolean playCardsFromGraveyard
) {
this.canPlayCardsFromGraveyard = playCardsFromGraveyard; this.canPlayCardsFromGraveyard = playCardsFromGraveyard;
} }
@ -3014,12 +3049,14 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public void setStoredBookmark(int storedBookmark) { public void setStoredBookmark(int storedBookmark
) {
this.storedBookmark = storedBookmark; this.storedBookmark = storedBookmark;
} }
@Override @Override
public synchronized void resetStoredBookmark(Game game) { public synchronized void resetStoredBookmark(Game game
) {
if (this.storedBookmark != -1) { if (this.storedBookmark != -1) {
game.removeBookmark(this.storedBookmark); game.removeBookmark(this.storedBookmark);
} }
@ -3027,7 +3064,8 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public boolean lookAtFaceDownCard(Card card, Game game) { public boolean lookAtFaceDownCard(Card card, Game game
) {
if (game.getContinuousEffects().asThough(card.getId(), AsThoughEffectType.LOOK_AT_FACE_DOWN, this.getId(), game)) { if (game.getContinuousEffects().asThough(card.getId(), AsThoughEffectType.LOOK_AT_FACE_DOWN, this.getId(), game)) {
if (chooseUse(Outcome.Benefit, "Look at that card?", null, game)) { if (chooseUse(Outcome.Benefit, "Look at that card?", null, game)) {
Cards cards = new CardsImpl(card); Cards cards = new CardsImpl(card);
@ -3039,7 +3077,8 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public void setPriorityTimeLeft(int timeLeft) { public void setPriorityTimeLeft(int timeLeft
) {
priorityTimeLeft = timeLeft; priorityTimeLeft = timeLeft;
} }
@ -3064,7 +3103,8 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public void setReachedNextTurnAfterLeaving(boolean reachedNextTurnAfterLeaving) { public void setReachedNextTurnAfterLeaving(boolean reachedNextTurnAfterLeaving
) {
this.reachedNextTurnAfterLeaving = reachedNextTurnAfterLeaving; this.reachedNextTurnAfterLeaving = reachedNextTurnAfterLeaving;
} }
@ -3074,12 +3114,14 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public boolean canJoinTable(Table table) { public boolean canJoinTable(Table table
) {
return !table.userIsBanned(name); return !table.userIsBanned(name);
} }
@Override @Override
public void addCommanderId(UUID commanderId) { public void addCommanderId(UUID commanderId
) {
this.commandersIds.add(commanderId); this.commandersIds.add(commanderId);
} }
@ -3089,12 +3131,17 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public boolean moveCards(Card card, Zone toZone, Ability source, Game game) { public boolean moveCards(Card card, Zone toZone,
Ability source, Game game
) {
return moveCards(card, toZone, source, game, false, false, false, null); return moveCards(card, toZone, source, game, false, false, false, null);
} }
@Override @Override
public boolean moveCards(Card card, Zone toZone, Ability source, Game game, boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects) { public boolean moveCards(Card card, Zone toZone,
Ability source, Game game,
boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects
) {
Set<Card> cardList = new HashSet<>(); Set<Card> cardList = new HashSet<>();
if (card != null) { if (card != null) {
cardList.add(card); cardList.add(card);
@ -3103,17 +3150,24 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public boolean moveCards(Cards cards, Zone toZone, Ability source, Game game) { public boolean moveCards(Cards cards, Zone toZone,
Ability source, Game game
) {
return moveCards(cards.getCards(game), toZone, source, game); return moveCards(cards.getCards(game), toZone, source, game);
} }
@Override @Override
public boolean moveCards(Set<Card> cards, Zone toZone, Ability source, Game game) { public boolean moveCards(Set<Card> cards, Zone toZone,
Ability source, Game game
) {
return moveCards(cards, toZone, source, game, false, false, false, null); return moveCards(cards, toZone, source, game, false, false, false, null);
} }
@Override @Override
public boolean moveCards(Set<Card> cards, Zone toZone, Ability source, Game game, boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects) { public boolean moveCards(Set<Card> cards, Zone toZone,
Ability source, Game game,
boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects
) {
if (cards.isEmpty()) { if (cards.isEmpty()) {
return true; return true;
} }
@ -3187,14 +3241,20 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public boolean moveCardsToExile(Card card, Ability source, Game game, boolean withName, UUID exileId, String exileZoneName) { public boolean moveCardsToExile(Card card, Ability source,
Game game, boolean withName, UUID exileId,
String exileZoneName
) {
Set<Card> cards = new HashSet<>(); Set<Card> cards = new HashSet<>();
cards.add(card); cards.add(card);
return moveCardsToExile(cards, source, game, withName, exileId, exileZoneName); return moveCardsToExile(cards, source, game, withName, exileId, exileZoneName);
} }
@Override @Override
public boolean moveCardsToExile(Set<Card> cards, Ability source, Game game, boolean withName, UUID exileId, String exileZoneName) { public boolean moveCardsToExile(Set<Card> cards, Ability source,
Game game, boolean withName, UUID exileId,
String exileZoneName
) {
if (cards.isEmpty()) { if (cards.isEmpty()) {
return true; return true;
} }
@ -3207,12 +3267,16 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public boolean moveCardToHandWithInfo(Card card, UUID sourceId, Game game) { public boolean moveCardToHandWithInfo(Card card, UUID sourceId,
Game game
) {
return this.moveCardToHandWithInfo(card, sourceId, game, true); return this.moveCardToHandWithInfo(card, sourceId, game, true);
} }
@Override @Override
public boolean moveCardToHandWithInfo(Card card, UUID sourceId, Game game, boolean withName) { public boolean moveCardToHandWithInfo(Card card, UUID sourceId,
Game game, boolean withName
) {
boolean result = false; boolean result = false;
Zone fromZone = game.getState().getZone(card.getId()); Zone fromZone = game.getState().getZone(card.getId());
if (fromZone == Zone.BATTLEFIELD && !(card instanceof Permanent)) { if (fromZone == Zone.BATTLEFIELD && !(card instanceof Permanent)) {
@ -3235,7 +3299,9 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public Set<Card> moveCardsToGraveyardWithInfo(Set<Card> allCards, Ability source, Game game, Zone fromZone) { public Set<Card> moveCardsToGraveyardWithInfo(Set<Card> allCards, Ability source,
Game game, Zone fromZone
) {
UUID sourceId = source == null ? null : source.getSourceId(); UUID sourceId = source == null ? null : source.getSourceId();
Set<Card> movedCards = new LinkedHashSet<>(); Set<Card> movedCards = new LinkedHashSet<>();
while (!allCards.isEmpty()) { while (!allCards.isEmpty()) {
@ -3302,7 +3368,9 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public boolean moveCardToGraveyardWithInfo(Card card, UUID sourceId, Game game, Zone fromZone) { public boolean moveCardToGraveyardWithInfo(Card card, UUID sourceId,
Game game, Zone fromZone
) {
if (card == null) { if (card == null) {
return false; return false;
} }
@ -3329,7 +3397,10 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public boolean moveCardToLibraryWithInfo(Card card, UUID sourceId, Game game, Zone fromZone, boolean toTop, boolean withName) { public boolean moveCardToLibraryWithInfo(Card card, UUID sourceId,
Game game, Zone fromZone,
boolean toTop, boolean withName
) {
if (card == null) { if (card == null) {
return false; return false;
} }
@ -3361,7 +3432,11 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public boolean moveCardToExileWithInfo(Card card, UUID exileId, String exileName, UUID sourceId, Game game, Zone fromZone, boolean withName) { public boolean moveCardToExileWithInfo(Card card, UUID exileId,
String exileName, UUID sourceId,
Game game, Zone fromZone,
boolean withName
) {
if (card == null) { if (card == null) {
return false; return false;
} }
@ -3391,7 +3466,8 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public boolean hasOpponent(UUID playerToCheckId, Game game) { public boolean hasOpponent(UUID playerToCheckId, Game game
) {
return !this.getId().equals(playerToCheckId) && game.isOpponent(this, playerToCheckId); return !this.getId().equals(playerToCheckId) && game.isOpponent(this, playerToCheckId);
} }
@ -3436,7 +3512,8 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public void setJustActivatedType(AbilityType justActivatedType) { public void setJustActivatedType(AbilityType justActivatedType
) {
this.justActivatedType = justActivatedType; this.justActivatedType = justActivatedType;
} }
@ -3446,7 +3523,8 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public void addPermissionToShowHandCards(UUID watcherUserId) { public void addPermissionToShowHandCards(UUID watcherUserId
) {
usersAllowedToSeeHandCards.add(watcherUserId); usersAllowedToSeeHandCards.add(watcherUserId);
} }
@ -3456,7 +3534,8 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public boolean hasUserPermissionToSeeHand(UUID userId) { public boolean hasUserPermissionToSeeHand(UUID userId
) {
return usersAllowedToSeeHandCards.contains(userId); return usersAllowedToSeeHandCards.contains(userId);
} }
@ -3466,7 +3545,8 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public void setMatchPlayer(MatchPlayer matchPlayer) { public void setMatchPlayer(MatchPlayer matchPlayer
) {
this.matchPlayer = matchPlayer; this.matchPlayer = matchPlayer;
} }
@ -3481,7 +3561,9 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public boolean scry(int value, Ability source, Game game) { public boolean scry(int value, Ability source,
Game game
) {
game.informPlayers(getLogName() + " scries " + value); game.informPlayers(getLogName() + " scries " + value);
Cards cards = new CardsImpl(); Cards cards = new CardsImpl();
cards.addAll(getLibrary().getTopCards(game, value)); cards.addAll(getLibrary().getTopCards(game, value));
@ -3503,7 +3585,8 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
@Override @Override
public boolean addTargets(Ability ability, Game game) { public boolean addTargets(Ability ability, Game game
) {
// only used for TestPlayer to preSet Targets // only used for TestPlayer to preSet Targets
return true; return true;
} }