server: fixed wrong cheater detection in some tourney sideboardings (closes #11877)

This commit is contained in:
Oleg Agafonov 2024-06-11 00:30:00 +04:00
parent 72cf60085c
commit e209ce1c97
17 changed files with 146 additions and 55 deletions

View file

@ -254,10 +254,11 @@ public class Deck implements Serializable, Copyable<Deck> {
return new Deck(this);
}
public long getDeckHash() {
public long getDeckHash(boolean ignoreMainBasicLands) {
return DeckUtil.getDeckHash(
this.cards.stream().map(MageObject::getName).collect(Collectors.toList()),
this.sideboard.stream().map(MageObject::getName).collect(Collectors.toList())
this.sideboard.stream().map(MageObject::getName).collect(Collectors.toList()),
ignoreMainBasicLands
);
}
}

View file

@ -11,12 +11,15 @@ import java.util.stream.Collectors;
*/
public abstract class DeckValidator implements Serializable {
protected static final List<String> basicLandNames = Arrays.asList(
public static final HashSet<String> MAIN_BASIC_LAND_NAMES = new HashSet<>(Arrays.asList(
"Plains",
"Island",
"Swamp",
"Mountain",
"Forest",
"Forest"
));
public static final HashSet<String> ADDITIONAL_BASIC_LAND_NAMES = new HashSet<>(Arrays.asList(
"Wastes",
"Snow-Covered Plains",
"Snow-Covered Island",
@ -24,11 +27,19 @@ public abstract class DeckValidator implements Serializable {
"Snow-Covered Mountain",
"Snow-Covered Forest",
"Snow-Covered Wastes"
);
));
public static final HashSet<String> ALL_BASIC_LAND_NAMES = new HashSet<>();
{
ALL_BASIC_LAND_NAMES.addAll(MAIN_BASIC_LAND_NAMES);
ALL_BASIC_LAND_NAMES.addAll(ADDITIONAL_BASIC_LAND_NAMES);
}
protected static final Map<String, Integer> maxCopiesMap = new HashMap<>();
static {
basicLandNames.stream().forEach(s -> maxCopiesMap.put(s, Integer.MAX_VALUE));
MAIN_BASIC_LAND_NAMES.forEach(s -> maxCopiesMap.put(s, Integer.MAX_VALUE));
ADDITIONAL_BASIC_LAND_NAMES.forEach(s -> maxCopiesMap.put(s, Integer.MAX_VALUE));
maxCopiesMap.put("Relentless Rats", Integer.MAX_VALUE);
maxCopiesMap.put("Shadowborn Apostle", Integer.MAX_VALUE);
maxCopiesMap.put("Rat Colony", Integer.MAX_VALUE);

View file

@ -41,7 +41,7 @@ public interface Match {
void submitDeck(UUID playerId, Deck deck);
void updateDeck(UUID playerId, Deck deck);
void updateDeck(UUID playerId, Deck deck, boolean ignoreMainBasicLands);
void startMatch();

View file

@ -389,14 +389,14 @@ public abstract class MatchImpl implements Match {
}
@Override
public void updateDeck(UUID playerId, Deck deck) {
public void updateDeck(UUID playerId, Deck deck, boolean ignoreMainBasicLands) {
// used for auto-save deck
MatchPlayer player = getPlayer(playerId);
if (player == null) {
return;
}
player.updateDeck(deck);
player.updateDeck(deck, ignoreMainBasicLands);
}
protected String createGameStartMessage() {

View file

@ -97,7 +97,7 @@ public class MatchPlayer implements Serializable {
this.doneSideboarding = true;
}
public boolean updateDeck(Deck newDeck) {
public boolean updateDeck(Deck newDeck, boolean ignoreMainBasicLands) {
// used for auto-save by timeout from client side
// workaround to keep deck name for Tiny Leaders because it must be hidden for players
@ -106,7 +106,7 @@ public class MatchPlayer implements Serializable {
}
// make sure it's the same deck (player do not add or remove something)
boolean isGood = (this.deck.getDeckHash() == newDeck.getDeckHash());
boolean isGood = (this.deck.getDeckHash(ignoreMainBasicLands) == newDeck.getDeckHash(ignoreMainBasicLands));
if (!isGood) {
logger.error("Found cheating player " + player.getName()
+ " with changed deck, main " + newDeck.getCards().size() + ", side " + newDeck.getSideboard().size());

View file

@ -49,7 +49,7 @@ public interface Tournament {
void submitDeck(UUID playerId, Deck deck);
void updateDeck(UUID playerId, Deck deck);
void updateDeck(UUID playerId, Deck deck, boolean ignoreMainBasicLands);
void autoSubmit(UUID playerId, Deck deck);

View file

@ -139,13 +139,13 @@ public abstract class TournamentImpl implements Tournament {
}
@Override
public void updateDeck(UUID playerId, Deck deck) {
public void updateDeck(UUID playerId, Deck deck, boolean ignoreMainBasicLands) {
TournamentPlayer player = players.getOrDefault(playerId, null);
if (player == null) {
return;
}
player.updateDeck(deck);
player.updateDeck(deck, ignoreMainBasicLands);
}
protected Round createRoundRandom() {

View file

@ -92,11 +92,11 @@ public class TournamentPlayer {
this.setState(TournamentPlayerState.WAITING);
}
public boolean updateDeck(Deck deck) {
public boolean updateDeck(Deck deck, boolean ignoreMainBasicLands) {
// used for auto-save deck
// make sure it's the same deck (player do not add or remove something)
boolean isGood = (this.getDeck().getDeckHash() == deck.getDeckHash());
boolean isGood = (this.getDeck().getDeckHash(ignoreMainBasicLands) == deck.getDeckHash(ignoreMainBasicLands));
if (!isGood) {
logger.error("Found cheating tourney player " + player.getName()
+ " with changed deck, main " + deck.getCards().size() + ", side " + deck.getSideboard().size());

View file

@ -1,7 +1,6 @@
package mage.util;
import mage.cards.decks.Deck;
import mage.players.Player;
import mage.cards.decks.DeckValidator;
import org.apache.log4j.Logger;
import java.io.BufferedWriter;
@ -22,10 +21,22 @@ public final class DeckUtil {
/**
* Find deck hash (main + sideboard)
* It must be same after sideboard (do not depend on main/sb structure)
*
* @param ignoreMainBasicLands - drafts allow to use any basic lands, so ignore it for hash calculation
*/
public static long getDeckHash(List<String> mainCards, List<String> sideboardCards) {
public static long getDeckHash(List<String> mainCards, List<String> sideboardCards, boolean ignoreMainBasicLands) {
List<String> all = new ArrayList<>(mainCards);
all.addAll(sideboardCards);
// related rules:
// 7.2 Card Use in Limited Tournaments
// Players may add an unlimited number of cards named Plains, Island, Swamp, Mountain, or Forest to their
// deck and sideboard. They may not add additional snow basic land cards (e.g., Snow-Covered Forest, etc)
// or Wastes basic land cards, even in formats in which they are legal.
if (ignoreMainBasicLands) {
all.removeIf(DeckValidator.MAIN_BASIC_LAND_NAMES::contains);
}
Collections.sort(all);
return getStringHash(all.toString());
}
@ -58,27 +69,4 @@ public final class DeckUtil {
}
return null;
}
/**
* make sure it's the same deck (player do not add or remove something)
*
* @param newDeck will be clear on cheating
*/
public boolean checkDeckForModification(String checkInfo, Player player, Deck oldDeck, Deck newDeck) {
boolean isGood = (oldDeck.getDeckHash() == newDeck.getDeckHash());
if (!isGood) {
String message = String.format("Found cheating player [%s] in [%s] with changed deck, main %d -> %d, side %d -> %d",
player.getName(),
checkInfo,
oldDeck.getCards().size(),
newDeck.getCards().size(),
oldDeck.getSideboard().size(),
newDeck.getSideboard().size()
);
logger.error(message);
newDeck.getCards().clear();
newDeck.getSideboard().clear();
}
return isGood;
}
}