Improved network stability and other related fixes:

* server: fixed that a critical errors ignored in user commands threads (now it will be added to the logs);
* network: fixed frozen user responses in some use cases;
* network: fixed accidental and incorrect user responses (only latest response will be used now);
* network: improved freeze logs, added problem method name and code's line number;
* cheats: removed outdated deck and card load logic (only init.txt commands supports now);
* cheats: fixed wrong priority after add card dialog (closes #11437);
* cheats: improved stability and random errors on cheat executes (related to #11437);
* docs: added details on network and thread logic, human feedback life cycle, etc (see HumanPlayer, ThreadExecutorImpl);
This commit is contained in:
Oleg Agafonov 2023-11-24 21:22:16 +04:00
parent 4ba3e1fec5
commit 53add71826
33 changed files with 476 additions and 273 deletions

View file

@ -225,8 +225,8 @@ public final class SessionHandler {
return session.isTestMode(); return session.isTestMode();
} }
public static void cheat(UUID gameId, UUID playerId, DeckCardLists deckCardLists) { public static void cheatShow(UUID gameId, UUID playerId) {
session.cheat(gameId, playerId, deckCardLists); session.cheatShow(gameId, playerId);
} }
public static String getSessionId() { public static String getSessionId() {

View file

@ -588,7 +588,7 @@ public class PlayAreaPanel extends javax.swing.JPanel {
} }
private void btnCheatActionPerformed(java.awt.event.ActionEvent evt) { private void btnCheatActionPerformed(java.awt.event.ActionEvent evt) {
SessionHandler.cheat(gameId, playerId, DeckImporter.importDeckFromFile("cheat.dck", false)); SessionHandler.cheatShow(gameId, playerId);
} }
public boolean isSmallMode() { public boolean isSmallMode() {

View file

@ -970,8 +970,7 @@ public class PlayerPanelExt extends javax.swing.JPanel {
} }
private void btnCheatActionPerformed(java.awt.event.ActionEvent evt) { private void btnCheatActionPerformed(java.awt.event.ActionEvent evt) {
DckDeckImporter deckImporter = new DckDeckImporter(); SessionHandler.cheatShow(gameId, playerId);
SessionHandler.cheat(gameId, playerId, deckImporter.importDeck("cheat.dck", false));
} }
private void btnToolHintsHelperActionPerformed(java.awt.event.ActionEvent evt) { private void btnToolHintsHelperActionPerformed(java.awt.event.ActionEvent evt) {

View file

@ -64,8 +64,4 @@ public interface CardImageSource {
default boolean isTokenImageProvided(String setCode, String cardName, Integer tokenNumber) { default boolean isTokenImageProvided(String setCode, String cardName, Integer tokenNumber) {
return false; return false;
} }
default int getDownloadTimeoutMs() {
return 0;
}
} }

View file

@ -155,9 +155,7 @@ public interface MageServer {
void replaySkipForward(UUID gameId, String sessionId, int moves) throws MageException; void replaySkipForward(UUID gameId, String sessionId, int moves) throws MageException;
void cheatMultiple(UUID gameId, String sessionId, UUID playerId, DeckCardLists deckList) throws MageException; void cheatShow(UUID gameId, String sessionId, UUID playerId) throws MageException;
boolean cheatOne(UUID gameId, String sessionId, UUID playerId, String cardName) throws MageException;
List<UserView> adminGetUsers(String sessionId) throws MageException; List<UserView> adminGetUsers(String sessionId) throws MageException;

View file

@ -1499,10 +1499,10 @@ public class SessionImpl implements Session {
} }
@Override @Override
public boolean cheat(UUID gameId, UUID playerId, DeckCardLists deckList) { public boolean cheatShow(UUID gameId, UUID playerId) {
try { try {
if (isConnected()) { if (isConnected()) {
server.cheatMultiple(gameId, sessionId, playerId, deckList); server.cheatShow(gameId, sessionId, playerId);
return true; return true;
} }
} catch (MageException ex) { } catch (MageException ex) {

View file

@ -12,5 +12,5 @@ public interface Testable {
boolean isTestMode(); boolean isTestMode();
boolean cheat(UUID gameId, UUID playerId, DeckCardLists deckList); boolean cheatShow(UUID gameId, UUID playerId);
} }

View file

@ -1,4 +1,4 @@
package mage.server.util; package mage.utils;
import mage.MageObject; import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
@ -248,23 +248,15 @@ public final class SystemUtil {
} }
/** /**
* Replaces cards in player's hands by specified in config/init.txt.<br/> * Execute cheat commands from an init.txt file, for more details see
* <br/> * <a href="https://github.com/magefree/mage/wiki/Development-Testing-Tools">wiki page</a>
* <b>Implementation note:</b><br/> * about testing tools
* 1. Read init.txt line by line<br/>
* 2. Parse line using for searching groups like: [group 1] 3. Parse line
* using the following format: line ::=
* <zone>:<nickname>:<card name>:<amount><br/>
* 4. If zone equals to 'hand', add card to player's library<br/>
* 5a. Then swap added card with any card in player's hand<br/>
* 5b. Parse next line (go to 2.), If EOF go to 4.<br/>
* 6. Log message to all players that cards were added (to prevent unfair
* play).<br/>
* 7. Exit<br/>
* *
* @param game * @param game
* @param commandsFilePath file path with commands in init.txt format
* @param feedbackPlayer player to execute that cheats (will see choose dialogs)
*/ */
public static void addCardsForTesting(Game game, String fileSource, Player feedbackPlayer) { public static void executeCheatCommands(Game game, String commandsFilePath, Player feedbackPlayer) {
// fake test ability for triggers and events // fake test ability for triggers and events
Ability fakeSourceAbilityTemplate = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("adding testing cards")); Ability fakeSourceAbilityTemplate = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("adding testing cards"));
@ -272,7 +264,7 @@ public final class SystemUtil {
List<String> errorsList = new ArrayList<>(); List<String> errorsList = new ArrayList<>();
try { try {
String fileName = fileSource; String fileName = commandsFilePath;
if (fileName == null) { if (fileName == null) {
fileName = INIT_FILE_PATH; fileName = INIT_FILE_PATH;
} }
@ -755,12 +747,13 @@ public final class SystemUtil {
} }
} }
} catch (Exception e) { } catch (Exception e) {
String mes = String.format("Catch critical error: %s", e.getMessage()); String mes = String.format("Catch critical error on cheating: %s", e.getMessage());
errorsList.add(mes); errorsList.add(mes);
logger.error(mes, e); logger.error(mes, e);
} } finally {
sendCheatCommandsFeedback(game, feedbackPlayer, errorsList); sendCheatCommandsFeedback(game, feedbackPlayer, errorsList);
} }
}
private static void sendCheatCommandsFeedback(Game game, Player feedbackPlayer, List<String> errorsList) { private static void sendCheatCommandsFeedback(Game game, Player feedbackPlayer, List<String> errorsList) {
// inform all players about wrong commands or other errors // inform all players about wrong commands or other errors
@ -907,4 +900,21 @@ public final class SystemUtil {
} }
return Arrays.asList(cardSet, cardName); return Arrays.asList(cardSet, cardName);
} }
public static void ensureRunInGameThread() {
String name = Thread.currentThread().getName();
if (!name.startsWith("GAME")) {
// how-to fix: use signal logic to inform a game about new command to execute instead direct execute (see example with WantConcede)
// reason: user responses/commands are received by network/call thread, but must be processed by game thread
throw new IllegalArgumentException("Wrong code usage: game related code must run in GAME thread, but it used in " + name, new Throwable());
}
}
public static void ensureRunInCallThread() {
String name = Thread.currentThread().getName();
if (!name.startsWith("CALL")) {
// how-to fix: something wrong in your code logic
throw new IllegalArgumentException("Wrong code usage: client commands code must run in CALL threads, but used in " + name, new Throwable());
}
}
} }

View file

@ -45,6 +45,7 @@ import mage.target.common.TargetAttackingCreature;
import mage.target.common.TargetDefender; import mage.target.common.TargetDefender;
import mage.target.targetpointer.TargetPointer; import mage.target.targetpointer.TargetPointer;
import mage.util.*; import mage.util.*;
import mage.utils.SystemUtil;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.awt.*; import java.awt.*;
@ -58,7 +59,7 @@ import static mage.constants.PlayerAction.REQUEST_AUTO_ANSWER_RESET_ALL;
import static mage.constants.PlayerAction.TRIGGER_AUTO_ORDER_RESET_ALL; import static mage.constants.PlayerAction.TRIGGER_AUTO_ORDER_RESET_ALL;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com, JayDi85
*/ */
public class HumanPlayer extends PlayerImpl { public class HumanPlayer extends PlayerImpl {
@ -66,8 +67,36 @@ public class HumanPlayer extends PlayerImpl {
// TODO: all user feedback actions executed and waited in diff threads and can't catch exeptions, e.g. on wrong code usage // TODO: all user feedback actions executed and waited in diff threads and can't catch exeptions, e.g. on wrong code usage
// must catch and log such errors // must catch and log such errors
private transient Boolean responseOpenedForAnswer = false; // can't get response until prepared target (e.g. until send all fire events to all players)
// Network and threads logic:
// * starting point: ThreadExecutorImpl.java
// * server executing a game's code by single game thread (named: "GAME xxx", one thread per game)
// * data transfering goes by inner jboss threads (named: "WorkerThread xxx", one thread per client connection)
// - from server to client: sync mode, a single game thread uses jboss networking to send data to each player and viewer/watcher one by one
// - from client to server: async mode, each income command executes by shared call thread
//
// Client commands logic:
// * commands can do anything in server, user, table and game contexts
// * it's income in async mode at any time and in any order by "CALL xxx" threads
// * there are two types of client commands:
// - feedback commands - must be executed and synced by GAME thread (example: answer for choose dialog, priority, etc)
// - other commands - can be executed at any time and don't require sync with a game and a GAME thread (example: user/skip settings, concede/cheat, chat messages)
//
// Feedback commands logic:
// * income by CALL thread
// * must be synced and executed by GAME thread
// * keep only latest income feedback (if user sends multiple clicks/choices)
// * HumanPlayer contains "response" object for threads sync and data exchange
// * so sync logic:
// * - GAME thread: open response for income command and wait (go to sleep by response.wait)
// * - CALL thread: on closed response - waiting open status of player's response object (if it's too long then cancel the answer)
// * - CALL thread: on opened response - save answer to player's response object and notify GAME thread about it by response.notifyAll
// * - GAME thread: on nofify from response - check new answer value and process it (if it bad then repeat and wait the next one);
private transient Boolean responseOpenedForAnswer = false; // GAME thread waiting new answer
private transient long responseLastWaitingThreadId = 0;
private final transient PlayerResponse response = new PlayerResponse(); private final transient PlayerResponse response = new PlayerResponse();
private final int RESPONSE_WAITING_TIME_SECS = 30; // waiting time before cancel current response
private final int RESPONSE_WAITING_CHECK_MS = 100; // timeout for open status check
protected static FilterCreatureForCombatBlock filterCreatureForCombatBlock = new FilterCreatureForCombatBlock(); protected static FilterCreatureForCombatBlock filterCreatureForCombatBlock = new FilterCreatureForCombatBlock();
protected static FilterCreatureForCombat filterCreatureForCombat = new FilterCreatureForCombat(); protected static FilterCreatureForCombat filterCreatureForCombat = new FilterCreatureForCombat();
@ -141,24 +170,68 @@ public class HumanPlayer extends PlayerImpl {
|| (actionIterations > 0 && !actionQueueSaved.isEmpty())); || (actionIterations > 0 && !actionQueueSaved.isEmpty()));
} }
protected void waitResponseOpen() { /**
// wait response open for answer process * Waiting for opened response and save new value in it
int numTimesWaiting = 0; * Use it in CALL threads only, e.g. for client commands
*
* @return on true result game can use response value, on false result - it's outdated response
*/
protected boolean waitResponseOpen() {
// client send commands in async mode and can come too early
// so if next command come too fast then wait here until game ready
//
// example with too early response:
// * game must send new update to 3 users and ask user 2 for feedback answer, but user 3 is too slow
// +0 secs: start sending data to 3 players
// +0 secs: user 1 getting data
// +1 secs: user 1 done
// +1 secs: user 2 getting data
// +3 secs: user 2 done
// +3 secs: user 3 getting data (it's slow)
// +4 secs: user 2 sent answer 1 (but game closed for it, so waiting)
// +5 secs: user 2 sent answer 2 (he can't see changes after answer 1, so trying again - server must keep only latest answer)
// +8 secs: user 3 done
// +8 secs: game start wating a new answer from user 2
// +8 secs: game find answer
int currentTimesWaiting = 0;
int maxTimesWaiting = RESPONSE_WAITING_TIME_SECS * 1000 / RESPONSE_WAITING_CHECK_MS;
long currentThreadId = Thread.currentThread().getId();
// it's a latest response
responseLastWaitingThreadId = currentThreadId;
while (!responseOpenedForAnswer && canRespond()) { while (!responseOpenedForAnswer && canRespond()) {
numTimesWaiting++; if (responseLastWaitingThreadId != currentThreadId) {
if (numTimesWaiting >= 300) { // there is another latest response, so cancel current
// game frozen -- need to report about error and continue to execute return false;
String s = "Game frozen in waitResponseOpen for user " + getName() + " (connection problem)"; }
logger.warn(s);
break; // keep waiting
currentTimesWaiting++;
if (currentTimesWaiting > maxTimesWaiting) {
// game frozen, possible reasons:
// * ANOTHER player lost connection and GAME thread trying to send data to him
// * current player send answer, but lost connect after it
String possibleReason;
if (response.getActiveAction() == null) {
possibleReason = "maybe connection problem with another player/watcher";
} else {
possibleReason = "something wrong with your priority on " + response.getActiveAction();
}
logger.warn(String.format("Game frozen in waitResponseOpen for %d secs: current user %s, %s",
RESPONSE_WAITING_CHECK_MS * currentTimesWaiting / 1000,
this.getName(),
possibleReason
));
return false;
} }
try { try {
Thread.sleep(100); Thread.sleep(RESPONSE_WAITING_CHECK_MS);
} catch (InterruptedException e) { } catch (InterruptedException ignore) {
logger.warn("Response waiting interrupted for " + getId());
} }
} }
return true; // can use new value
} }
protected boolean pullResponseFromQueue(Game game) { protected boolean pullResponseFromQueue(Game game) {
@ -179,7 +252,7 @@ public class HumanPlayer extends PlayerImpl {
} }
//waitResponseOpen(); // it's a macro action, no need it here? //waitResponseOpen(); // it's a macro action, no need it here?
synchronized (response) { synchronized (response) {
response.copy(action); response.copyFrom(action);
response.notifyAll(); response.notifyAll();
macroTriggeredSelectionFlag = false; macroTriggeredSelectionFlag = false;
return true; return true;
@ -188,12 +261,39 @@ public class HumanPlayer extends PlayerImpl {
return false; return false;
} }
/**
* Prepare priority player for new feedback, call it for every choose cycle before waitForResponse
*/
protected void prepareForResponse(Game game) { protected void prepareForResponse(Game game) {
//logger.info("Prepare waiting " + getId()); SystemUtil.ensureRunInGameThread();
// prepare priority player
// on null - it's a discard in cleanaup and other non-user code, so don't change it here at that moment
// TODO: must research null use case
if (game.getState().getPriorityPlayerId() != null) {
if (getId() == null) {
logger.fatal("Player with no ID: " + name);
this.quit(game);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Setting game priority for " + getId() + " [" + DebugUtil.getMethodNameWithSource(2) + ']');
}
game.getState().setPriorityPlayerId(getId());
}
responseOpenedForAnswer = false; responseOpenedForAnswer = false;
} }
/**
* Waiting feedback from priority player
*
* @param game
*/
protected void waitForResponse(Game game) { protected void waitForResponse(Game game) {
SystemUtil.ensureRunInGameThread();
;
if (isExecutingMacro()) { if (isExecutingMacro()) {
pullResponseFromQueue(game); pullResponseFromQueue(game);
// logger.info("MACRO pull from queue: " + response.toString()); // logger.info("MACRO pull from queue: " + response.toString());
@ -204,36 +304,46 @@ public class HumanPlayer extends PlayerImpl {
return; return;
} }
// wait player's answer loop
boolean loop = true; boolean loop = true;
while (loop) { while (loop) {
// start waiting for next answer // start waiting for next answer
response.clear(); response.clear();
response.setActiveAction(DebugUtil.getMethodNameWithSource(2));
game.resumeTimer(getTurnControlledBy()); game.resumeTimer(getTurnControlledBy());
responseOpenedForAnswer = true; responseOpenedForAnswer = true;
loop = false; loop = false;
synchronized (response) { // TODO: synchronized response smells bad here, possible deadlocks? Need research
synchronized (response) {
try { try {
response.wait(); response.wait(); // start waiting a response.notifyAll command from CALL thread (client answer)
} catch (InterruptedException ex) { } catch (InterruptedException ignore) {
logger.error("Response error for player " + getName() + " gameId: " + game.getId(), ex);
} finally { } finally {
responseOpenedForAnswer = false; responseOpenedForAnswer = false;
game.pauseTimer(getTurnControlledBy()); game.pauseTimer(getTurnControlledBy());
} }
} }
// async command: concede by any player
// game recived immediately response on OTHER player concede -- need to process end game and continue to wait // game recived immediately response on OTHER player concede -- need to process end game and continue to wait
if (response.getResponseConcedeCheck()) { // TODO: is it possible to break choose dialog of current player (check it in multiplayer)?
if (response.getAsyncWantConcede()) {
((GameImpl) game).checkConcede(); ((GameImpl) game).checkConcede();
if (game.hasEnded()) { if (game.hasEnded()) {
return; return;
} }
if (canRespond()) {
// wait another answer // wait another answer
if (canRespond()) {
loop = true;
}
}
// async command: cheat by current player
if (response.getAsyncWantCheat()) {
// execute cheats and continue
SystemUtil.executeCheatCommands(game, null, this);
game.fireUpdatePlayersEvent(); // need force to game update for new possible data
// wait another answer
if (canRespond()) {
loop = true; loop = true;
} }
} }
@ -265,7 +375,6 @@ public class HumanPlayer extends PlayerImpl {
options.put("UI.left.btn.text", "Mulligan"); options.put("UI.left.btn.text", "Mulligan");
options.put("UI.right.btn.text", "Keep"); options.put("UI.right.btn.text", "Keep");
updateGameStatePriority("chooseMulligan", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireAskPlayerEvent(playerId, new MessageToClient(message), null, options); game.fireAskPlayerEvent(playerId, new MessageToClient(message), null, options);
@ -323,7 +432,6 @@ public class HumanPlayer extends PlayerImpl {
messageToClient.setSecondMessage(getRelatedObjectName(source, game)); messageToClient.setSecondMessage(getRelatedObjectName(source, game));
} }
updateGameStatePriority("chooseUse", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireAskPlayerEvent(playerId, messageToClient, source, options); game.fireAskPlayerEvent(playerId, messageToClient, source, options);
@ -415,7 +523,6 @@ public class HumanPlayer extends PlayerImpl {
} }
while (canRespond()) { while (canRespond()) {
updateGameStatePriority("chooseEffect", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireChooseChoiceEvent(playerId, replacementEffectChoice); game.fireChooseChoiceEvent(playerId, replacementEffectChoice);
@ -493,7 +600,6 @@ public class HumanPlayer extends PlayerImpl {
} }
while (canRespond()) { while (canRespond()) {
updateGameStatePriority("choose(3)", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireChooseChoiceEvent(playerId, choice); game.fireChooseChoiceEvent(playerId, choice);
@ -556,7 +662,6 @@ public class HumanPlayer extends PlayerImpl {
List<UUID> chosenTargets = target.getTargets(); List<UUID> chosenTargets = target.getTargets();
options.put("chosenTargets", (Serializable) chosenTargets); options.put("chosenTargets", (Serializable) chosenTargets);
updateGameStatePriority("choose(5)", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireSelectTargetEvent(getId(), new MessageToClient(target.getMessage(), getRelatedObjectName(source, game)), possibleTargetIds, required, getOptions(target, options)); game.fireSelectTargetEvent(getId(), new MessageToClient(target.getMessage(), getRelatedObjectName(source, game)), possibleTargetIds, required, getOptions(target, options));
@ -658,7 +763,6 @@ public class HumanPlayer extends PlayerImpl {
List<UUID> chosenTargets = target.getTargets(); List<UUID> chosenTargets = target.getTargets();
options.put("chosenTargets", (Serializable) chosenTargets); options.put("chosenTargets", (Serializable) chosenTargets);
updateGameStatePriority("chooseTarget", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireSelectTargetEvent(getId(), new MessageToClient(target.getMessage(), getRelatedObjectName(source, game)), game.fireSelectTargetEvent(getId(), new MessageToClient(target.getMessage(), getRelatedObjectName(source, game)),
@ -757,7 +861,6 @@ public class HumanPlayer extends PlayerImpl {
options.put("possibleTargets", (Serializable) possibleTargets); options.put("possibleTargets", (Serializable) possibleTargets);
} }
updateGameStatePriority("choose(4)", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireSelectTargetEvent(playerId, new MessageToClient(target.getMessage()), cards, required, options); game.fireSelectTargetEvent(playerId, new MessageToClient(target.getMessage()), cards, required, options);
@ -841,7 +944,6 @@ public class HumanPlayer extends PlayerImpl {
options.put("possibleTargets", (Serializable) possibleTargets); options.put("possibleTargets", (Serializable) possibleTargets);
} }
updateGameStatePriority("chooseTarget(5)", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireSelectTargetEvent(playerId, new MessageToClient(target.getMessage(), getRelatedObjectName(source, game)), cards, required, options); game.fireSelectTargetEvent(playerId, new MessageToClient(target.getMessage(), getRelatedObjectName(source, game)), cards, required, options);
@ -936,7 +1038,6 @@ public class HumanPlayer extends PlayerImpl {
options.put("possibleTargets", (Serializable) possibleTargets); options.put("possibleTargets", (Serializable) possibleTargets);
} }
updateGameStatePriority("chooseTargetAmount", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
String multiType = multiAmountType == MultiAmountType.DAMAGE ? " to divide %d damage" : " to distribute %d counters"; String multiType = multiAmountType == MultiAmountType.DAMAGE ? " to divide %d damage" : " to distribute %d counters";
@ -1204,7 +1305,6 @@ public class HumanPlayer extends PlayerImpl {
while (canRespond()) { while (canRespond()) {
holdingPriority = false; holdingPriority = false;
updateGameStatePriority("priority", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.firePriorityEvent(playerId); game.firePriorityEvent(playerId);
@ -1428,7 +1528,6 @@ public class HumanPlayer extends PlayerImpl {
} }
macroTriggeredSelectionFlag = true; macroTriggeredSelectionFlag = true;
updateGameStatePriority("chooseTriggeredAbility", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireSelectTargetTriggeredAbilityEvent(playerId, "Pick triggered ability (goes to the stack first)", abilitiesWithNoOrderSet); game.fireSelectTargetTriggeredAbilityEvent(playerId, "Pick triggered ability (goes to the stack first)", abilitiesWithNoOrderSet);
@ -1477,7 +1576,6 @@ public class HumanPlayer extends PlayerImpl {
// TODO: make canRespond cycle? // TODO: make canRespond cycle?
if (canRespond()) { if (canRespond()) {
Map<String, Serializable> options = new HashMap<>(); Map<String, Serializable> options = new HashMap<>();
updateGameStatePriority("playMana", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.firePlayManaEvent(playerId, "Pay " + promptText, options); game.firePlayManaEvent(playerId, "Pay " + promptText, options);
@ -1486,17 +1584,19 @@ public class HumanPlayer extends PlayerImpl {
UUID responseId = getFixedResponseUUID(game); UUID responseId = getFixedResponseUUID(game);
if (response.getBoolean() != null) { if (response.getBoolean() != null) {
// cancel
return false; return false;
} else if (responseId != null) { } else if (responseId != null) {
// pay from mana object
playManaAbilities(responseId, abilityToCast, unpaid, game); playManaAbilities(responseId, abilityToCast, unpaid, game);
} else if (response.getString() != null } else if (response.getString() != null && response.getString().equals("special")) {
&& response.getString().equals("special")) { // pay from special action
if (unpaid instanceof ManaCostsImpl) { if (unpaid instanceof ManaCostsImpl) {
activateSpecialAction(game, unpaid); activateSpecialAction(game, unpaid);
} }
} else if (response.getManaType() != null) { } else if (response.getManaType() != null) {
// this mana type can be paid once from pool // pay from own mana pool
if (response.getResponseManaTypePlayerId().equals(this.getId())) { if (response.getManaPlayerId().equals(this.getId())) {
this.getManaPool().unlockManaType(response.getManaType()); this.getManaPool().unlockManaType(response.getManaType());
} }
// TODO: Handle if mana pool // TODO: Handle if mana pool
@ -1521,7 +1621,6 @@ public class HumanPlayer extends PlayerImpl {
int xValue = 0; int xValue = 0;
while (canRespond()) { while (canRespond()) {
updateGameStatePriority("announceRepetitions", game);
prepareForResponse(game); prepareForResponse(game);
game.fireGetAmountEvent(playerId, "How many times do you want to repeat your shortcut?", 0, 999); game.fireGetAmountEvent(playerId, "How many times do you want to repeat your shortcut?", 0, 999);
waitForResponse(game); waitForResponse(game);
@ -1557,7 +1656,6 @@ public class HumanPlayer extends PlayerImpl {
int xValue = 0; int xValue = 0;
String extraMessage = (multiplier == 1 ? "" : ", X will be increased by " + multiplier + " times"); String extraMessage = (multiplier == 1 ? "" : ", X will be increased by " + multiplier + " times");
while (canRespond()) { while (canRespond()) {
updateGameStatePriority("announceXMana", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireGetAmountEvent(playerId, message + extraMessage, min, max); game.fireGetAmountEvent(playerId, message + extraMessage, min, max);
@ -1583,7 +1681,6 @@ public class HumanPlayer extends PlayerImpl {
int xValue = 0; int xValue = 0;
while (canRespond()) { while (canRespond()) {
updateGameStatePriority("announceXCost", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireGetAmountEvent(playerId, message, min, max); game.fireGetAmountEvent(playerId, message, min, max);
@ -1602,7 +1699,6 @@ public class HumanPlayer extends PlayerImpl {
} }
protected void playManaAbilities(UUID objectId, Ability abilityToCast, ManaCost unpaid, Game game) { protected void playManaAbilities(UUID objectId, Ability abilityToCast, ManaCost unpaid, Game game) {
updateGameStatePriority("playManaAbilities", game);
MageObject object = game.getObject(objectId); MageObject object = game.getObject(objectId);
if (object == null) { if (object == null) {
return; return;
@ -1703,7 +1799,6 @@ public class HumanPlayer extends PlayerImpl {
options.put(Constants.Option.SPECIAL_BUTTON, "All attack"); options.put(Constants.Option.SPECIAL_BUTTON, "All attack");
} }
updateGameStatePriority("selectAttackers", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireSelectEvent(playerId, "Select attackers", options); game.fireSelectEvent(playerId, "Select attackers", options);
@ -1930,7 +2025,6 @@ public class HumanPlayer extends PlayerImpl {
} }
while (canRespond()) { while (canRespond()) {
updateGameStatePriority("selectBlockers", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
Map<String, Serializable> options = new HashMap<>(); Map<String, Serializable> options = new HashMap<>();
@ -1974,7 +2068,6 @@ public class HumanPlayer extends PlayerImpl {
} }
while (canRespond()) { while (canRespond()) {
updateGameStatePriority("chooseAttackerOrder", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireSelectTargetEvent(playerId, "Pick attacker", attackers, true); game.fireSelectTargetEvent(playerId, "Pick attacker", attackers, true);
@ -2000,7 +2093,6 @@ public class HumanPlayer extends PlayerImpl {
} }
while (canRespond()) { while (canRespond()) {
updateGameStatePriority("chooseBlockerOrder", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireSelectTargetEvent(playerId, "Pick blocker", blockers, true); game.fireSelectTargetEvent(playerId, "Pick blocker", blockers, true);
@ -2032,7 +2124,6 @@ public class HumanPlayer extends PlayerImpl {
UUID responseId = null; UUID responseId = null;
updateGameStatePriority("selectCombatGroup", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
// possible attackers to block // possible attackers to block
@ -2075,7 +2166,6 @@ public class HumanPlayer extends PlayerImpl {
@Override @Override
public void assignDamage(int damage, java.util.List<UUID> targets, String singleTargetName, UUID attackerId, Ability source, Game game) { public void assignDamage(int damage, java.util.List<UUID> targets, String singleTargetName, UUID attackerId, Ability source, Game game) {
updateGameStatePriority("assignDamage", game);
int remainingDamage = damage; int remainingDamage = damage;
while (remainingDamage > 0 && canRespond()) { while (remainingDamage > 0 && canRespond()) {
Target target = new TargetAnyTarget(); Target target = new TargetAnyTarget();
@ -2083,9 +2173,9 @@ public class HumanPlayer extends PlayerImpl {
if (singleTargetName != null) { if (singleTargetName != null) {
target.setTargetName(singleTargetName); target.setTargetName(singleTargetName);
} }
choose(Outcome.Damage, target, source, game); this.choose(Outcome.Damage, target, source, game);
if (targets.isEmpty() || targets.contains(target.getFirstTarget())) { if (targets.isEmpty() || targets.contains(target.getFirstTarget())) {
int damageAmount = getAmount(0, remainingDamage, "Select amount", game); int damageAmount = this.getAmount(0, remainingDamage, "Select amount", game);
Permanent permanent = game.getPermanent(target.getFirstTarget()); Permanent permanent = game.getPermanent(target.getFirstTarget());
if (permanent != null) { if (permanent != null) {
permanent.damage(damageAmount, attackerId, source, game, false, true); permanent.damage(damageAmount, attackerId, source, game, false, true);
@ -2108,7 +2198,6 @@ public class HumanPlayer extends PlayerImpl {
} }
while (canRespond()) { while (canRespond()) {
updateGameStatePriority("getAmount", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireGetAmountEvent(playerId, message, min, max); game.fireGetAmountEvent(playerId, message, min, max);
@ -2149,7 +2238,6 @@ public class HumanPlayer extends PlayerImpl {
List<Integer> answer = null; List<Integer> answer = null;
while (canRespond()) { while (canRespond()) {
updateGameStatePriority("getMultiAmount", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
Map<String, Serializable> options = new HashMap<>(2); Map<String, Serializable> options = new HashMap<>(2);
@ -2222,8 +2310,6 @@ public class HumanPlayer extends PlayerImpl {
Map<UUID, SpecialAction> specialActions = game.getState().getSpecialActions().getControlledBy(playerId, unpaidForManaAction != null); Map<UUID, SpecialAction> specialActions = game.getState().getSpecialActions().getControlledBy(playerId, unpaidForManaAction != null);
if (!specialActions.isEmpty()) { if (!specialActions.isEmpty()) {
updateGameStatePriority("specialAction", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireGetChoiceEvent(playerId, name, null, new ArrayList<>(specialActions.values())); game.fireGetChoiceEvent(playerId, name, null, new ArrayList<>(specialActions.values()));
@ -2253,8 +2339,6 @@ public class HumanPlayer extends PlayerImpl {
return; return;
} }
updateGameStatePriority("activateAbility", game);
if (abilities.size() == 1 if (abilities.size() == 1
&& suppressAbilityPicker(abilities.values().iterator().next(), game)) { && suppressAbilityPicker(abilities.values().iterator().next(), game)) {
ActivatedAbility ability = abilities.values().iterator().next(); ActivatedAbility ability = abilities.values().iterator().next();
@ -2281,7 +2365,8 @@ public class HumanPlayer extends PlayerImpl {
message = message + "<br>" + object.getLogName(); message = message + "<br>" + object.getLogName();
} }
// TODO: add canRespond cycle? // it's inner method, parent code already uses while and canRespond cycle,
// so can request one time here
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireGetChoiceEvent(playerId, message, object, new ArrayList<>(abilities.values())); game.fireGetChoiceEvent(playerId, message, object, new ArrayList<>(abilities.values()));
@ -2305,6 +2390,8 @@ public class HumanPlayer extends PlayerImpl {
*/ */
private boolean suppressAbilityPicker(ActivatedAbility ability, Game game) { private boolean suppressAbilityPicker(ActivatedAbility ability, Game game) {
if (getControllingPlayersUserData(game).isShowAbilityPickerForced()) { if (getControllingPlayersUserData(game).isShowAbilityPickerForced()) {
// TODO: is it bugged on mana payment + under control?
// (if player under control then priority player must use own settings, not controlling)
// user activated an ability picker in preferences // user activated an ability picker in preferences
// force to show ability picker for double faces cards in hand/commander/exile and other zones // force to show ability picker for double faces cards in hand/commander/exile and other zones
@ -2355,7 +2442,6 @@ public class HumanPlayer extends PlayerImpl {
} else if (useableAbilities != null } else if (useableAbilities != null
&& !useableAbilities.isEmpty()) { && !useableAbilities.isEmpty()) {
updateGameStatePriority("chooseAbilityForCast", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireGetChoiceEvent(playerId, message, object, new ArrayList<>(useableAbilities.values())); game.fireGetChoiceEvent(playerId, message, object, new ArrayList<>(useableAbilities.values()));
@ -2404,7 +2490,6 @@ public class HumanPlayer extends PlayerImpl {
case 1: case 1:
return useableAbilities.values().iterator().next(); return useableAbilities.values().iterator().next();
default: default:
updateGameStatePriority("chooseLandOrSpellAbility", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
String message = "Choose spell or ability to play" + (noMana ? " for FREE" : "") + "<br>" + object.getLogName(); String message = "Choose spell or ability to play" + (noMana ? " for FREE" : "") + "<br>" + object.getLogName();
@ -2491,7 +2576,6 @@ public class HumanPlayer extends PlayerImpl {
message = message + "<br>" + obj.getLogName(); message = message + "<br>" + obj.getLogName();
} }
updateGameStatePriority("chooseMode", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireGetModeEvent(playerId, message, modeMap); game.fireGetModeEvent(playerId, message, modeMap);
@ -2541,7 +2625,6 @@ public class HumanPlayer extends PlayerImpl {
} }
while (canRespond()) { while (canRespond()) {
updateGameStatePriority("choosePile", game);
prepareForResponse(game); prepareForResponse(game);
if (!isExecutingMacro()) { if (!isExecutingMacro()) {
game.fireChoosePileEvent(playerId, message, pile1, pile2); game.fireChoosePileEvent(playerId, message, pile1, pile2);
@ -2562,7 +2645,9 @@ public class HumanPlayer extends PlayerImpl {
@Override @Override
public void setResponseString(String responseString) { public void setResponseString(String responseString) {
waitResponseOpen(); if (!waitResponseOpen()) {
return;
}
synchronized (response) { synchronized (response) {
response.setString(responseString); response.setString(responseString);
response.notifyAll(); response.notifyAll();
@ -2572,10 +2657,12 @@ public class HumanPlayer extends PlayerImpl {
@Override @Override
public void setResponseManaType(UUID manaTypePlayerId, ManaType manaType) { public void setResponseManaType(UUID manaTypePlayerId, ManaType manaType) {
waitResponseOpen(); if (!waitResponseOpen()) {
return;
}
synchronized (response) { synchronized (response) {
response.setManaType(manaType); response.setManaType(manaType);
response.setResponseManaTypePlayerId(manaTypePlayerId); response.setResponseManaPlayerId(manaTypePlayerId);
response.notifyAll(); response.notifyAll();
logger.debug("Got response mana type from player: " + getId()); logger.debug("Got response mana type from player: " + getId());
} }
@ -2583,7 +2670,9 @@ public class HumanPlayer extends PlayerImpl {
@Override @Override
public void setResponseUUID(UUID responseUUID) { public void setResponseUUID(UUID responseUUID) {
waitResponseOpen(); if (!waitResponseOpen()) {
return;
}
synchronized (response) { synchronized (response) {
response.setUUID(responseUUID); response.setUUID(responseUUID);
response.notifyAll(); response.notifyAll();
@ -2593,7 +2682,9 @@ public class HumanPlayer extends PlayerImpl {
@Override @Override
public void setResponseBoolean(Boolean responseBoolean) { public void setResponseBoolean(Boolean responseBoolean) {
waitResponseOpen(); if (!waitResponseOpen()) {
return;
}
synchronized (response) { synchronized (response) {
response.setBoolean(responseBoolean); response.setBoolean(responseBoolean);
response.notifyAll(); response.notifyAll();
@ -2603,7 +2694,9 @@ public class HumanPlayer extends PlayerImpl {
@Override @Override
public void setResponseInteger(Integer responseInteger) { public void setResponseInteger(Integer responseInteger) {
waitResponseOpen(); if (!waitResponseOpen()) {
return;
}
synchronized (response) { synchronized (response) {
response.setInteger(responseInteger); response.setInteger(responseInteger);
response.notifyAll(); response.notifyAll();
@ -2613,8 +2706,8 @@ public class HumanPlayer extends PlayerImpl {
@Override @Override
public void abort() { public void abort() {
// abort must cancel any response and stop waiting immediately
abort = true; abort = true;
waitResponseOpen();
synchronized (response) { synchronized (response) {
response.notifyAll(); response.notifyAll();
logger.debug("Got cancel action from player: " + getId()); logger.debug("Got cancel action from player: " + getId());
@ -2623,17 +2716,28 @@ public class HumanPlayer extends PlayerImpl {
@Override @Override
public void signalPlayerConcede() { public void signalPlayerConcede() {
//waitResponseOpen(); //concede is direct event, no need to wait it // waitResponseOpen(); // concede is async event, will be processed on first priority
synchronized (response) { synchronized (response) {
response.setResponseConcedeCheck(); response.setAsyncWantConcede();
response.notifyAll(); response.notifyAll();
logger.debug("Set check concede for waiting player: " + getId()); logger.debug("Set check concede for waiting player: " + getId());
} }
} }
@Override
public void signalPlayerCheat() {
// waitResponseOpen(); // cheat is async event, will be processed on first player's priority
synchronized (response) {
response.setAsyncWantCheat();
response.notifyAll();
logger.debug("Set cheat for waiting player: " + getId());
}
}
@Override @Override
public void skip() { public void skip() {
// waitResponseOpen(); //skip is direct event, no need to wait it // waitResponseOpen(); //skip is direct event, no need to wait it
// TODO: can be bugged and must be reworked, see wantConcede as example?!
synchronized (response) { synchronized (response) {
response.setInteger(0); response.setInteger(0);
response.notifyAll(); response.notifyAll();
@ -2646,20 +2750,6 @@ public class HumanPlayer extends PlayerImpl {
return new HumanPlayer(this); return new HumanPlayer(this);
} }
protected void updateGameStatePriority(String methodName, Game game) {
// call that for every choose cycle before prepareForResponse
// (some choose logic can asks another question with different game state priority)
if (game.getState().getPriorityPlayerId() != null) { // don't do it if priority was set to null before (e.g. discard in cleanaup)
if (getId() == null) {
logger.fatal("Player with no ID: " + name);
this.quit(game);
return;
}
logger.debug("Setting game priority to " + getId() + " [" + methodName + ']');
game.getState().setPriorityPlayerId(getId());
}
}
@Override @Override
public void sendPlayerAction(PlayerAction playerAction, Game game, Object data) { public void sendPlayerAction(PlayerAction playerAction, Game game, Object data) {
switch (playerAction) { switch (playerAction) {

View file

@ -1,23 +1,40 @@
package mage.player.human; package mage.player.human;
import mage.constants.ManaType;
import mage.util.Copyable;
import java.io.Serializable; import java.io.Serializable;
import java.util.UUID; import java.util.UUID;
import mage.constants.ManaType;
/** /**
* Network: server side data for waiting a user's response like new choice
* <p>
* Details:
* - one response object per user;
* - support multiple data types;
* - waiting and writing response on diff threads;
* - start by response.wait (game thread) and end by response.notifyAll (network/call thread)
* - user's request can income in diff order, so only one latest response allowed (except async commands like concede and cheat)
* *
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com, JayDi85
*/ */
public class PlayerResponse implements Serializable { public class PlayerResponse implements Serializable, Copyable<PlayerResponse> {
private String activeAction; // for debug information
private String responseString; private String responseString;
private UUID responseUUID; private UUID responseUUID;
private Boolean responseBoolean; private Boolean responseBoolean;
private Integer responseInteger; private Integer responseInteger;
private ManaType responseManaType; private ManaType responseManaType;
private UUID responseManaTypePlayerId; private UUID responseManaPlayerId;
private Boolean responseConcedeCheck;
// async commands can income any time from network thread as signal,
// must process it in game thread on any priority
// TODO: is concede/hand view confirmation can broken waiting cycle for other choosing/priority player
// (with same response type, with diff response type)???
private Boolean asyncWantConcede;
private Boolean asyncWantCheat;
public PlayerResponse() { public PlayerResponse() {
clear(); clear();
@ -25,96 +42,122 @@ public class PlayerResponse implements Serializable {
@Override @Override
public String toString() { public String toString() {
return ((responseString == null) ? "null" : responseString) return ((this.responseString == null) ? "null" : this.responseString)
+ ',' + responseUUID + "," + this.responseUUID
+ ',' + responseBoolean + "," + this.responseBoolean
+ ',' + responseInteger + "," + this.responseInteger
+ ',' + responseManaType + "," + this.responseManaType
+ ',' + responseManaTypePlayerId + "," + this.responseManaPlayerId
+ ',' + responseConcedeCheck; + "," + this.asyncWantConcede
+ "," + this.asyncWantCheat
+ " (" + (this.activeAction == null ? "sleep" : this.activeAction) + ")";
} }
public PlayerResponse(PlayerResponse other) { protected PlayerResponse(final PlayerResponse response) {
copy(other); this.copyFrom(response);
} }
public void copy(PlayerResponse other) { @Override
responseString = other.responseString; public PlayerResponse copy() {
responseUUID = other.responseUUID; return new PlayerResponse(this);
responseBoolean = other.responseBoolean; }
responseInteger = other.responseInteger;
responseManaType = other.responseManaType; public void copyFrom(final PlayerResponse response) {
responseManaTypePlayerId = other.responseManaTypePlayerId; this.activeAction = response.activeAction;
responseConcedeCheck = other.responseConcedeCheck; this.responseString = response.responseString;
this.responseUUID = response.responseUUID;
this.responseBoolean = response.responseBoolean;
this.responseInteger = response.responseInteger;
this.responseManaType = response.responseManaType;
this.responseManaPlayerId = response.responseManaPlayerId;
this.asyncWantConcede = response.asyncWantConcede;
this.asyncWantCheat = response.asyncWantCheat;
} }
public void clear() { public void clear() {
responseString = null; this.activeAction = null;
responseUUID = null; this.responseString = null;
responseBoolean = null; this.responseUUID = null;
responseInteger = null; this.responseBoolean = null;
responseManaType = null; this.responseInteger = null;
responseManaTypePlayerId = null; this.responseManaType = null;
responseConcedeCheck = null; this.responseManaPlayerId = null;
this.asyncWantConcede = null;
this.asyncWantCheat = null;
}
public String getActiveAction() {
return this.activeAction;
}
public void setActiveAction(String activeAction) {
this.activeAction = activeAction;
} }
public String getString() { public String getString() {
return responseString; return this.responseString;
} }
public void setString(String responseString) { public void setString(String newString) {
this.responseString = responseString; this.responseString = newString;
} }
public UUID getUUID() { public UUID getUUID() {
return responseUUID; return this.responseUUID;
} }
public void setUUID(UUID responseUUID) { public void setUUID(UUID newUUID) {
this.responseUUID = responseUUID; this.responseUUID = newUUID;
} }
public Boolean getBoolean() { public Boolean getBoolean() {
return responseBoolean; return this.responseBoolean;
} }
public void setBoolean(Boolean responseBoolean) { public void setBoolean(Boolean newBoolean) {
this.responseBoolean = responseBoolean; this.responseBoolean = newBoolean;
}
public Boolean getResponseConcedeCheck() {
if (responseConcedeCheck == null) {
return false;
}
return responseConcedeCheck;
}
public void setResponseConcedeCheck() {
this.responseConcedeCheck = true;
} }
public Integer getInteger() { public Integer getInteger() {
return responseInteger; return responseInteger;
} }
public void setInteger(Integer responseInteger) { public void setInteger(Integer newInteger) {
this.responseInteger = responseInteger; this.responseInteger = newInteger;
} }
public ManaType getManaType() { public ManaType getManaType() {
return responseManaType; return this.responseManaType;
} }
public void setManaType(ManaType responseManaType) { public void setManaType(ManaType newManaType) {
this.responseManaType = responseManaType; this.responseManaType = newManaType;
} }
public UUID getResponseManaTypePlayerId() { public UUID getManaPlayerId() {
return responseManaTypePlayerId; return this.responseManaPlayerId;
} }
public void setResponseManaTypePlayerId(UUID responseManaTypePlayerId) { public void setResponseManaPlayerId(UUID newManaPlayerId) {
this.responseManaTypePlayerId = responseManaTypePlayerId; this.responseManaPlayerId = newManaPlayerId;
} }
public Boolean getAsyncWantConcede() {
return this.asyncWantConcede != null && this.asyncWantConcede;
}
public void setAsyncWantConcede() {
this.asyncWantConcede = true;
}
public Boolean getAsyncWantCheat() {
return this.asyncWantCheat != null && this.asyncWantCheat;
}
/**
* Start cheat dialog on next player's priority
*/
public void setAsyncWantCheat() {
this.asyncWantCheat = true;
}
} }

View file

@ -7,7 +7,7 @@ import mage.server.exceptions.UserNotFoundException;
import mage.server.game.GameController; import mage.server.game.GameController;
import mage.server.managers.ChatManager; import mage.server.managers.ChatManager;
import mage.server.managers.ManagerFactory; import mage.server.managers.ManagerFactory;
import mage.server.util.SystemUtil; import mage.utils.SystemUtil;
import mage.view.ChatMessage.MessageColor; import mage.view.ChatMessage.MessageColor;
import mage.view.ChatMessage.MessageType; import mage.view.ChatMessage.MessageType;
import mage.view.ChatMessage.SoundToPlay; import mage.view.ChatMessage.SoundToPlay;

View file

@ -30,7 +30,7 @@ import mage.server.managers.ManagerFactory;
import mage.server.services.impl.FeedbackServiceImpl; import mage.server.services.impl.FeedbackServiceImpl;
import mage.server.tournament.TournamentFactory; import mage.server.tournament.TournamentFactory;
import mage.server.util.ServerMessagesUtil; import mage.server.util.ServerMessagesUtil;
import mage.server.util.SystemUtil; import mage.utils.SystemUtil;
import mage.utils.*; import mage.utils.*;
import mage.view.*; import mage.view.*;
import mage.view.ChatMessage.MessageColor; import mage.view.ChatMessage.MessageColor;
@ -974,36 +974,17 @@ public class MageServerImpl implements MageServer {
} }
@Override @Override
public void cheatMultiple(final UUID gameId, final String sessionId, final UUID playerId, final DeckCardLists deckList) throws MageException { public void cheatShow(final UUID gameId, final String sessionId, final UUID playerId) throws MageException {
execute("cheat", sessionId, () -> { execute("cheatShow", sessionId, () -> {
if (testMode) { if (testMode) {
managerFactory.sessionManager().getSession(sessionId).ifPresent(session -> { managerFactory.sessionManager().getSession(sessionId).ifPresent(session -> {
UUID userId = session.getUserId(); UUID userId = session.getUserId();
managerFactory.gameManager().cheat(gameId, userId, playerId, deckList); managerFactory.gameManager().cheatShow(gameId, userId, playerId);
}); });
} }
}); });
} }
@Override
public boolean cheatOne(final UUID gameId, final String sessionId, final UUID playerId, final String cardName) throws MageException {
return executeWithResult("cheatOne", sessionId, new ActionWithBooleanResult() {
@Override
public Boolean execute() {
if (testMode) {
Optional<Session> session = managerFactory.sessionManager().getSession(sessionId);
if (!session.isPresent()) {
logger.error("Session not found : " + sessionId);
} else {
UUID userId = session.get().getUserId();
return managerFactory.gameManager().cheat(gameId, userId, playerId, cardName);
}
}
return false;
}
});
}
public void handleException(Exception ex) throws MageException { public void handleException(Exception ex) throws MageException {
if (!ex.getMessage().equals("No message")) { if (!ex.getMessage().equals("No message")) {
logger.fatal("", ex); logger.fatal("", ex);

View file

@ -22,6 +22,7 @@ import mage.server.util.*;
import mage.server.util.config.GamePlugin; import mage.server.util.config.GamePlugin;
import mage.server.util.config.Plugin; import mage.server.util.config.Plugin;
import mage.utils.MageVersion; import mage.utils.MageVersion;
import mage.utils.SystemUtil;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.jboss.remoting.*; import org.jboss.remoting.*;
import org.jboss.remoting.callback.InvokerCallbackHandler; import org.jboss.remoting.callback.InvokerCallbackHandler;

View file

@ -9,7 +9,7 @@ import mage.players.net.UserGroup;
import mage.server.game.GamesRoom; import mage.server.game.GamesRoom;
import mage.server.managers.ConfigSettings; import mage.server.managers.ConfigSettings;
import mage.server.managers.ManagerFactory; import mage.server.managers.ManagerFactory;
import mage.server.util.SystemUtil; import mage.utils.SystemUtil;
import mage.util.RandomUtil; import mage.util.RandomUtil;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.jboss.remoting.callback.AsynchInvokerCallbackHandler; import org.jboss.remoting.callback.AsynchInvokerCallbackHandler;

View file

@ -19,7 +19,7 @@ import mage.server.record.UserStatsRepository;
import mage.server.tournament.TournamentController; import mage.server.tournament.TournamentController;
import mage.server.tournament.TournamentSession; import mage.server.tournament.TournamentSession;
import mage.server.util.ServerMessagesUtil; import mage.server.util.ServerMessagesUtil;
import mage.server.util.SystemUtil; import mage.utils.SystemUtil;
import mage.view.TableClientMessage; import mage.view.TableClientMessage;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;

View file

@ -27,7 +27,6 @@ import mage.server.Main;
import mage.server.User; import mage.server.User;
import mage.server.managers.ManagerFactory; import mage.server.managers.ManagerFactory;
import mage.server.util.Splitter; import mage.server.util.Splitter;
import mage.server.util.SystemUtil;
import mage.util.MultiAmountMessage; import mage.util.MultiAmountMessage;
import mage.utils.StreamUtils; import mage.utils.StreamUtils;
import mage.utils.timer.PriorityTimer; import mage.utils.timer.PriorityTimer;
@ -502,6 +501,10 @@ public class GameController implements GameCallback {
} }
public void sendPlayerAction(PlayerAction playerAction, UUID userId, Object data) { public void sendPlayerAction(PlayerAction playerAction, UUID userId, Object data) {
// TODO: critical bug, must be enabled and research/rework due:
// * game change commands must be executed by game thread (example: undo)
// * user change commands can be executed by network thread??? (example: change skip settings)
//SystemUtil.ensureRunInGameThread();
switch (playerAction) { switch (playerAction) {
case UNDO: case UNDO:
game.undo(getPlayerId(userId)); game.undo(getPlayerId(userId));
@ -705,31 +708,10 @@ public class GameController implements GameCallback {
} }
} }
public void cheat(UUID userId, UUID playerId, DeckCardLists deckList) { public void cheatShow(UUID playerId) {
try { Player player = game.getPlayer(playerId);
Deck deck = Deck.load(deckList, false, false); if (player != null) {
game.loadCards(deck.getCards(), playerId); player.signalPlayerCheat();
for (Card card : deck.getCards()) {
card.putOntoBattlefield(game, Zone.OUTSIDE, null, playerId);
}
} catch (GameException ex) {
logger.warn(ex.getMessage());
}
addCardsForTesting(game, playerId);
updateGame();
}
public boolean cheat(UUID userId, UUID playerId, String cardName) {
CardInfo cardInfo = CardRepository.instance.findCard(cardName);
Card card = cardInfo != null ? cardInfo.getCard() : null;
if (card != null) {
Set<Card> cards = new HashSet<>();
cards.add(card);
game.loadCards(cards, playerId);
card.moveToZone(Zone.HAND, null, game, false);
return true;
} else {
return false;
} }
} }
@ -985,13 +967,6 @@ public class GameController implements GameCallback {
return false; return false;
} }
/**
* Adds cards in player's hands that are specified in config/init.txt.
*/
private void addCardsForTesting(Game game, UUID playerId) {
SystemUtil.addCardsForTesting(game, null, game.getPlayer(playerId));
}
/** /**
* Performs a request to a player * Performs a request to a player
* *

View file

@ -146,22 +146,13 @@ public class GameManagerImpl implements GameManager {
} }
@Override @Override
public void cheat(UUID gameId, UUID userId, UUID playerId, DeckCardLists deckList) { public void cheatShow(UUID gameId, UUID userId, UUID playerId) {
GameController gameController = getGameControllerSafe(gameId); GameController gameController = getGameControllerSafe(gameId);
if (gameController != null) { if (gameController != null) {
gameController.cheat(userId, playerId, deckList); gameController.cheatShow(playerId);
} }
} }
@Override
public boolean cheat(UUID gameId, UUID userId, UUID playerId, String cardName) {
GameController gameController = getGameControllerSafe(gameId);
if (gameController != null) {
return gameController.cheat(userId, playerId, cardName);
}
return false;
}
@Override @Override
public void removeGame(UUID gameId) { public void removeGame(UUID gameId) {
GameController gameController = getGameControllerSafe(gameId); GameController gameController = getGameControllerSafe(gameId);

View file

@ -38,9 +38,7 @@ public interface GameManager {
void stopWatching(UUID gameId, UUID userId); void stopWatching(UUID gameId, UUID userId);
void cheat(UUID gameId, UUID userId, UUID playerId, DeckCardLists deckList); void cheatShow(UUID gameId, UUID userId, UUID playerId);
boolean cheat(UUID gameId, UUID userId, UUID playerId, String cardName);
void removeGame(UUID gameId); void removeGame(UUID gameId);

View file

@ -2,15 +2,19 @@ package mage.server.util;
import mage.server.managers.ConfigSettings; import mage.server.managers.ConfigSettings;
import mage.server.managers.ThreadExecutor; import mage.server.managers.ThreadExecutor;
import org.apache.log4j.Logger;
import java.util.concurrent.*; import java.util.concurrent.*;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com, JayDi85
*/ */
public class ThreadExecutorImpl implements ThreadExecutor { public class ThreadExecutorImpl implements ThreadExecutor {
private final ExecutorService callExecutor;
private final ExecutorService gameExecutor; private static final Logger logger = Logger.getLogger(ThreadExecutorImpl.class);
private final ExecutorService callExecutor; // shareable threads to run single task (example: save new game settings from a user, send chat message, etc)
private final ExecutorService gameExecutor; // game threads to run long tasks, one per game (example: run game and wait user's feedback)
private final ScheduledExecutorService timeoutExecutor; private final ScheduledExecutorService timeoutExecutor;
private final ScheduledExecutorService timeoutIdleExecutor; private final ScheduledExecutorService timeoutIdleExecutor;
@ -26,8 +30,10 @@ public class ThreadExecutorImpl implements ThreadExecutor {
*/ */
public ThreadExecutorImpl(ConfigSettings config) { public ThreadExecutorImpl(ConfigSettings config) {
callExecutor = Executors.newCachedThreadPool(); //callExecutor = Executors.newCachedThreadPool();
gameExecutor = Executors.newFixedThreadPool(config.getMaxGameThreads()); callExecutor = new CachedThreadPoolWithException();
//gameExecutor = Executors.newFixedThreadPool(config.getMaxGameThreads());
gameExecutor = new FixedThreadPoolWithException(config.getMaxGameThreads());
timeoutExecutor = Executors.newScheduledThreadPool(4); timeoutExecutor = Executors.newScheduledThreadPool(4);
timeoutIdleExecutor = Executors.newScheduledThreadPool(4); timeoutIdleExecutor = Executors.newScheduledThreadPool(4);
@ -45,6 +51,42 @@ public class ThreadExecutorImpl implements ThreadExecutor {
((ThreadPoolExecutor) timeoutIdleExecutor).setThreadFactory(new XMageThreadFactory("TIMEOUT_IDLE")); ((ThreadPoolExecutor) timeoutIdleExecutor).setThreadFactory(new XMageThreadFactory("TIMEOUT_IDLE"));
} }
static class CachedThreadPoolWithException extends ThreadPoolExecutor {
CachedThreadPoolWithException() {
// use same params as Executors.newCachedThreadPool()
super(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
// catch errors in CALL threads (from client commands)
if (t != null) {
logger.error("Catch unhandled error in CALL thread: " + t.getMessage(), t);
}
}
}
static class FixedThreadPoolWithException extends ThreadPoolExecutor {
FixedThreadPoolWithException(int nThreads) {
// use same params as Executors.newFixedThreadPool()
super(nThreads, nThreads,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
// catch errors in GAME threads (from game processing)
if (t != null) {
// it's impossible to brake game thread in normal use case, so each bad use case must be researched
logger.error("Catch unhandled error in GAME thread: " + t.getMessage(), t);
}
}
}
@Override @Override
public int getActiveThreads(ExecutorService executerService) { public int getActiveThreads(ExecutorService executerService) {
@ -90,5 +132,4 @@ class XMageThreadFactory implements ThreadFactory {
thread.setName(prefix + ' ' + thread.getThreadGroup().getName() + '-' + thread.getId()); thread.setName(prefix + ' ' + thread.getThreadGroup().getName() + '-' + thread.getId());
return thread; return thread;
} }
} }

View file

@ -2931,6 +2931,11 @@ public class TestPlayer implements Player {
computerPlayer.signalPlayerConcede(); computerPlayer.signalPlayerConcede();
} }
@Override
public void signalPlayerCheat() {
computerPlayer.signalPlayerCheat();
}
@Override @Override
public void abortReset() { public void abortReset() {
computerPlayer.abortReset(); computerPlayer.abortReset();

View file

@ -35,7 +35,7 @@ import mage.server.managers.ConfigSettings;
import mage.server.util.ConfigFactory; import mage.server.util.ConfigFactory;
import mage.server.util.ConfigWrapper; import mage.server.util.ConfigWrapper;
import mage.server.util.PluginClassLoader; import mage.server.util.PluginClassLoader;
import mage.server.util.SystemUtil; import mage.utils.SystemUtil;
import mage.server.util.config.GamePlugin; import mage.server.util.config.GamePlugin;
import mage.server.util.config.Plugin; import mage.server.util.config.Plugin;
import mage.target.TargetPermanent; import mage.target.TargetPermanent;

View file

@ -28,7 +28,7 @@ import mage.player.ai.ComputerPlayerMCTS;
import mage.players.ManaPool; import mage.players.ManaPool;
import mage.players.Player; import mage.players.Player;
import mage.server.game.GameSessionPlayer; import mage.server.game.GameSessionPlayer;
import mage.server.util.SystemUtil; import mage.utils.SystemUtil;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.view.GameView; import mage.view.GameView;
import org.junit.Assert; import org.junit.Assert;

View file

@ -1,7 +1,7 @@
package org.mage.test.serverside.cheats; package org.mage.test.serverside.cheats;
import mage.constants.*; import mage.constants.*;
import mage.server.util.SystemUtil; import mage.utils.SystemUtil;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase; import org.mage.test.serverside.base.CardTestPlayerBase;
@ -56,7 +56,7 @@ public class LoadCheatsTest extends CardTestPlayerBase {
execute(); execute();
setChoice(playerA, "5"); // choose [group 3]: 5 = 2 default menus + 3 group setChoice(playerA, "5"); // choose [group 3]: 5 = 2 default menus + 3 group
SystemUtil.addCardsForTesting(currentGame, commandsFile, playerA); SystemUtil.executeCheatCommands(currentGame, commandsFile, playerA);
assertHandCount(playerA, "Razorclaw Bear", 1); assertHandCount(playerA, "Razorclaw Bear", 1);
assertPermanentCount(playerA, "Mountain", 3); assertPermanentCount(playerA, "Mountain", 3);

View file

@ -724,6 +724,11 @@ public class PlayerStub implements Player {
} }
@Override
public void signalPlayerCheat() {
}
@Override @Override
public void abortReset() { public void abortReset() {

View file

@ -2,7 +2,7 @@ package org.mage.test.testapi;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.Zone; import mage.constants.Zone;
import mage.server.util.SystemUtil; import mage.utils.SystemUtil;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase; import org.mage.test.serverside.base.CardTestPlayerBase;

View file

@ -35,7 +35,7 @@ import mage.game.draft.DraftCube;
import mage.game.permanent.token.Token; import mage.game.permanent.token.Token;
import mage.game.permanent.token.TokenImpl; import mage.game.permanent.token.TokenImpl;
import mage.game.permanent.token.custom.CreatureToken; import mage.game.permanent.token.custom.CreatureToken;
import mage.server.util.SystemUtil; import mage.utils.SystemUtil;
import mage.sets.TherosBeyondDeath; import mage.sets.TherosBeyondDeath;
import mage.target.targetpointer.TargetPointer; import mage.target.targetpointer.TargetPointer;
import mage.util.CardUtil; import mage.util.CardUtil;

View file

@ -1,18 +1,21 @@
package mage.cards.decks; package mage.cards.decks;
import mage.util.CardUtil;
import mage.util.Copyable;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
/** /**
* *
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com, JayDi85
*/ */
public class DeckCardLists implements Serializable { public class DeckCardLists implements Serializable, Copyable<DeckCardLists> {
private String name; private String name = null;
private String author; private String author = null;
private List<DeckCardInfo> cards = new ArrayList<>(); private List<DeckCardInfo> cards = new ArrayList<>();
private List<DeckCardInfo> sideboard = new ArrayList<>(); private List<DeckCardInfo> sideboard = new ArrayList<>();
@ -20,6 +23,23 @@ public class DeckCardLists implements Serializable {
private DeckCardLayout cardLayout = null; private DeckCardLayout cardLayout = null;
private DeckCardLayout sideboardLayout = null; private DeckCardLayout sideboardLayout = null;
public DeckCardLists() {
}
protected DeckCardLists(final DeckCardLists deck) {
this.name = deck.name;
this.author = deck.author;
this.cards = CardUtil.deepCopyObject(deck.cards);
this.sideboard = CardUtil.deepCopyObject(deck.sideboard);
this.cardLayout = CardUtil.deepCopyObject(deck.cardLayout);
this.sideboardLayout = CardUtil.deepCopyObject(deck.sideboardLayout);
}
@Override
public DeckCardLists copy() {
return new DeckCardLists(this);
}
/** /**
* @return The layout of the cards * @return The layout of the cards
*/ */

View file

@ -534,7 +534,6 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
// game cheats (for tests only) // game cheats (for tests only)
void cheat(UUID ownerId, Map<Zone, String> commands); void cheat(UUID ownerId, Map<Zone, String> commands);
void cheat(UUID ownerId, List<Card> library, List<Card> hand, List<PermanentCard> battlefield, List<Card> graveyard, List<Card> command); void cheat(UUID ownerId, List<Card> library, List<Card> hand, List<PermanentCard> battlefield, List<Card> graveyard, List<Card> command);
// controlling the behaviour of replacement effects while permanents entering the battlefield // controlling the behaviour of replacement effects while permanents entering the battlefield

View file

@ -794,7 +794,7 @@ public abstract class GameImpl implements Game {
if (!concedingPlayers.contains(playerId)) { if (!concedingPlayers.contains(playerId)) {
logger.debug("Game over for player Id: " + playerId + " gameId " + getId()); logger.debug("Game over for player Id: " + playerId + " gameId " + getId());
concedingPlayers.add(playerId); concedingPlayers.add(playerId);
player.signalPlayerConcede(); player.signalPlayerConcede(); // will be executed on next priority
} }
} else { } else {
// no asynchronous action so check directly // no asynchronous action so check directly
@ -3799,8 +3799,9 @@ public abstract class GameImpl implements Game {
for (Player playerObject : getPlayers().values()) { for (Player playerObject : getPlayers().values()) {
if (playerObject.isHuman() && playerObject.canRespond()) { if (playerObject.isHuman() && playerObject.canRespond()) {
playerObject.resetStoredBookmark(this); playerObject.resetStoredBookmark(this);
playerObject.abort();
playerObject.resetPlayerPassedActions(); playerObject.resetPlayerPassedActions();
playerObject.abort();
} }
} }
fireUpdatePlayersEvent(); fireUpdatePlayersEvent();

View file

@ -77,7 +77,7 @@ public class GameState implements Serializable, Copyable<GameState> {
private Turn turn; private Turn turn;
private TurnMods turnMods; // one time turn modifications (turn, phase or step) private TurnMods turnMods; // one time turn modifications (turn, phase or step)
private UUID activePlayerId; // playerId which turn it is private UUID activePlayerId; // playerId which turn it is
private UUID priorityPlayerId; // player that has currently priority private UUID priorityPlayerId; // player that has currently priority (setup before any choose)
private UUID playerByOrderId; // player that has currently priority private UUID playerByOrderId; // player that has currently priority
private UUID monarchId; // player that is the monarch private UUID monarchId; // player that is the monarch
private UUID initiativeId; // player that has the initiative private UUID initiativeId; // player that has the initiative

View file

@ -550,6 +550,8 @@ public interface Player extends MageItem, Copyable<Player> {
void signalPlayerConcede(); void signalPlayerConcede();
void signalPlayerCheat();
void skip(); void skip();
// priority, undo, ... // priority, undo, ...

View file

@ -5088,7 +5088,10 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public void signalPlayerConcede() { public void signalPlayerConcede() {
}
@Override
public void signalPlayerCheat() {
} }
@Override @Override

View file

@ -1,5 +1,7 @@
package mage.util; package mage.util;
import java.lang.reflect.Method;
/** /**
* Devs only: enable or disable debug features * Devs only: enable or disable debug features
* <p> * <p>
@ -28,4 +30,47 @@ public class DebugUtil {
public static boolean GUI_GAME_DRAW_BATTLEFIELD_BORDER = false; public static boolean GUI_GAME_DRAW_BATTLEFIELD_BORDER = false;
public static boolean GUI_GAME_DRAW_HAND_AND_STACK_BORDER = false; public static boolean GUI_GAME_DRAW_HAND_AND_STACK_BORDER = false;
public static boolean GUI_GAME_DIALOGS_DRAW_CARDS_AREA_BORDER = false; public static boolean GUI_GAME_DIALOGS_DRAW_CARDS_AREA_BORDER = false;
public static String getMethodNameWithSource(final int depth) {
return TraceHelper.getMethodNameWithSource(depth);
}
}
/**
* Debug: allows to find a caller's method name
* <a href="https://stackoverflow.com/a/11726687/1276632">Original code</a>
*/
class TraceHelper {
private static Method m;
static {
try {
m = Throwable.class.getDeclaredMethod("getStackTraceElement", int.class);
m.setAccessible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
public static String getMethodName(final int depth) {
try {
StackTraceElement element = (StackTraceElement) m.invoke(new Throwable(), depth + 1);
return element.getMethodName();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static String getMethodNameWithSource(final int depth) {
try {
StackTraceElement element = (StackTraceElement) m.invoke(new Throwable(), depth + 1);
return String.format("%s - %s:%d", element.getMethodName(), element.getFileName(), element.getLineNumber());
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
} }