[Client] Tell the user when their playmat art is blocked (#7533)

* [Client] Tell the user when their playmat art is blocked

A server DENY rule silently drops banned playmat art: SetPlaymat fails
with no handler and DeckSelect strips the mat while answering RespOk.

Surface both cases with the same critical message box the banner card
art flow uses. DeckSelect strips are detected by comparing the
requested mat, passed via PendingCommand extraData, against the
server-echoed deck. Notices dedupe per card and reset on success.

* [Client] Gate playmat blocked notice on RespFunctionNotAllowed per review

Only SetPlaymat with RespFunctionNotAllowed signals a card art DENY
rule. Older servers answer RespInvalidCommand and strip the mat from
DeckSelect, so drop the DeckSelect comparison to avoid false
blacklisted popups on pre-playmat servers.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: BruebachL <bruebachl@users.noreply.github.com>
This commit is contained in:
BruebachL 2026-10-16 19:50:57 +02:00 • committed by GitHub
parent b11b096828
commit 711386574d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 27 additions and 3 deletions

View file

@ -354,6 +354,19 @@ void DeckViewContainer::resolveAndSendPlaymat()
pp->set_vertical_offset(resolved.params.verticalOffset); pp->set_vertical_offset(resolved.params.verticalOffset);
pp->set_zoom(resolved.params.zoom); pp->set_zoom(resolved.params.zoom);
PendingCommand *playmatPend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(playmatCmd); PendingCommand *playmatPend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(playmatCmd);
const QString sentName = resolved.card.name;
connect(playmatPend, &PendingCommand::finished, this, [this, sentName](const Response &r) {
// Only RespFunctionNotAllowed signals a card art DENY rule. Older servers that predate
// playmats answer RespInvalidCommand and strip the mat from DeckSelect, which must stay silent.
if (r.response_code() == Response::RespFunctionNotAllowed) {
if (!sentName.isEmpty()) {
notifyPlaymatBlacklisted(
sentName, tr("The selected playmat card is blacklisted on this server, so it was not applied."));
}
} else if (r.response_code() == Response::RespOk && !sentName.isEmpty()) {
lastBlacklistedPlaymatNotice.clear();
}
});
parentGame->getGame()->getGameEventHandler()->sendGameCommand(playmatPend, playerId); parentGame->getGame()->getGameEventHandler()->sendGameCommand(playmatPend, playerId);
} }
@ -412,6 +425,15 @@ void DeckViewContainer::deckSelectFinished(const Response &r)
switchToDeckLoadedView(); switchToDeckLoadedView();
} }
void DeckViewContainer::notifyPlaymatBlacklisted(const QString &cardName, const QString &message)
{
if (cardName.isEmpty() || cardName == lastBlacklistedPlaymatNotice) {
return;
}
lastBlacklistedPlaymatNotice = cardName;
QMessageBox::critical(this, tr("Error"), message);
}
void DeckViewContainer::readyStart() void DeckViewContainer::readyStart()
{ {
sendReadyStartCommand(!readyStartButton->getState()); sendReadyStartCommand(!readyStartButton->getState());

View file

@ -60,12 +60,14 @@ private:
VisualDeckStorageWidget *visualDeckStorageWidget; VisualDeckStorageWidget *visualDeckStorageWidget;
TabGame *parentGame; TabGame *parentGame;
int playerId; int playerId;
int playmatRotationIndex = 0; ///< Per-match cursor for round-robin playmat mode. int playmatRotationIndex = 0; ///< Per-match cursor for round-robin playmat mode.
DeckList currentDeck; ///< Cached deck for live settings re-resolution. DeckList currentDeck; ///< Cached deck for live settings re-resolution.
PlaymatInfo lastResolvedPlaymat; ///< Tracks last sent playmat to avoid repeats in random mode. PlaymatInfo lastResolvedPlaymat; ///< Tracks last sent playmat to avoid repeats in random mode.
QString lastBlacklistedPlaymatNotice; ///< Card name already reported as blacklisted, to avoid repeat popups.
void tryCreateVisualDeckStorageWidget(); void tryCreateVisualDeckStorageWidget();
void sendReadyStartCommand(bool ready); void sendReadyStartCommand(bool ready);
void notifyPlaymatBlacklisted(const QString &cardName, const QString &message);
private slots: private slots:
void switchToDeckSelectView(); void switchToDeckSelectView();
void switchToDeckLoadedView(); void switchToDeckLoadedView();