Merge remote-tracking branch 'upstream/master' into first-run-tutorial

This commit is contained in:
Lukas Brübach 2026-10-11 15:14:26 +02:00
commit f5826d9d6f
1395 changed files with 67177 additions and 10446 deletions

View file

@ -2,11 +2,16 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../../game/abstract_game.h"
#include "../../../game/game_event_handler.h"
#include "../../../game/game_meta_info.h"
#include "../../../game/game_state.h"
#include "../../../game/player/player_info.h"
#include "../../../game/player/player_manager.h"
#include "../../../game_graphics/board/abstract_card_item.h"
#include "../game/game.h"
#include "../game/player/player_logic.h"
#include "../game/replay.h"
#include "../game_graphics/board/arrow_item.h"
#include "../game_graphics/board/card_item.h"
#include "../game_graphics/deckview/deck_view_container.h"
#include "../game_graphics/deckview/tabbed_deck_view_container.h"
#include "../game_graphics/game_scene.h"
@ -20,29 +25,53 @@
#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/dialogs/dlg_invite_to_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"
#include "../interface/window_main.h"
#include "../main.h"
#include "../menus/tearoff_menu.h"
#include "../replay/replay_widget.h"
#include "../utility/visibility_change_listener.h"
#include "card/card_completer_proxy_model.h"
#include "card/card_search_model.h"
#include "card_database_display_model.h"
#include "card_database_model.h"
#include "libcockatrice/card/database/card_database_querier.h"
#include "libcockatrice/card/printing/exact_card.h"
#include "libcockatrice/deck_list/deck_list.h"
#include "libcockatrice/protocol/pb/serverinfo_game.pb.h"
#include "libcockatrice/protocol/pb/serverinfo_playerproperties.pb.h"
#include "libcockatrice/utility/card_ref.h"
#include "tab_supervisor.h"
#include <QAction>
#include <QApplication>
#include <QClipboard>
#include <QCloseEvent>
#include <QCompleter>
#include <QDebug>
#include <QDockWidget>
#include <QFlags>
#include <QHBoxLayout>
#include <QLabel>
#include <QMapIterator>
#include <QMenu>
#include <QMessageBox>
#include <QPoint>
#include <QPushButton>
#include <QRegularExpression>
#include <QSharedPointer>
#include <QStackedWidget>
#include <QStringListModel>
#include <QSystemTrayIcon>
#include <QTimer>
#include <QVBoxLayout>
#include <QWidget>
#include <libcockatrice/card/database/card_database.h>
#include <QtGlobal>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/event_game_joined.pb.h>
#include <libcockatrice/protocol/pb/game_replay.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_player.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/settings/chat_settings.h>
@ -50,6 +79,18 @@
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/layouts_settings.h>
#include <libcockatrice/utility/string_limits.h>
#include <qnamespace.h>
#include <qobjectdefs.h>
#include <utility>
class Event_GameJoined;
namespace google
{
namespace protobuf
{
class Message;
} // namespace protobuf
} // namespace google
TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay)
: Tab(_tabSupervisor), sayLabel(nullptr), sayEdit(nullptr)
@ -450,13 +491,20 @@ void TabGame::linkCardToChat(const QString &cardName)
sayEdit->setFocus();
}
void TabGame::resetChatAndPhase()
void TabGame::resetForRewind()
{
// reset chat log
messageLog->clearChat();
// reset phase markers
game->getGameState()->setCurrentPhase(-1);
// reset view zones
static_cast<GameScene *>(gameView->scene())->emptyActiveViews();
// reset spectator state so the replay can rebuild it from the start
game->getPlayerManager()->clearSpectators();
playerListWidget->clearSpectators();
}
void TabGame::emitUserEvent()
@ -490,6 +538,9 @@ void TabGame::retranslateUi()
QString tabText = " | " + type + " #" + QString::number(game->getGameMetaInfo()->gameId());
updatePlayerListDockTitle();
if (inviteButton) {
inviteButton->setText(tr("Invite"));
}
cardInfoDock->setWindowTitle(tr("Card Info") + (cardInfoDock->isWindow() ? tabText : QString()));
messageLayoutDock->setWindowTitle(tr("Messages") + (messageLayoutDock->isWindow() ? tabText : QString()));
if (replayDock) {
@ -528,6 +579,12 @@ void TabGame::retranslateUi()
if (aGameInfo) {
aGameInfo->setText(tr("Game &information"));
}
if (aCopyGameLink) {
aCopyGameLink->setText(tr("Cop&y game link"));
}
if (aInviteToGame) {
aInviteToGame->setText(tr("Invite to Game..."));
}
if (aConcede) {
if (game->getPlayerManager()->isMainPlayerConceded()) {
aConcede->setText(tr("Un&concede"));
@ -695,6 +752,56 @@ 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::updateInviteButtonState()
{
// The dock button stays conservative (pre-start, not full); the menu action
// additionally covers started/full games, which are legitimate spectate
// invites, so it only needs the server-linked + not-closed conditions.
const bool canInvite = !tabSupervisor->getIsLocalGame() && !game->getGameState()->isGameClosed() &&
!game->getGameMetaInfo()->started() &&
game->getPlayerManager()->getPlayerCount() < game->getGameMetaInfo()->maxPlayers();
if (inviteButton) {
inviteButton->setVisible(canInvite);
}
if (aInviteToGame) {
aInviteToGame->setEnabled(!tabSupervisor->getIsLocalGame() && !game->getGameState()->isGameClosed());
}
}
void TabGame::actInviteToGame()
{
if (!tabSupervisor || tabSupervisor->getIsLocalGame()) {
return;
}
GameMetaInfo *metaInfo = game->getGameMetaInfo();
const QString inviteUrl = makeGameJoinLink(
tabSupervisor->getClient()->serverName(), tabSupervisor->getClient()->serverPort(), metaInfo->proto().room_id(),
metaInfo->gameId(), QString::fromStdString(metaInfo->proto().description()));
QStringList excludeUserNames;
excludeUserNames << tabSupervisor->getUserListManager()->getOwnUsername();
for (auto player : game->getPlayerManager()->getPlayers()) {
excludeUserNames << player->getPlayerInfo()->getName();
}
for (auto it = game->getPlayerManager()->getSpectators().cbegin();
it != game->getPlayerManager()->getSpectators().cend(); ++it) {
excludeUserNames << QString::fromStdString(it.value().name());
}
DlgInviteToGame dlg(tabSupervisor, inviteUrl, metaInfo->proto().only_buddies(), excludeUserNames, this);
dlg.exec();
}
void TabGame::actConcede()
{
PlayerLogic *player = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer());
@ -742,7 +849,8 @@ bool TabGame::leaveGame()
void TabGame::actSay()
{
if (completer->popup()->isVisible()) {
if (sayEdit->hasVisibleCompleterPopup()) {
sayEdit->hideCompleterPopups();
return;
}
@ -762,14 +870,14 @@ void TabGame::addPlayerToAutoCompleteList(QString playerName)
{
if (sayEdit && !autocompleteUserList.contains(playerName)) {
autocompleteUserList << playerName;
sayEdit->setCompletionList(autocompleteUserList);
mentionModel->setStringList(autocompleteUserList);
}
}
void TabGame::removePlayerFromAutoCompleteList(QString playerName)
{
if (sayEdit && autocompleteUserList.removeOne(playerName)) {
sayEdit->setCompletionList(autocompleteUserList);
mentionModel->setStringList(autocompleteUserList);
}
}
@ -832,8 +940,8 @@ void TabGame::actRotateViewCCW()
void TabGame::actCompleterChanged()
{
SettingsCache::instance().chat().getChatMentionCompleter() ? completer->setCompletionRole(2)
: completer->setCompletionRole(1);
SettingsCache::instance().chat().getChatMentionCompleter() ? mentionCompleter->setCompletionRole(2)
: mentionCompleter->setCompletionRole(1);
}
void TabGame::notifyPlayerJoin(QString playerName)
@ -1045,6 +1153,7 @@ void TabGame::stopGame()
QMapIterator<int, TabbedDeckViewContainer *> i(deckViewContainers);
while (i.hasNext()) {
i.next();
i.value()->playerDeckView->advancePlaymatRotation();
i.value()->show();
}
@ -1185,6 +1294,11 @@ 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);
aInviteToGame = new QAction(this);
connect(aInviteToGame, &QAction::triggered, this, &TabGame::actInviteToGame);
aConcede = new QAction(this);
connect(aConcede, &QAction::triggered, this, &TabGame::actConcede);
if (!game->getGameMetaInfo()->started()) {
@ -1223,6 +1337,8 @@ void TabGame::createMenuItems()
gameMenu->addAction(aRotateViewCCW);
gameMenu->addSeparator();
gameMenu->addAction(aGameInfo);
gameMenu->addAction(aCopyGameLink);
gameMenu->addAction(aInviteToGame);
gameMenu->addAction(aConcede);
gameMenu->addAction(aFocusChat);
gameMenu->addAction(aLeaveGame);
@ -1231,6 +1347,9 @@ void TabGame::createMenuItems()
aCardMenu = gameMenu->addMenu(new QMenu(this));
// Sync the new action with the same state the dock button already shows.
updateInviteButtonState();
addTabMenu(gameMenu);
}
@ -1245,6 +1364,8 @@ void TabGame::createReplayMenuItems()
aRotateViewCCW = nullptr;
aResetLayout = nullptr;
aGameInfo = nullptr;
aCopyGameLink = nullptr;
aInviteToGame = nullptr;
aConcede = nullptr;
aFocusChat = nullptr;
aLeaveGame = new QAction(this);
@ -1387,7 +1508,7 @@ void TabGame::createReplayDock(GameReplay *replay)
replayDock->setWidget(replayWidget);
replayDock->setFloating(false);
connect(replayWidget, &ReplayWidget::rewound, this, &TabGame::resetChatAndPhase);
connect(replayWidget, &ReplayWidget::rewound, this, &TabGame::resetForRewind);
connect(replayWidget, &ReplayWidget::eventReplayed, game->getGameEventHandler(),
[this](const auto &event, auto options) {
game->getGameEventHandler()->processGameEventContainer(event, nullptr, options);
@ -1443,11 +1564,29 @@ void TabGame::createPlayerListDock(bool bReplay)
}
playerListWidget->setFocusPolicy(Qt::NoFocus);
auto *playerListBox = new QWidget(this);
auto *vbox = new QVBoxLayout(playerListBox);
vbox->setContentsMargins(0, 0, 0, 0);
vbox->setSpacing(0);
vbox->addWidget(playerListWidget);
if (!bReplay) {
inviteButton = new QPushButton(tr("Invite"), playerListBox);
inviteButton->setVisible(false);
connect(inviteButton, &QPushButton::clicked, this, &TabGame::actInviteToGame);
vbox->addWidget(inviteButton);
connect(game->getGameMetaInfo(), &GameMetaInfo::startedChanged, this, &TabGame::updateInviteButtonState);
connect(game->getPlayerManager(), &PlayerManager::playerCountChanged, this, &TabGame::updateInviteButtonState);
updateInviteButtonState();
}
playerListDock = new QDockWidget(this);
playerListDock->setObjectName("playerListDock");
playerListDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable |
QDockWidget::DockWidgetMovable);
playerListDock->setWidget(playerListWidget);
playerListDock->setWidget(playerListBox);
playerListDock->setFloating(false);
}
@ -1471,6 +1610,7 @@ void TabGame::createMessageDock(bool bReplay)
qOverload<const QString &>(&CardInfoFrameWidget::setCard));
connect(messageLog, &MessageLogWidget::showCardInfoPopup, this, &TabGame::showCardInfoPopup);
connect(messageLog, &MessageLogWidget::deleteCardInfoPopup, this, &TabGame::deleteCardInfoPopup);
connect(messageLog, &MessageLogWidget::cockatriceLinkActivated, this, &TabGame::cockatriceLinkActivated);
if (!bReplay) {
connect(messageLog, &MessageLogWidget::openMessageDialog, this, &TabGame::openMessageDialog);
@ -1488,12 +1628,25 @@ void TabGame::createMessageDock(bool bReplay)
sayEdit->setMaxLength(MAX_TEXT_LENGTH);
sayLabel->setBuddy(sayEdit);
connect(this, &TabGame::chatMessageSent, game->getGameEventHandler(), &GameEventHandler::handleChatMessageSent);
completer = new QCompleter(autocompleteUserList, sayEdit);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setMaxVisibleItems(5);
completer->setFilterMode(Qt::MatchStartsWith);
mentionModel = new QStringListModel(autocompleteUserList, sayEdit);
mentionCompleter = createMentionCompleter(mentionModel, sayEdit);
sayEdit->addCompleter(mentionCompleter, CompleterTrigger::Mention);
auto *cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, sayEdit);
auto *displayModel = new CardDatabaseDisplayModel(sayEdit);
displayModel->setSourceModel(cardDatabaseModel);
const CardCompleterSetup cardSetup = createCardCompleter(displayModel, sayEdit);
sayEdit->addCompleter(cardSetup.completer, CompleterTrigger::Card);
connect(sayEdit, &LineEditCompleter::cardPartialChanged, this, [this, cardSetup](const QString &text) {
cardSetup.searchModel->updateSearchResults(text);
cardSetup.proxyModel->setFilterRegularExpression(
QRegularExpression(QRegularExpression::escape(text), QRegularExpression::CaseInsensitiveOption));
if (sayEdit->hasFocus()) {
cardSetup.completer->complete();
}
});
sayEdit->setCompleter(completer);
actCompleterChanged();
if (game->getPlayerManager()->isSpectator()) {