[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_zoom(resolved.params.zoom);
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);
}
@ -412,6 +425,15 @@ void DeckViewContainer::deckSelectFinished(const Response &r)
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()
{
sendReadyStartCommand(!readyStartButton->getState());