deck improves:

* gui: removed public deck hash info;
* gui: improved xmage dck-file - now it correctly load a card's amount (related to files from third party services);
* server: fixed wrong cheating warning on deck construction (closes #11877);
* refactor: removed outdated hash code and calculations;
* other: added docs, added multiple deck hash tests;
This commit is contained in:
Oleg Agafonov 2024-04-10 22:18:07 +04:00
parent 889c1125e8
commit 7817a5cac6
32 changed files with 551 additions and 247 deletions

View file

@ -236,35 +236,51 @@ public class TableController {
return true;
}
/**
* Check and join real player
*/
public synchronized boolean joinTable(UUID userId, String name, PlayerType playerType, int skill, DeckCardLists deckList, String password) throws MageException {
// user - must exist
Optional<User> _user = managerFactory.userManager().getUser(userId);
if (!_user.isPresent()) {
logger.error("Join Table: can't find user to join " + name + " Id = " + userId);
return false;
}
// table - already joined
User user = _user.get();
if (userPlayerMap.containsKey(userId) && playerType == PlayerType.HUMAN) {
user.showUserMessage("Join Table", new StringBuilder("You can join a table only one time.").toString());
return false;
}
// table - already started
if (table.getState() != TableState.WAITING) {
user.showUserMessage("Join Table", "No available seats.");
return false;
}
// check password
// table - wrong password
if (!table.getMatch().getOptions().getPassword().isEmpty() && playerType == PlayerType.HUMAN) {
if (!table.getMatch().getOptions().getPassword().equals(password)) {
user.showUserMessage("Join Table", "Wrong password.");
return false;
}
}
// table - no more free seats
Seat seat = table.getNextAvailableSeat(playerType);
if (seat == null) {
user.showUserMessage("Join Table", "No available seats.");
return false;
}
// deck - try to load real deck (any unknown cards will raise exception error here)
// P.S. Quick start button can raise error here on memory problems, but it's ok
// (real table creating is unaffected and will not create empty table)
Deck deck = Deck.load(deckList, false, false);
// deck - validate
if (!Main.isTestMode() && !table.getValidator().validate(deck)) {
StringBuilder sb = new StringBuilder("You (").append(name).append(") have an invalid deck for the selected ").append(table.getValidator().getName()).append(" Format. \n\n");
List<DeckValidatorError> errorsList = table.getValidator().getErrorsListSorted();
@ -273,13 +289,16 @@ public class TableController {
});
sb.append("\n\nSelect a deck that is appropriate for the selected format and try again!");
user.showUserMessage("Join Table", sb.toString());
// owner must create table with valid deck only
if (isOwner(userId)) {
logger.debug("New table removed because owner submitted invalid deck tableId " + table.getId());
logger.error("New table removed because owner submitted invalid deck tableId " + table.getId());
managerFactory.tableManager().removeTable(table.getId());
}
return false;
}
// Check quit ratio.
// user - restrict by quit ratio
int quitRatio = table.getMatch().getOptions().getQuitRatio();
if (quitRatio < user.getMatchQuitRatio()) {
String message = new StringBuilder("Your quit ratio ").append(user.getMatchQuitRatio())
@ -288,7 +307,7 @@ public class TableController {
return false;
}
// Check minimum rating.
// user - restrict by rating
int minimumRating = table.getMatch().getOptions().getMinimumRating();
int userRating;
if (table.getMatch().getOptions().isLimited()) {
@ -303,7 +322,7 @@ public class TableController {
return false;
}
// Check power level for table (currently only used for EDH/Commander table)
// user - restrict by deck power level and cards colors (see edh power level for details)
int edhPowerLevel = table.getMatch().getOptions().getEdhPowerLevel();
if (edhPowerLevel > 0 && table.getValidator().getName().toLowerCase(Locale.ENGLISH).equals("commander")) {
int deckEdhPowerLevel = table.getValidator().getEdhPowerLevel(deck);
@ -349,6 +368,7 @@ public class TableController {
}
}
// player - try to create (human, ai, etc)
Optional<Player> playerOpt = createPlayer(name, seat.getPlayerType(), skill);
if (!playerOpt.isPresent()) {
String message = "Could not create player " + name + " of type " + seat.getPlayerType();
@ -356,14 +376,18 @@ public class TableController {
user.showUserMessage("Join Table", message);
return false;
}
// player - restrict by player type, e.g. draft bot can join to tourney only
Player player = playerOpt.get();
if (!player.canJoinTable(table)) {
user.showUserMessage("Join Table", "A " + seat.getPlayerType() + " player can't join this table.");
return false;
}
// all fine, player can be added
match.addPlayer(player, deck);
table.joinTable(player, seat);
logger.trace(player.getName() + " joined tableId: " + table.getId());
//only inform human players and add them to sessionPlayerMap
if (seat.getPlayer().isHuman()) {
seat.getPlayer().setUserData(user.getUserData());
@ -373,6 +397,7 @@ public class TableController {
user.ccJoinedTable(table.getRoomId(), table.getId(), false);
userPlayerMap.put(userId, player.getId());
}
return true;
}
@ -391,8 +416,22 @@ public class TableController {
}
}
/**
* Submit deck on sideboarding/construction (final deck)
*/
public synchronized boolean submitDeck(UUID userId, DeckCardLists deckList) throws MageException {
UUID playerId = userPlayerMap.get(userId);
// player - update tourney's player status
// TODO: need research, why it here instead real time check?
if (table.isTournamentSubTable()) {
TournamentPlayer tournamentPlayer = table.getTournament().getPlayer(playerId);
if (tournamentPlayer != null) {
tournamentPlayer.setStateInfo(""); // reset sideboarding state
}
}
// player - already quit
if (table.isTournament()) {
TournamentPlayer player = tournament.getPlayer(playerId);
if (player == null || player.hasQuit()) {
@ -403,23 +442,26 @@ public class TableController {
if (mPlayer == null || mPlayer.hasQuit()) {
return true; // so the construct panel closes after submit
}
if (table.isTournamentSubTable()) {
TournamentPlayer tournamentPlayer = table.getTournament().getPlayer(mPlayer.getPlayer().getId());
if (tournamentPlayer != null) {
tournamentPlayer.setStateInfo(""); // reset sideboarding state
}
}
}
if (table.getState() != TableState.SIDEBOARDING && table.getState() != TableState.CONSTRUCTING) {
// tourney - too late for submit
if (table.getState() != TableState.SIDEBOARDING
&& table.getState() != TableState.CONSTRUCTING) {
return false;
}
// deck - try to load real deck (any unknown cards will raise exception error here)
Deck deck = Deck.load(deckList, false, false);
// workaround to keep deck name for Tiny Leaders because it must be hidden for players
if (table.getState() == TableState.SIDEBOARDING && table.getMatch() != null) {
MatchPlayer mPlayer = table.getMatch().getPlayer(playerId);
if (mPlayer != null) {
deck.setName(mPlayer.getDeck().getName());
}
}
// deck - validate
if (!Main.isTestMode() && !table.getValidator().validate(deck)) {
Optional<User> _user = managerFactory.userManager().getUser(userId);
if (!_user.isPresent()) {
@ -434,21 +476,20 @@ public class TableController {
_user.get().showUserMessage("Submit deck", sb.toString());
return false;
}
submitDeck(userId, playerId, deck);
return true;
}
public void updateDeck(UUID userId, DeckCardLists deckList) throws MageException {
boolean validDeck;
UUID playerId = userPlayerMap.get(userId);
if (table.getState() != TableState.SIDEBOARDING && table.getState() != TableState.CONSTRUCTING) {
// TODO: need refactor, many duplicated code like state check
if (table.getState() != TableState.SIDEBOARDING
&& table.getState() != TableState.CONSTRUCTING) {
return;
}
Deck deck = Deck.load(deckList, false, false);
validDeck = updateDeck(userId, playerId, deck);
if (!validDeck && getTableState() == TableState.SIDEBOARDING) {
logger.warn(" userId: " + userId + " - Modified deck card list!");
}
updateDeck(userId, playerId, deck);
}
private void submitDeck(UUID userId, UUID playerId, Deck deck) {
@ -461,20 +502,21 @@ public class TableController {
}
}
private boolean updateDeck(UUID userId, UUID playerId, Deck deck) {
boolean validDeck = true;
private void updateDeck(UUID userId, UUID playerId, Deck deck) {
if (table.isTournament()) {
if (tournament != null) {
validDeck = managerFactory.tournamentManager().updateDeck(tournament.getId(), playerId, deck);
// TODO: is it possible to update from direct call command in game?!
managerFactory.tournamentManager().updateDeck(tournament.getId(), playerId, deck);
} else {
logger.fatal("Tournament == null table: " + table.getId() + " userId: " + userId);
}
} else if (TableState.SIDEBOARDING == table.getState()) {
validDeck = match.updateDeck(playerId, deck);
} else if (table.getState() == TableState.SIDEBOARDING) {
match.updateDeck(playerId, deck);
} else {
// deck was meanwhile submitted so the autoupdate can be ignored
// TODO: need research
logger.warn("wtf, why it submitting?!");
}
return validDeck;
}
public boolean watchTable(UUID userId) {
@ -538,6 +580,7 @@ public class TableController {
} else {
UUID playerId = userPlayerMap.get(userId);
if (playerId != null) {
// TODO: wtf, need research and document all that checks, can be never used code
if (table.getState() == TableState.WAITING || table.getState() == TableState.READY_TO_START) {
table.leaveNotStartedTable(playerId);
if (table.isTournament()) {
@ -884,7 +927,7 @@ public class TableController {
private void autoSideboard() {
for (MatchPlayer player : match.getPlayers()) {
if (!player.isDoneSideboarding()) {
match.submitDeck(player.getPlayer().getId(), player.generateDeck(table.getValidator()));
match.submitDeck(player.getPlayer().getId(), player.autoCompleteDeck(table.getValidator()));
}
}
}

View file

@ -22,7 +22,7 @@ public interface TournamentManager {
void submitDeck(UUID tournamentId, UUID playerId, Deck deck);
boolean updateDeck(UUID tournamentId, UUID playerId, Deck deck);
void updateDeck(UUID tournamentId, UUID playerId, Deck deck);
TournamentView getTournamentView(UUID tournamentId);

View file

@ -324,11 +324,13 @@ public class TournamentController {
}
}
public boolean updateDeck(UUID playerId, Deck deck) {
if (tournamentSessions.containsKey(playerId)) {
return tournamentSessions.get(playerId).updateDeck(deck);
public void updateDeck(UUID playerId, Deck deck) {
TournamentSession session = tournamentSessions.getOrDefault(playerId, null);
if (session == null) {
return;
}
return false;
session.updateDeck(deck);
}
public void timeout(UUID userId) {

View file

@ -17,6 +17,7 @@ import java.util.concurrent.ConcurrentMap;
*/
public class TournamentManagerImpl implements TournamentManager {
// TODO: must check all usage of controllers.get and insert null check and error log
private final ManagerFactory managerFactory;
private final ConcurrentMap<UUID, TournamentController> controllers = new ConcurrentHashMap<>();
@ -61,8 +62,8 @@ public class TournamentManagerImpl implements TournamentManager {
}
@Override
public boolean updateDeck(UUID tournamentId, UUID playerId, Deck deck) {
return controllers.get(tournamentId).updateDeck(playerId, deck);
public void updateDeck(UUID tournamentId, UUID playerId, Deck deck) {
controllers.get(tournamentId).updateDeck(playerId, deck);
}
@Override

View file

@ -83,8 +83,8 @@ public class TournamentSession {
tournament.submitDeck(playerId, deck);
}
public boolean updateDeck(Deck deck) {
return tournament.updateDeck(playerId, deck);
public void updateDeck(Deck deck) {
tournament.updateDeck(playerId, deck);
}
public void setKilled() {