From 7c134efd29e3384535a2e28e62bb211e8c5695bc Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 16 Aug 2026 19:13:03 +0200 Subject: [PATCH 1/5] [Server] Consolidate Server_Game construction parameters into a GameConfig struct (#7128) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lukas Brübach --- .../network/server/remote/CMakeLists.txt | 1 + .../network/server/remote/game/game_config.h | 26 ++++++++++++++ .../server/remote/game/server_game.cpp | 35 ++++++------------- .../network/server/remote/game/server_game.h | 17 ++------- .../server/remote/server_protocolhandler.cpp | 21 ++++++++--- .../movecard_tests/reverse_card_move_test.cpp | 17 ++++++++- 6 files changed, 72 insertions(+), 45 deletions(-) create mode 100644 libcockatrice_network/libcockatrice/network/server/remote/game/game_config.h diff --git a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt index 9fb63c221..80a80e1ae 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt @@ -10,6 +10,7 @@ set(HEADERS game/server_card.h game/server_cardzone.h game/server_counter.h + game/game_config.h game/server_game.h game/server_player.h game/server_spectator.h diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/game_config.h b/libcockatrice_network/libcockatrice/network/server/remote/game/game_config.h new file mode 100644 index 000000000..baf946632 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/game_config.h @@ -0,0 +1,26 @@ +#ifndef GAME_CONFIG_H +#define GAME_CONFIG_H + +#include +#include +#include + +struct GameConfig +{ + ServerInfo_User creatorInfo; + int gameId = -1; + QString description; + QString password; + int maxPlayers = 2; + QList gameTypes; + bool onlyBuddies = false; + bool onlyRegistered = false; + bool spectatorsAllowed = false; + bool spectatorsNeedPassword = false; + bool spectatorsCanTalk = true; + bool spectatorsSeeEverything = true; + int startingLifeTotal = 20; + bool shareDecklistsOnLoad = true; +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp index b9e548653..60d11ead1 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -53,34 +53,19 @@ #include #include -Server_Game::Server_Game(const ServerInfo_User &_creatorInfo, - int _gameId, - const QString &_description, - const QString &_password, - int _maxPlayers, - const QList &_gameTypes, - bool _onlyBuddies, - bool _onlyRegistered, - bool _spectatorsAllowed, - bool _spectatorsNeedPassword, - bool _spectatorsCanTalk, - bool _spectatorsSeeEverything, - int _startingLifeTotal, - bool _shareDecklistsOnLoad, - Server_Room *_room) - : QObject(), room(_room), nextPlayerId(0), hostId(0), creatorInfo(new ServerInfo_User(_creatorInfo)), - gameStarted(false), gameClosed(false), gameId(_gameId), password(_password), maxPlayers(_maxPlayers), - gameTypes(_gameTypes), activePlayer(-1), activePhase(-1), onlyBuddies(_onlyBuddies), - onlyRegistered(_onlyRegistered), spectatorsAllowed(_spectatorsAllowed), - spectatorsNeedPassword(_spectatorsNeedPassword), spectatorsCanTalk(_spectatorsCanTalk), - spectatorsSeeEverything(_spectatorsSeeEverything), startingLifeTotal(_startingLifeTotal), - shareDecklistsOnLoad(_shareDecklistsOnLoad), inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0), - firstGameStarted(false), turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr), - gameMutex() +Server_Game::Server_Game(const GameConfig &config, Server_Room *_room) + : QObject(), room(_room), nextPlayerId(0), hostId(0), creatorInfo(new ServerInfo_User(config.creatorInfo)), + gameStarted(false), gameClosed(false), gameId(config.gameId), description(config.description.simplified()), + password(config.password), maxPlayers(config.maxPlayers), gameTypes(config.gameTypes), activePlayer(-1), + activePhase(-1), onlyBuddies(config.onlyBuddies), onlyRegistered(config.onlyRegistered), + spectatorsAllowed(config.spectatorsAllowed), spectatorsNeedPassword(config.spectatorsNeedPassword), + spectatorsCanTalk(config.spectatorsCanTalk), spectatorsSeeEverything(config.spectatorsSeeEverything), + startingLifeTotal(config.startingLifeTotal), shareDecklistsOnLoad(config.shareDecklistsOnLoad), + inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0), firstGameStarted(false), + turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr), gameMutex() { currentReplay = new GameReplay; currentReplay->set_replay_id(room->getServer()->getDatabaseInterface()->getNextReplayId()); - description = _description.simplified(); connect(this, &Server_Game::sigStartGameIfReady, this, &Server_Game::doStartGameIfReady, Qt::QueuedConnection); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h index e0e7896b7..60b5398f2 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h @@ -21,6 +21,7 @@ #define SERVERGAME_H #include "../server_response_containers.h" +#include "game_config.h" #include #include @@ -92,21 +93,7 @@ private slots: public: mutable QRecursiveMutex gameMutex; - Server_Game(const ServerInfo_User &_creatorInfo, - int _gameId, - const QString &_description, - const QString &_password, - int _maxPlayers, - const QList &_gameTypes, - bool _onlyBuddies, - bool _onlyRegistered, - bool _spectatorsAllowed, - bool _spectatorsNeedPassword, - bool _spectatorsCanTalk, - bool _spectatorsSeeEverything, - int _startingLifeTotal, - bool _shareDecklistsOnLoad, - Server_Room *parent); + Server_Game(const GameConfig &config, Server_Room *parent); ~Server_Game() override; Server_Room *getRoom() const { diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index ba6ac4691..561115084 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -1,5 +1,6 @@ #include "server_protocolhandler.h" +#include "game/game_config.h" #include "game/server_game.h" #include "game/server_player.h" #include "server_database_interface.h" @@ -920,10 +921,22 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room // When server doesn't permit registered users to exist, do not honor only-reg setting bool onlyRegisteredUsers = cmd.only_registered() && (server->permitUnregisteredUsers()); - auto *game = new Server_Game(copyUserInfo(false), gameId, description, QString::fromStdString(cmd.password()), - cmd.max_players(), gameTypes, cmd.only_buddies(), onlyRegisteredUsers, - cmd.spectators_allowed(), cmd.spectators_need_password(), cmd.spectators_can_talk(), - cmd.spectators_see_everything(), startingLifeTotal, shareDecklistsOnLoad, room); + GameConfig config{.creatorInfo = copyUserInfo(false), + .gameId = gameId, + .description = description, + .password = QString::fromStdString(cmd.password()), + .maxPlayers = static_cast(cmd.max_players()), + .gameTypes = gameTypes, + .onlyBuddies = cmd.only_buddies(), + .onlyRegistered = onlyRegisteredUsers, + .spectatorsAllowed = cmd.spectators_allowed(), + .spectatorsNeedPassword = cmd.spectators_need_password(), + .spectatorsCanTalk = cmd.spectators_can_talk(), + .spectatorsSeeEverything = cmd.spectators_see_everything(), + .startingLifeTotal = startingLifeTotal, + .shareDecklistsOnLoad = shareDecklistsOnLoad}; + + auto *game = new Server_Game(config, room); game->addPlayer(this, rc, asSpectator, asJudge, false); room->addGame(game); diff --git a/tests/movecard_tests/reverse_card_move_test.cpp b/tests/movecard_tests/reverse_card_move_test.cpp index 2231a7e3b..64c93078c 100644 --- a/tests/movecard_tests/reverse_card_move_test.cpp +++ b/tests/movecard_tests/reverse_card_move_test.cpp @@ -1,3 +1,4 @@ +#include "game/game_config.h" #include "game/server_abstract_player.h" #include "game/server_card.h" #include "game/server_cardzone.h" @@ -22,7 +23,21 @@ TEST(ReverseCardMoveTest, MoveCardFromBottomTest) // instantiate a fake server instance FakeServer server; Server_Room room(0, 0, "", "", "", "", false, "", {}, &server); - Server_Game game(user, 1, "", "", 2, QList(), false, false, false, false, false, false, 20, false, &room); + GameConfig config{.creatorInfo = user, + .gameId = 1, + .description = QString(), + .password = QString(), + .maxPlayers = 2, + .gameTypes = QList(), + .onlyBuddies = false, + .onlyRegistered = false, + .spectatorsAllowed = false, + .spectatorsNeedPassword = false, + .spectatorsCanTalk = false, + .spectatorsSeeEverything = false, + .startingLifeTotal = 20, + .shareDecklistsOnLoad = false}; + Server_Game game(config, &room); Server_AbstractPlayer player(&game, 1, user, false, nullptr); Server_CardZone deckZone(&player, ZoneNames::DECK, true, ServerInfo_Zone::PublicZone); Server_CardZone exileZone(&player, ZoneNames::EXILE, true, ServerInfo_Zone::PublicZone); From d36865518e36acaedbc8ed971c89a46554a63e2f Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 16 Aug 2026 22:14:49 +0200 Subject: [PATCH 2/5] [Game] Extract makeGameJoinLink helper for cockatrice://joingame links (#7133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inline URL building in GameSelector's copy-link action moves into a shared helper so every invite/copy site produces the same link format. The helper embeds the game description as an extra "game" query item (percent-encoded); links without it stay valid — the receiving parser ignores unknown query items. Co-authored-by: Lukas Brübach --- cockatrice/CMakeLists.txt | 1 + .../interface/widgets/server/game_link.cpp | 23 +++++++++++ .../src/interface/widgets/server/game_link.h | 41 +++++++++++++++++++ .../widgets/server/game_selector.cpp | 16 ++------ 4 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 cockatrice/src/interface/widgets/server/game_link.cpp create mode 100644 cockatrice/src/interface/widgets/server/game_link.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 6fd683461..9ea463eef 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -246,6 +246,7 @@ set(cockatrice_SOURCES src/interface/widgets/replay/replay_widget.cpp src/interface/widgets/server/chat_view/chat_view.cpp src/interface/widgets/server/game_filter_configs.cpp + src/interface/widgets/server/game_link.cpp src/interface/widgets/server/game_selector.cpp src/interface/widgets/server/game_selector_quick_filter_toolbar.cpp src/interface/widgets/server/games_model.cpp diff --git a/cockatrice/src/interface/widgets/server/game_link.cpp b/cockatrice/src/interface/widgets/server/game_link.cpp new file mode 100644 index 000000000..c866f6571 --- /dev/null +++ b/cockatrice/src/interface/widgets/server/game_link.cpp @@ -0,0 +1,23 @@ +#include "game_link.h" + +#include +#include + +QString makeGameJoinLink(const QString &hostname, int port, int roomId, int gameId, const QString &description) +{ + QUrl url; + url.setScheme("cockatrice"); + url.setHost("joingame"); + QUrlQuery query; + query.addQueryItem("hostname", hostname); + query.addQueryItem("port", QString::number(port)); + query.addQueryItem("roomid", QString::number(roomId)); + query.addQueryItem("gameid", QString::number(gameId)); + if (!description.isEmpty()) { + // addQueryItem percent-encodes, so arbitrary descriptions (quotes, + // ampersands, non-ASCII…) survive the trip through chat. + query.addQueryItem("game", description); + } + url.setQuery(query); + return url.toString(QUrl::FullyEncoded); +} diff --git a/cockatrice/src/interface/widgets/server/game_link.h b/cockatrice/src/interface/widgets/server/game_link.h new file mode 100644 index 000000000..d57bbd6d2 --- /dev/null +++ b/cockatrice/src/interface/widgets/server/game_link.h @@ -0,0 +1,41 @@ +/** + * @file game_link.h + * @ingroup UI + * @brief Builds cockatrice://joingame links that let another user join a server game. + */ + +#ifndef GAME_LINK_H +#define GAME_LINK_H + +#include + +/** + * Builds a cockatrice://joingame link for the given server game. The receiver's + * client opens it through the intent chain (connect -> join room -> join game). + * @p description, when non-empty, is embedded in the link as the URL-encoded + * "game" query item so the receiving client can name the game in its confirm + * prompt and chat anchor instead of only its numeric id. Links built without it + * stay valid: the parser and chat renderer fall back to the id alone. + */ +QString +makeGameJoinLink(const QString &hostname, int port, int roomId, int gameId, const QString &description = QString()); + +/** + * One game the inviter is currently in and can invite another user to. + * @p label is meant for display in menus, @p url is the ready-made invite link. + * @p description is the raw game description for building tr()-wrapped invite + * messages (the label already embeds it, but the send sites need the raw value). + * @p onlyBuddies and @p creatorName mirror the server game's room settings so + * callers can gate the invite to the creator's buddies. + */ +struct GameInviteOption +{ + int gameId = 0; + QString label; + QString url; + QString description; + bool onlyBuddies = false; + QString creatorName; +}; + +#endif // GAME_LINK_H diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index 6580f0262..a1a2fb577 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -7,6 +7,7 @@ #include "../interface/widgets/tabs/tab_room.h" #include "../interface/widgets/tabs/tab_supervisor.h" #include "../interface/widgets/utility/get_text_with_max.h" +#include "game_link.h" #include "games_model.h" #include "user/user_list_manager.h" @@ -18,8 +19,6 @@ #include #include #include -#include -#include #include #include #include @@ -323,16 +322,9 @@ void GameSelector::customContextMenu(const QPoint &point) QAction copyLink(tr("Copy Game Link")); connect(©Link, &QAction::triggered, this, [=, this]() { const ServerInfo_Game &gameInfo = gameListModel->getGame(index.data(Qt::UserRole).toInt()); - QUrl url; - url.setScheme("cockatrice"); - url.setHost("joingame"); - QUrlQuery query; - query.addQueryItem("hostname", client->serverName()); - query.addQueryItem("port", QString::number(client->serverPort())); - query.addQueryItem("roomid", QString::number(gameInfo.room_id())); - query.addQueryItem("gameid", QString::number(gameInfo.game_id())); - url.setQuery(query); - QGuiApplication::clipboard()->setText(url.toString(QUrl::FullyEncoded)); + QGuiApplication::clipboard()->setText(makeGameJoinLink(client->serverName(), client->serverPort(), + gameInfo.room_id(), gameInfo.game_id(), + QString::fromStdString(gameInfo.description()))); }); QMenu menu; From 078e67c56fd65f34e2e9771ec8a414a93681eee6 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 16 Aug 2026 23:03:29 +0200 Subject: [PATCH 3/5] [Client] Name the game in the join-game password prompt (#7141) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lukas Brübach --- cockatrice/src/interface/widgets/server/game_selector.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index a1a2fb577..2ccf18e5d 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -370,7 +370,12 @@ void GameSelector::joinGame(const bool asSpectator, const bool asJudge) QString password; if (game.with_password() && !(spectator && !game.spectators_need_password()) && !overrideRestrictions) { bool ok; - password = getTextWithMax(this, tr("Join game"), tr("Password:"), QLineEdit::Password, QString(), &ok); + // Games without a description have no sensible label — fall back to the + // game id so the prompt still tells the user which game they're entering. + const QString gameLabel = QString::fromStdString(game.description()); + const QString prompt = gameLabel.isEmpty() ? tr("Password for game #%1:").arg(game.game_id()) + : tr("Password for \"%1\":").arg(gameLabel); + password = getTextWithMax(this, tr("Join game"), prompt, QLineEdit::Password, QString(), &ok); if (!ok) { return; } From 94943f7ff3954325f9153b10861364c970fee8a0 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 16 Aug 2026 23:20:36 +0200 Subject: [PATCH 4/5] [Client] Add copy-game-link action to the game menu (#7139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lukas Brübach --- .../widgets/server/game_selector.cpp | 2 +- .../src/interface/widgets/tabs/tab_game.cpp | 20 +++++++++++++++++++ .../src/interface/widgets/tabs/tab_game.h | 5 +++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index 2ccf18e5d..e9efc8663 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -319,7 +319,7 @@ void GameSelector::customContextMenu(const QPoint &point) dlg.exec(); }); - QAction copyLink(tr("Copy Game Link")); + QAction copyLink(tr("Cop&y game link")); connect(©Link, &QAction::triggered, this, [=, this]() { const ServerInfo_Game &gameInfo = gameListModel->getGame(index.data(Qt::UserRole).toInt()); QGuiApplication::clipboard()->setText(makeGameJoinLink(client->serverName(), client->serverPort(), diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index 82d99b605..3f165c1d5 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -20,6 +20,7 @@ #include "../interface/card_picture_loader/card_picture_loader.h" #include "../interface/widgets/cards/card_info_frame_widget.h" #include "../interface/widgets/dialogs/dlg_create_game.h" +#include "../interface/widgets/server/game_link.h" #include "../interface/widgets/server/user/user_list_manager.h" #include "../interface/widgets/utility/completer_utils.h" #include "../interface/widgets/utility/line_edit_completer.h" @@ -33,6 +34,8 @@ #include "tab_supervisor.h" #include +#include +#include #include #include #include @@ -331,6 +334,9 @@ void TabGame::retranslateUi() if (aGameInfo) { aGameInfo->setText(tr("Game &information")); } + if (aCopyGameLink) { + aCopyGameLink->setText(tr("Cop&y game link")); + } if (aConcede) { if (game->getPlayerManager()->isMainPlayerConceded()) { aConcede->setText(tr("Un&concede")); @@ -498,6 +504,15 @@ void TabGame::actGameInfo() dlg.exec(); } +void TabGame::actCopyGameLink() +{ + const QString link = + makeGameJoinLink(tabSupervisor->getClient()->serverName(), tabSupervisor->getClient()->serverPort(), + game->getGameMetaInfo()->proto().room_id(), game->getGameMetaInfo()->gameId(), + QString::fromStdString(game->getGameMetaInfo()->proto().description())); + QApplication::clipboard()->setText(link); +} + void TabGame::actConcede() { PlayerLogic *player = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer()); @@ -986,6 +1001,9 @@ void TabGame::createMenuItems() connect(aRotateViewCCW, &QAction::triggered, this, &TabGame::actRotateViewCCW); aGameInfo = new QAction(this); connect(aGameInfo, &QAction::triggered, this, &TabGame::actGameInfo); + aCopyGameLink = new QAction(this); + aCopyGameLink->setEnabled(!tabSupervisor->getIsLocalGame() && !tabSupervisor->getClient()->serverName().isEmpty()); + connect(aCopyGameLink, &QAction::triggered, this, &TabGame::actCopyGameLink); aConcede = new QAction(this); connect(aConcede, &QAction::triggered, this, &TabGame::actConcede); if (!game->getGameMetaInfo()->started()) { @@ -1024,6 +1042,7 @@ void TabGame::createMenuItems() gameMenu->addAction(aRotateViewCCW); gameMenu->addSeparator(); gameMenu->addAction(aGameInfo); + gameMenu->addAction(aCopyGameLink); gameMenu->addAction(aConcede); gameMenu->addAction(aFocusChat); gameMenu->addAction(aLeaveGame); @@ -1046,6 +1065,7 @@ void TabGame::createReplayMenuItems() aRotateViewCCW = nullptr; aResetLayout = nullptr; aGameInfo = nullptr; + aCopyGameLink = nullptr; aConcede = nullptr; aFocusChat = nullptr; aLeaveGame = new QAction(this); diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.h b/cockatrice/src/interface/widgets/tabs/tab_game.h index fc51817c6..b6555deef 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.h +++ b/cockatrice/src/interface/widgets/tabs/tab_game.h @@ -83,8 +83,8 @@ private: QAction *playersSeparator; QMenu *gameMenu, *viewMenu; TearOffMenu *phasesMenu; - QAction *aGameInfo, *aConcede, *aLeaveGame, *aNextPhase, *aNextPhaseAction, *aNextTurn, *aReverseTurn, - *aRemoveLocalArrows, *aRotateViewCW, *aRotateViewCCW, *aResetLayout, *aResetReplayLayout; + QAction *aGameInfo, *aConcede, *aCopyGameLink, *aLeaveGame, *aNextPhase, *aNextPhaseAction, *aNextTurn, + *aReverseTurn, *aRemoveLocalArrows, *aRotateViewCW, *aRotateViewCCW, *aResetLayout, *aResetReplayLayout; QAction *aFocusChat; QList phaseActions; QAction *aCardMenu; @@ -148,6 +148,7 @@ private slots: void actGameInfo(); void actConcede(); + void actCopyGameLink(); void actRemoveLocalArrows(); void actRotateViewCW(); void actRotateViewCCW(); From 6c8fcf7d197dacc7b49a86cbff7c8dd6282608b2 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 16 Aug 2026 23:20:47 +0200 Subject: [PATCH 5/5] [Client] Confirm before joining a full game as a spectator (#7140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lukas Brübach --- .../interface/widgets/server/game_selector.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index e9efc8663..28e2ae607 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -364,9 +364,22 @@ void GameSelector::joinGame(const bool asSpectator, const bool asJudge) return; } - bool spectator = asSpectator || game.player_count() == game.max_players(); - bool overrideRestrictions = !tabSupervisor->getAdminLocked(); + + // Joining a full game without override privileges silently becomes a + // spectator join, so ask first instead of surprising the player. + const bool gameFull = game.player_count() == game.max_players(); + if (gameFull && !asSpectator && !asJudge && !overrideRestrictions) { + const QMessageBox::StandardButton answer = + QMessageBox::question(this, tr("Join game"), tr("The game is full. Join as a spectator instead?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (answer != QMessageBox::Yes) { + return; + } + } + + bool spectator = asSpectator || gameFull; + QString password; if (game.with_password() && !(spectator && !game.spectators_need_password()) && !overrideRestrictions) { bool ok;