server: improved server stability (#11285) and reworked triggers/playable logic (#8426):

* game: now all playable calculations done in game simulation, outside real game (no more freeze and ruined games by wrong Nyxbloom Ancient and other cards with wrong replacement dialog);
* game: fixed multiple problems with triggers (wrong order, duplicated calls or "too many mana" bugs, see #8426, #12087);
* tests: added data integrity checks for game's triggers (3 enabled and 3 disabled due current game engine logic);
This commit is contained in:
Oleg Agafonov 2024-04-16 23:10:04 +04:00
parent f68e435fc4
commit e8e2f23284
23 changed files with 362 additions and 120 deletions

View file

@ -98,8 +98,10 @@ public abstract class GameImpl implements Game {
private transient Object customData; // temporary data, used in AI simulations
private transient Player losingPlayer; // temporary data, used in AI simulations
protected boolean simulation = false;
protected boolean checkPlayableState = false;
protected boolean simulation = false; // for inner simulations (game without user messages)
protected boolean aiGame = false; // for inner simulations (ai game, debug only)
protected boolean checkPlayableState = false; // for inner playable calculations (game without user dialogs)
protected AtomicInteger totalErrorsCount = new AtomicInteger(); // for debug only: error stats
@ -180,6 +182,7 @@ public abstract class GameImpl implements Game {
protected GameImpl(final GameImpl game) {
//this.customData = game.customData; // temporary data, no need on game copy
//this.losingPlayer = game.losingPlayer; // temporary data, no need on game copy
this.aiGame = game.aiGame;
this.simulation = game.simulation;
this.checkPlayableState = game.checkPlayableState;
@ -248,13 +251,19 @@ public abstract class GameImpl implements Game {
}
@Override
public void setSimulation(boolean simulation) {
this.simulation = simulation;
public Game createSimulationForAI() {
Game res = this.copy();
((GameImpl) res).simulation = true;
((GameImpl) res).aiGame = true;
return res;
}
@Override
public void setCheckPlayableState(boolean checkPlayableState) {
this.checkPlayableState = checkPlayableState;
public Game createSimulationForPlayableCalc() {
Game res = this.copy();
((GameImpl) res).simulation = true;
((GameImpl) res).checkPlayableState = true;
return res;
}
@Override
@ -1602,6 +1611,10 @@ public abstract class GameImpl implements Game {
@Override
public void playPriority(UUID activePlayerId, boolean resuming) {
if (!this.isSimulation() && this.inCheckPlayableState()) {
throw new IllegalStateException("Wrong code usage. Only simulation games can be in CheckPlayableState");
}
int priorityErrorsCount = 0;
infiniteLoopCounter = 0;
int rollbackBookmarkOnPriorityStart = 0;
@ -1711,7 +1724,7 @@ public abstract class GameImpl implements Game {
throw new MageException(UNIT_TESTS_ERROR_TEXT);
}
} finally {
setCheckPlayableState(false);
//setCheckPlayableState(false); // TODO: delete
}
state.getPlayerList().getNext();
}
@ -1730,7 +1743,7 @@ public abstract class GameImpl implements Game {
} finally {
resetLKI();
clearAllBookmarks();
setCheckPlayableState(false);
//setCheckPlayableState(false);
}
}
@ -4045,8 +4058,24 @@ public abstract class GameImpl implements Game {
@Override
public String toString() {
Player activePayer = this.getPlayer(this.getActivePlayerId());
// show non-standard game state (not part of the real game, e.g. AI or mana calculation)
List<String> simInfo = new ArrayList<>();
if (this.simulation) {
simInfo.add("SIMULATION");
}
if (this.aiGame) {
simInfo.add("AI");
}
if (this.checkPlayableState) {
simInfo.add("PLAYABLE CALC");
}
if (!ThreadUtils.isRunGameThread()) {
simInfo.add("NOT GAME THREAD");
}
StringBuilder sb = new StringBuilder()
.append(this.isSimulation() ? "!!!SIMULATION!!! " : "")
.append(!simInfo.isEmpty() ? "!!!" + String.join(", ", simInfo) + "!!! " : "")
.append(this.getGameType().toString())
.append("; ").append(CardUtil.getTurnInfo(this))
.append("; active: ").append((activePayer == null ? "none" : activePayer.getName()))