mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-24 18:33:03 -07:00
Compare commits
9 commits
ee224adbbe
...
7fb3032961
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fb3032961 | ||
|
|
54b6b273a4 | ||
|
|
e1ac4d38a3 | ||
|
|
21871ffbae | ||
|
|
76a386d6ed | ||
|
|
68e4fa054d | ||
|
|
6f86c45ea8 | ||
|
|
dade7ae78a | ||
|
|
8f52223322 |
52 changed files with 2004 additions and 74 deletions
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
|
|
@ -40,7 +40,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: "Checkout repository"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: "Initialize CodeQL"
|
||||
uses: github/codeql-action/init@v4
|
||||
|
|
|
|||
2
.github/workflows/docker-release.yml
vendored
2
.github/workflows/docker-release.yml
vendored
|
|
@ -127,7 +127,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: "Download digests"
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digest-*
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/dialogs/dlg_settings.cpp
|
||||
src/interface/widgets/dialogs/dlg_startup_card_check.cpp
|
||||
src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp
|
||||
src/interface/widgets/dialogs/dlg_tournament_settings.cpp
|
||||
src/interface/widgets/dialogs/dlg_update.cpp
|
||||
src/interface/widgets/dialogs/dlg_view_log.cpp
|
||||
src/interface/widgets/dialogs/override_printing_warning.cpp
|
||||
|
|
@ -215,6 +216,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/deck_editor/deck_list_style_proxy.cpp
|
||||
src/interface/widgets/deck_editor/deck_state_manager.cpp
|
||||
src/interface/widgets/deck_editor/printing_disabled_info_widget.cpp
|
||||
src/interface/widgets/draft/tournament_widget.cpp
|
||||
src/interface/widgets/general/background_sources.cpp
|
||||
src/interface/widgets/general/display/background_plate_widget.cpp
|
||||
src/interface/widgets/general/display/banner_widget.cpp
|
||||
|
|
@ -391,6 +393,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/tabs/tab_server.cpp
|
||||
src/interface/widgets/tabs/tab_supervisor.cpp
|
||||
src/interface/widgets/tabs/tab_visual_database_display.cpp
|
||||
src/interface/widgets/tabs/tournament_tab_game_extension.cpp
|
||||
src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp
|
||||
src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp
|
||||
src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
#include <libcockatrice/protocol/pb/event_reverse_turn.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_set_active_phase.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_set_active_player.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_tournament_state.pb.h>
|
||||
#include <libcockatrice/protocol/pb/game_event_container.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
|
||||
|
|
@ -158,6 +159,9 @@ void GameEventHandler::processGameEventContainer(const GameEventContainer &cont,
|
|||
case GameEvent::REVERSE_TURN:
|
||||
eventReverseTurn(event.GetExtension(Event_ReverseTurn::ext), playerId, context);
|
||||
break;
|
||||
case GameEvent::TOURNAMENT_STATE:
|
||||
emit tournamentStateChanged(event.GetExtension(Event_TournamentState::ext));
|
||||
break;
|
||||
|
||||
default: {
|
||||
PlayerLogic *player = game->getPlayerManager()->getPlayers().value(playerId, 0);
|
||||
|
|
@ -263,6 +267,12 @@ void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event
|
|||
int /*eventPlayerId*/,
|
||||
const GameEventContext & /*context*/)
|
||||
{
|
||||
// Sub-games of a tournament report their parent hub game so the client can
|
||||
// route "close game" back to the parent tab.
|
||||
if (event.parent_game_id() != -1) {
|
||||
game->getGameMetaInfo()->setParentGameId(event.parent_game_id());
|
||||
}
|
||||
|
||||
const int playerListSize = event.player_list_size();
|
||||
|
||||
QVector<QPair<int, QPair<QString, QString>>> opponentDecksToDisplay;
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ class Event_SetActivePhase;
|
|||
class Event_GameSay;
|
||||
class Event_Kicked;
|
||||
class Event_ReverseTurn;
|
||||
class Event_TournamentState;
|
||||
class Event_Ping;
|
||||
|
||||
inline Q_LOGGING_CATEGORY(GameEventHandlerLog, "game_event_handler");
|
||||
|
|
@ -329,6 +330,7 @@ signals:
|
|||
|
||||
void gameStopped();
|
||||
void gameClosed();
|
||||
void tournamentStateChanged(const Event_TournamentState &state);
|
||||
void playerPropertiesChanged(const ServerInfo_PlayerProperties &prop, int playerId);
|
||||
void playerJoined(const ServerInfo_PlayerProperties &playerInfo);
|
||||
void playerLeft(int leavingPlayerId);
|
||||
|
|
|
|||
|
|
@ -84,6 +84,26 @@ public:
|
|||
return roomGameTypes.find(gameInfo_.game_types(index)).value();
|
||||
}
|
||||
|
||||
bool isTournament() const
|
||||
{
|
||||
return gameInfo_.is_tournament();
|
||||
}
|
||||
|
||||
void setIsTournament(bool t)
|
||||
{
|
||||
gameInfo_.set_is_tournament(t);
|
||||
}
|
||||
|
||||
int parentGameId() const
|
||||
{
|
||||
return parentGameId_;
|
||||
}
|
||||
|
||||
void setParentGameId(int id)
|
||||
{
|
||||
parentGameId_ = id;
|
||||
}
|
||||
|
||||
public slots:
|
||||
void setStarted(bool s)
|
||||
{
|
||||
|
|
@ -108,6 +128,7 @@ signals:
|
|||
|
||||
private:
|
||||
ServerInfo_Game gameInfo_;
|
||||
int parentGameId_ = -1;
|
||||
};
|
||||
|
||||
#endif // GAME_META_INFO_H
|
||||
|
|
|
|||
|
|
@ -90,10 +90,10 @@ public:
|
|||
* @param context Additional context (undo, judge, etc.).
|
||||
* @param options Processing options (UI suppression, reveal behavior).
|
||||
*/
|
||||
void processGameEvent(GameEvent::GameEventType type,
|
||||
const GameEvent &event,
|
||||
const GameEventContext &context,
|
||||
EventProcessingOptions options);
|
||||
virtual void processGameEvent(GameEvent::GameEventType type,
|
||||
const GameEvent &event,
|
||||
const GameEventContext &context,
|
||||
EventProcessingOptions options);
|
||||
|
||||
/** @} */
|
||||
|
||||
|
|
@ -266,7 +266,7 @@ signals:
|
|||
void cardZoneChanged(CardItem *card, bool sameZone);
|
||||
void requestCardMenuUpdate(const CardItem *card);
|
||||
|
||||
private:
|
||||
protected:
|
||||
/** Owning player instance. */
|
||||
PlayerLogic *player;
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,19 @@ PlayerLogic::PlayerLogic(const ServerInfo_User &info, int _id, bool _local, bool
|
|||
initializeZones();
|
||||
}
|
||||
|
||||
PlayerLogic::PlayerLogic(const ServerInfo_User &info,
|
||||
int _id,
|
||||
bool _local,
|
||||
bool _judge,
|
||||
AbstractGame *_parent,
|
||||
PlayerEventHandler *customEventHandler)
|
||||
: QObject(_parent), game(_parent), playerInfo(new PlayerInfo(info, _id, _local, _judge)),
|
||||
playerEventHandler(customEventHandler), playerActions(new PlayerActions(this)), active(false), conceded(false),
|
||||
zoneId(0), dialogSemaphore(false)
|
||||
{
|
||||
initializeZones();
|
||||
}
|
||||
|
||||
void PlayerLogic::initializeZones()
|
||||
{
|
||||
addZone(new PileZoneLogic(this, ZoneNames::DECK, false, true, false, this));
|
||||
|
|
|
|||
|
|
@ -99,7 +99,30 @@ public:
|
|||
PlayerLogic(const ServerInfo_User &info, int _id, bool _local, bool _judge, AbstractGame *_parent);
|
||||
~PlayerLogic() override;
|
||||
|
||||
void initializeZones();
|
||||
protected:
|
||||
/**
|
||||
* @brief Constructor for subclasses that need a custom event handler (e.g. DraftPlayerLogic).
|
||||
*
|
||||
* @p customEventHandler must be non-null and either QObject-parented to this PlayerLogic
|
||||
* or deleted externally; it is not owned by PlayerLogic. The handler connects to @c player
|
||||
* during its own constructor, so passing a freshly built subclass handler from an
|
||||
* initializer list is safe.
|
||||
*/
|
||||
PlayerLogic(const ServerInfo_User &info,
|
||||
int _id,
|
||||
bool _local,
|
||||
bool _judge,
|
||||
AbstractGame *_parent,
|
||||
PlayerEventHandler *customEventHandler);
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Creates the standard zone set.
|
||||
*
|
||||
* Not virtually dispatched from constructors — subclasses overriding this must add
|
||||
* their extra zones in their own constructor body.
|
||||
*/
|
||||
virtual void initializeZones();
|
||||
void updateZones();
|
||||
void clear();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
#include "dlg_convert_deck_to_cod_format.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../deck_loader/deck_loader.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||
|
||||
DialogConvertDeckToCodFormat::DialogConvertDeckToCodFormat(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
|
|
@ -38,3 +46,71 @@ bool DialogConvertDeckToCodFormat::dontAskAgain() const
|
|||
{
|
||||
return dontAskAgainCheckbox->isChecked();
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
|
||||
{
|
||||
QFileInfo fileInfo(filePath);
|
||||
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
|
||||
|
||||
if (QFile::exists(newFileName)) {
|
||||
QMessageBox::StandardButton reply =
|
||||
QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"),
|
||||
QObject::tr("A .cod version of this deck already exists. Overwrite it?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
return reply == QMessageBox::Yes;
|
||||
}
|
||||
return true; // Safe to proceed
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool DialogConvertDeckToCodFormat::promptIfRequired(QWidget *parent,
|
||||
const QString &filePath,
|
||||
const std::function<bool()> &convert)
|
||||
{
|
||||
if (DeckFileFormat::getFormatFromName(filePath) == DeckFileFormat::Cockatrice) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Retrieve saved preference if the prompt is disabled
|
||||
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) {
|
||||
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!confirmOverwriteIfExists(parent, filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return convert();
|
||||
}
|
||||
|
||||
// Show the dialog to the user
|
||||
DialogConvertDeckToCodFormat conversionDialog(parent);
|
||||
if (conversionDialog.exec() != QDialog::Accepted) {
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(
|
||||
!conversionDialog.dontAskAgain());
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to convert file
|
||||
if (!confirmOverwriteIfExists(parent, filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!convert()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (conversionDialog.dontAskAgain()) {
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false);
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
#include <functional>
|
||||
|
||||
class QWidget;
|
||||
|
||||
class DialogConvertDeckToCodFormat : public QDialog
|
||||
{
|
||||
|
|
@ -24,6 +27,21 @@ public:
|
|||
|
||||
[[nodiscard]] bool dontAskAgain() const;
|
||||
|
||||
/**
|
||||
* @brief Checks whether the deck file at \a filePath can store tags.
|
||||
*
|
||||
* If the file is not a .cod deck, prompts the user for conversion to the
|
||||
* Cockatrice format, honoring the saved "always convert / don't ask again"
|
||||
* preference. On acceptance \a convert is called to perform the conversion.
|
||||
*
|
||||
* @param parent The widget to parent the prompt to.
|
||||
* @param filePath The path of the deck file to check.
|
||||
* @param convert Called to convert the deck once the user agrees.
|
||||
* @return true if tags can be stored (no conversion needed, or the conversion
|
||||
* was performed), false if the user declined to convert.
|
||||
*/
|
||||
static bool promptIfRequired(QWidget *parent, const QString &filePath, const std::function<bool()> &convert);
|
||||
|
||||
private:
|
||||
QVBoxLayout *layout;
|
||||
QLabel *label;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../interface/widgets/tabs/tab_room.h"
|
||||
#include "dlg_tournament_settings.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
|
|
@ -104,14 +105,29 @@ void DlgCreateGame::sharedCtor()
|
|||
|
||||
shareDecklistsOnLoadCheckBox = new QCheckBox(tr("Open decklists in lobby"));
|
||||
|
||||
tournamentCheckBox = new QCheckBox(tr("Tournament mode"));
|
||||
tournamentCheckBox->setToolTip(tr("All players submit a deck up front and rounds are paired automatically"));
|
||||
tournamentSettingsButton = new QPushButton(tr("Settings..."));
|
||||
tournamentSettingsButton->setEnabled(false);
|
||||
connect(tournamentCheckBox, &QCheckBox::toggled, tournamentSettingsButton, &QPushButton::setEnabled);
|
||||
connect(tournamentSettingsButton, &QPushButton::clicked, this, [this] {
|
||||
DlgTournamentSettings dlg(this);
|
||||
dlg.setCurrentGamesPerMatch(tournamentSettings.gamesPerMatch);
|
||||
if (dlg.exec() == QDialog::Accepted) {
|
||||
tournamentSettings = dlg.getResult();
|
||||
}
|
||||
});
|
||||
|
||||
createGameAsJudgeCheckBox = new QCheckBox(tr("Create game as judge"));
|
||||
|
||||
auto *gameSetupOptionsLayout = new QGridLayout;
|
||||
gameSetupOptionsLayout->addWidget(startingLifeTotalLabel, 0, 0);
|
||||
gameSetupOptionsLayout->addWidget(startingLifeTotalEdit, 0, 1);
|
||||
gameSetupOptionsLayout->addWidget(shareDecklistsOnLoadCheckBox, 1, 0);
|
||||
gameSetupOptionsLayout->addWidget(tournamentCheckBox, 2, 0);
|
||||
gameSetupOptionsLayout->addWidget(tournamentSettingsButton, 2, 1);
|
||||
if (room && room->getUserInfo()->user_level() & ServerInfo_User::IsJudge) {
|
||||
gameSetupOptionsLayout->addWidget(createGameAsJudgeCheckBox, 2, 0);
|
||||
gameSetupOptionsLayout->addWidget(createGameAsJudgeCheckBox, 3, 0);
|
||||
} else {
|
||||
createGameAsJudgeCheckBox->setChecked(false);
|
||||
createGameAsJudgeCheckBox->setHidden(true);
|
||||
|
|
@ -188,7 +204,7 @@ DlgCreateGame::DlgCreateGame(TabRoom *_room, const QMap<int, QString> &_gameType
|
|||
}
|
||||
|
||||
DlgCreateGame::DlgCreateGame(const ServerInfo_Game &gameInfo, const QMap<int, QString> &_gameTypes, QWidget *parent)
|
||||
: QDialog(parent), room(0), gameTypes(_gameTypes)
|
||||
: QDialog(parent), room(nullptr), gameTypes(_gameTypes)
|
||||
{
|
||||
sharedCtor();
|
||||
|
||||
|
|
@ -205,6 +221,8 @@ DlgCreateGame::DlgCreateGame(const ServerInfo_Game &gameInfo, const QMap<int, QS
|
|||
createGameAsSpectatorCheckBox->setEnabled(false);
|
||||
startingLifeTotalEdit->setEnabled(false);
|
||||
shareDecklistsOnLoadCheckBox->setEnabled(false);
|
||||
tournamentCheckBox->setEnabled(false);
|
||||
tournamentSettingsButton->setEnabled(false);
|
||||
|
||||
descriptionEdit->setText(QString::fromStdString(gameInfo.description()));
|
||||
maxPlayersEdit->setValue(gameInfo.max_players());
|
||||
|
|
@ -215,6 +233,7 @@ DlgCreateGame::DlgCreateGame(const ServerInfo_Game &gameInfo, const QMap<int, QS
|
|||
spectatorsCanTalkCheckBox->setChecked(gameInfo.spectators_can_chat());
|
||||
spectatorsSeeEverythingCheckBox->setChecked(gameInfo.spectators_omniscient());
|
||||
shareDecklistsOnLoadCheckBox->setChecked(gameInfo.share_decklists_on_load());
|
||||
tournamentCheckBox->setChecked(gameInfo.is_tournament());
|
||||
|
||||
QSet<int> types;
|
||||
for (int i = 0; i < gameInfo.game_types_size(); ++i) {
|
||||
|
|
@ -252,6 +271,8 @@ void DlgCreateGame::actReset()
|
|||
|
||||
startingLifeTotalEdit->setValue(20);
|
||||
shareDecklistsOnLoadCheckBox->setChecked(false);
|
||||
tournamentCheckBox->setChecked(false);
|
||||
tournamentSettings = DlgTournamentSettingsResult{};
|
||||
createGameAsJudgeCheckBox->setChecked(false);
|
||||
|
||||
QMapIterator<int, QRadioButton *> gameTypeCheckBoxIterator(gameTypeCheckBoxes);
|
||||
|
|
@ -282,6 +303,10 @@ void DlgCreateGame::actOK()
|
|||
cmd.set_join_as_spectator(createGameAsSpectatorCheckBox->isChecked());
|
||||
cmd.set_starting_life_total(startingLifeTotalEdit->value());
|
||||
cmd.set_share_decklists_on_load(shareDecklistsOnLoadCheckBox->isChecked());
|
||||
cmd.set_is_tournament(tournamentCheckBox->isChecked());
|
||||
if (tournamentCheckBox->isChecked()) {
|
||||
cmd.set_games_per_match(tournamentSettings.gamesPerMatch);
|
||||
}
|
||||
|
||||
auto _gameTypes = QString();
|
||||
QMapIterator<int, QRadioButton *> gameTypeCheckBoxIterator(gameTypeCheckBoxes);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
#ifndef DLG_CREATEGAME_H
|
||||
#define DLG_CREATEGAME_H
|
||||
|
||||
#include "dlg_tournament_settings.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QMap>
|
||||
#include <libcockatrice/utility/macros.h>
|
||||
|
|
@ -48,6 +50,9 @@ private:
|
|||
QCheckBox *spectatorsAllowedCheckBox, *spectatorsNeedPasswordCheckBox, *spectatorsCanTalkCheckBox,
|
||||
*spectatorsSeeEverythingCheckBox, *createGameAsJudgeCheckBox, *createGameAsSpectatorCheckBox;
|
||||
QCheckBox *shareDecklistsOnLoadCheckBox;
|
||||
QCheckBox *tournamentCheckBox;
|
||||
QPushButton *tournamentSettingsButton;
|
||||
DlgTournamentSettingsResult tournamentSettings;
|
||||
QDialogButtonBox *buttonBox;
|
||||
QPushButton *clearButton;
|
||||
QCheckBox *rememberGameSettings;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
#include "dlg_tournament_settings.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
|
||||
DlgTournamentSettings::DlgTournamentSettings(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Tournament Settings"));
|
||||
|
||||
auto *mainLayout = new QFormLayout(this);
|
||||
|
||||
gamesPerMatchSpin = new QSpinBox(this);
|
||||
gamesPerMatchSpin->setRange(1, 5);
|
||||
gamesPerMatchSpin->setValue(1);
|
||||
gamesPerMatchSpin->setToolTip(tr("Number of games per match (e.g., 3 for Best of 3)"));
|
||||
mainLayout->addRow(tr("Games per match:"), gamesPerMatchSpin);
|
||||
|
||||
QLabel *hintLabel = new QLabel(tr("Set to 3 for Best of 3, 5 for Best of 5, etc."), this);
|
||||
hintLabel->setStyleSheet("color: palette(placeholderText);");
|
||||
mainLayout->addRow(QString(), hintLabel);
|
||||
|
||||
buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgTournamentSettings::actOK);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
mainLayout->addRow(buttonBox);
|
||||
|
||||
setFixedHeight(sizeHint().height());
|
||||
}
|
||||
|
||||
DlgTournamentSettingsResult DlgTournamentSettings::getResult() const
|
||||
{
|
||||
DlgTournamentSettingsResult result;
|
||||
result.gamesPerMatch = gamesPerMatchSpin->value();
|
||||
return result;
|
||||
}
|
||||
|
||||
void DlgTournamentSettings::setCurrentGamesPerMatch(int n)
|
||||
{
|
||||
gamesPerMatchSpin->setValue(n);
|
||||
}
|
||||
|
||||
void DlgTournamentSettings::actOK()
|
||||
{
|
||||
accept();
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
#ifndef DLG_TOURNAMENT_SETTINGS_H
|
||||
#define DLG_TOURNAMENT_SETTINGS_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class QDialogButtonBox;
|
||||
class QSpinBox;
|
||||
|
||||
struct DlgTournamentSettingsResult
|
||||
{
|
||||
int gamesPerMatch = 1;
|
||||
};
|
||||
|
||||
class DlgTournamentSettings : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DlgTournamentSettings(QWidget *parent = nullptr);
|
||||
DlgTournamentSettingsResult getResult() const;
|
||||
void setCurrentGamesPerMatch(int n);
|
||||
|
||||
private slots:
|
||||
void actOK();
|
||||
|
||||
private:
|
||||
QSpinBox *gamesPerMatchSpin;
|
||||
QDialogButtonBox *buttonBox;
|
||||
};
|
||||
|
||||
#endif
|
||||
227
cockatrice/src/interface/widgets/draft/tournament_widget.cpp
Normal file
227
cockatrice/src/interface/widgets/draft/tournament_widget.cpp
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
#include "tournament_widget.h"
|
||||
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QTableWidgetItem>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace
|
||||
{
|
||||
const int PLAYER_COLUMN = 0;
|
||||
const int DECK_COLUMN = 1;
|
||||
const int WINS_COLUMN = 2;
|
||||
const int LOSSES_COLUMN = 3;
|
||||
const int DRAWS_COLUMN = 4;
|
||||
|
||||
const int PLAYER1_COLUMN = 0;
|
||||
const int SCORE_COLUMN = 1;
|
||||
const int STATUS_COLUMN = 2;
|
||||
const int PLAYER2_COLUMN = 3;
|
||||
} // namespace
|
||||
|
||||
TournamentWidget::TournamentWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
|
||||
statusLabel = new QLabel(this);
|
||||
statusLabel->setStyleSheet("font-weight: bold;");
|
||||
mainLayout->addWidget(statusLabel);
|
||||
|
||||
roundLabel = new QLabel(this);
|
||||
mainLayout->addWidget(roundLabel);
|
||||
|
||||
standingsTable = new QTableWidget(this);
|
||||
standingsTable->setColumnCount(5);
|
||||
standingsTable->horizontalHeader()->setStretchLastSection(true);
|
||||
standingsTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
standingsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
standingsTable->verticalHeader()->setVisible(false);
|
||||
mainLayout->addWidget(standingsTable);
|
||||
|
||||
pairingsTable = new QTableWidget(this);
|
||||
pairingsTable->setColumnCount(4);
|
||||
pairingsTable->horizontalHeader()->setStretchLastSection(true);
|
||||
pairingsTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
pairingsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
pairingsTable->verticalHeader()->setVisible(false);
|
||||
mainLayout->addWidget(pairingsTable);
|
||||
|
||||
openMatchButton = new QPushButton(this);
|
||||
openMatchButton->setEnabled(false);
|
||||
connect(openMatchButton, &QPushButton::clicked, this, [this]() { emit openMatchGameRequested(currentGameId); });
|
||||
mainLayout->addWidget(openMatchButton);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void TournamentWidget::retranslateUi()
|
||||
{
|
||||
standingsTable->setHorizontalHeaderLabels({tr("Player"), tr("Deck"), tr("W"), tr("L"), tr("D")});
|
||||
pairingsTable->setHorizontalHeaderLabels({tr("Player 1"), tr("Score"), tr("Status"), tr("Player 2")});
|
||||
openMatchButton->setText(tr("Open match game"));
|
||||
|
||||
if (lastState.has_phase()) {
|
||||
updateTournamentState(lastState);
|
||||
} else {
|
||||
statusLabel->setText(tr("Tournament"));
|
||||
roundLabel->setVisible(true);
|
||||
roundLabel->setText(tr("Waiting for the tournament to start"));
|
||||
}
|
||||
}
|
||||
|
||||
void TournamentWidget::setLocalPlayerId(int playerId)
|
||||
{
|
||||
localPlayerId = playerId;
|
||||
}
|
||||
|
||||
QString TournamentWidget::getPlayerName(const Event_TournamentState &state, int playerId) const
|
||||
{
|
||||
for (int i = 0; i < state.players_size(); ++i) {
|
||||
if (state.players(i).player_id() == playerId) {
|
||||
return QString::fromStdString(state.players(i).player_name());
|
||||
}
|
||||
}
|
||||
return tr("Player %1").arg(playerId);
|
||||
}
|
||||
|
||||
void TournamentWidget::updateTournamentState(const Event_TournamentState &state)
|
||||
{
|
||||
lastState = state;
|
||||
|
||||
int gamesPerMatch = getGamesPerMatch(state);
|
||||
QString statusText;
|
||||
switch (state.phase()) {
|
||||
case Event_TournamentState::PHASE_DECK_BUILDING:
|
||||
statusText = gamesPerMatch > 1 ? tr("Tournament - Deck building (Best of %1)").arg(gamesPerMatch * 2 - 1)
|
||||
: tr("Tournament - Deck building");
|
||||
break;
|
||||
case Event_TournamentState::PHASE_PLAYING:
|
||||
statusText = gamesPerMatch > 1 ? tr("Tournament - Playing (Best of %1)").arg(gamesPerMatch * 2 - 1)
|
||||
: tr("Tournament - Playing");
|
||||
break;
|
||||
case Event_TournamentState::PHASE_FINISHED:
|
||||
statusText = gamesPerMatch > 1 ? tr("Tournament - Finished (Best of %1)").arg(gamesPerMatch * 2 - 1)
|
||||
: tr("Tournament - Finished");
|
||||
break;
|
||||
default:
|
||||
statusText = tr("Tournament");
|
||||
break;
|
||||
}
|
||||
statusLabel->setText(statusText);
|
||||
|
||||
// The server does not report rounds before pairing starts; avoid showing "Round 0 of 0".
|
||||
bool roundStarted = state.current_round() > 0 || state.total_rounds() > 0;
|
||||
roundLabel->setVisible(roundStarted);
|
||||
if (roundStarted) {
|
||||
roundLabel->setText(tr("Round %1 of %2").arg(state.current_round()).arg(state.total_rounds()));
|
||||
}
|
||||
|
||||
rebuildTables(state);
|
||||
resolveCurrentGameId(state);
|
||||
updateOpenMatchButton();
|
||||
}
|
||||
|
||||
void TournamentWidget::updateOpenMatchButton()
|
||||
{
|
||||
bool playingPhase = lastState.phase() == Event_TournamentState::PHASE_PLAYING;
|
||||
openMatchButton->setVisible(playingPhase);
|
||||
if (!playingPhase) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasOwnLivePairing) {
|
||||
openMatchButton->setEnabled(true);
|
||||
openMatchButton->setText(tr("Open match game"));
|
||||
openMatchButton->setToolTip(tr("Switch to your current match game"));
|
||||
} else if (localPlayerId == -1 && hasAnyLivePairing) {
|
||||
// Spectators have no pairing of their own but may watch any running match.
|
||||
openMatchButton->setEnabled(true);
|
||||
openMatchButton->setText(tr("Spectate live match"));
|
||||
openMatchButton->setToolTip(tr("Watch a match that is currently running"));
|
||||
} else {
|
||||
openMatchButton->setEnabled(false);
|
||||
openMatchButton->setText(tr("Open match game"));
|
||||
openMatchButton->setToolTip(tr("No match game is running for you right now"));
|
||||
}
|
||||
}
|
||||
|
||||
void TournamentWidget::rebuildTables(const Event_TournamentState &state)
|
||||
{
|
||||
standingsTable->setRowCount(state.players_size());
|
||||
for (int i = 0; i < state.players_size(); ++i) {
|
||||
const auto &player = state.players(i);
|
||||
|
||||
auto *nameItem = new QTableWidgetItem(QString::fromStdString(player.player_name()));
|
||||
nameItem->setToolTip(QString::fromStdString(player.player_name()));
|
||||
standingsTable->setItem(i, PLAYER_COLUMN, nameItem);
|
||||
|
||||
auto *deckItem = new QTableWidgetItem(player.deck_submitted() ? tr("Submitted") : tr("Pending"));
|
||||
deckItem->setToolTip(player.deck_submitted() ? tr("Deck submitted") : tr("Still choosing a deck"));
|
||||
standingsTable->setItem(i, DECK_COLUMN, deckItem);
|
||||
|
||||
standingsTable->setItem(i, WINS_COLUMN, new QTableWidgetItem(QString::number(player.wins())));
|
||||
standingsTable->setItem(i, LOSSES_COLUMN, new QTableWidgetItem(QString::number(player.losses())));
|
||||
standingsTable->setItem(i, DRAWS_COLUMN, new QTableWidgetItem(QString::number(player.draws())));
|
||||
}
|
||||
|
||||
pairingsTable->setRowCount(state.pairings_size());
|
||||
for (int i = 0; i < state.pairings_size(); ++i) {
|
||||
const auto &pairing = state.pairings(i);
|
||||
|
||||
QString name1 = getPlayerName(state, pairing.player1_id());
|
||||
QString name2 = pairing.player2_id() == -1 ? tr("BYE") : getPlayerName(state, pairing.player2_id());
|
||||
|
||||
QString scoreStr;
|
||||
if (getGamesPerMatch(state) > 1 && pairing.player2_id() != -1) {
|
||||
scoreStr = tr("%1 - %2").arg(pairing.player1_match_wins()).arg(pairing.player2_match_wins());
|
||||
}
|
||||
|
||||
QString statusStr;
|
||||
if (pairing.player2_id() == -1) {
|
||||
statusStr = tr("BYE");
|
||||
} else if (pairing.winner_id() != -1) {
|
||||
statusStr = tr("Finished");
|
||||
} else {
|
||||
statusStr = tr("vs");
|
||||
}
|
||||
|
||||
pairingsTable->setItem(i, PLAYER1_COLUMN, new QTableWidgetItem(name1));
|
||||
pairingsTable->setItem(i, SCORE_COLUMN, new QTableWidgetItem(scoreStr));
|
||||
pairingsTable->setItem(i, STATUS_COLUMN, new QTableWidgetItem(statusStr));
|
||||
pairingsTable->setItem(i, PLAYER2_COLUMN, new QTableWidgetItem(name2));
|
||||
}
|
||||
}
|
||||
|
||||
int TournamentWidget::getGamesPerMatch(const Event_TournamentState &state) const
|
||||
{
|
||||
return state.has_settings() ? state.settings().games_per_match() : 1;
|
||||
}
|
||||
|
||||
void TournamentWidget::resolveCurrentGameId(const Event_TournamentState &state)
|
||||
{
|
||||
currentGameId = -1;
|
||||
hasOwnLivePairing = false;
|
||||
hasAnyLivePairing = false;
|
||||
|
||||
for (int i = 0; i < state.pairings_size(); ++i) {
|
||||
const auto &pairing = state.pairings(i);
|
||||
if (pairing.game_id() == -1) {
|
||||
continue;
|
||||
}
|
||||
if (!hasOwnLivePairing && localPlayerId != -1 &&
|
||||
(pairing.player1_id() == localPlayerId || pairing.player2_id() == localPlayerId)) {
|
||||
currentGameId = pairing.game_id();
|
||||
hasOwnLivePairing = true;
|
||||
}
|
||||
if (!hasAnyLivePairing) {
|
||||
hasAnyLivePairing = true;
|
||||
if (!hasOwnLivePairing) {
|
||||
currentGameId = pairing.game_id();
|
||||
}
|
||||
}
|
||||
if (hasOwnLivePairing && hasAnyLivePairing) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
cockatrice/src/interface/widgets/draft/tournament_widget.h
Normal file
56
cockatrice/src/interface/widgets/draft/tournament_widget.h
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
#ifndef TOURNAMENT_WIDGET_H
|
||||
#define TOURNAMENT_WIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/protocol/pb/event_tournament_state.pb.h>
|
||||
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QTableWidget;
|
||||
class QTableWidgetItem;
|
||||
|
||||
/**
|
||||
* @class TournamentWidget
|
||||
* @ingroup GameViews
|
||||
* @brief Displays live standings and pairings for a tournament game.
|
||||
*
|
||||
* Updated exclusively via updateTournamentState(); the widget holds no
|
||||
* logic of its own beyond mapping protocol state to table rows. Navigation
|
||||
* decisions (which pairing belongs to the local player) are resolved here so
|
||||
* callers only need to react to openMatchGameRequested().
|
||||
*/
|
||||
class TournamentWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TournamentWidget(QWidget *parent = nullptr);
|
||||
|
||||
void updateTournamentState(const Event_TournamentState &state);
|
||||
void setLocalPlayerId(int playerId);
|
||||
void retranslateUi();
|
||||
|
||||
signals:
|
||||
/*! Emitted when the user asks to open the match game of their current pairing. */
|
||||
void openMatchGameRequested(int gameId);
|
||||
|
||||
private:
|
||||
void rebuildTables(const Event_TournamentState &state);
|
||||
void resolveCurrentGameId(const Event_TournamentState &state);
|
||||
void updateOpenMatchButton();
|
||||
[[nodiscard]] QString getPlayerName(const Event_TournamentState &state, int playerId) const;
|
||||
[[nodiscard]] int getGamesPerMatch(const Event_TournamentState &state) const;
|
||||
|
||||
QLabel *statusLabel;
|
||||
QLabel *roundLabel;
|
||||
QTableWidget *standingsTable;
|
||||
QTableWidget *pairingsTable;
|
||||
QPushButton *openMatchButton;
|
||||
|
||||
int localPlayerId = -1;
|
||||
int currentGameId = -1;
|
||||
bool hasOwnLivePairing = false;
|
||||
bool hasAnyLivePairing = false;
|
||||
Event_TournamentState lastState;
|
||||
};
|
||||
|
||||
#endif // TOURNAMENT_WIDGET_H
|
||||
|
|
@ -525,6 +525,13 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o
|
|||
connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); });
|
||||
add(games);
|
||||
|
||||
// ── Invite (only while the inviter has a joinable game for this user) ────
|
||||
if (!isSelf && online && gameInviteAvailable && gameInviteAvailable(name)) {
|
||||
auto *invite = makeBtn(tr("Invite"), tr("Invite to your game"), actionArea, theme);
|
||||
connect(invite, &QPushButton::clicked, this, [this, name] { emit inviteRequested(name); });
|
||||
add(invite);
|
||||
}
|
||||
|
||||
// ── Buddy / ignore (registered users only) ────────────────────────────────
|
||||
if (!isSelf && isReg) {
|
||||
if (isBuddy) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include <QMap>
|
||||
#include <QPixmap>
|
||||
#include <QStandardItemModel>
|
||||
#include <functional>
|
||||
#include <libcockatrice/network/server/remote/user_level.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
|
||||
|
|
@ -149,6 +150,17 @@ public:
|
|||
/** Re-pulls the avatar/card art for the currently shown user (e.g. after it loads). */
|
||||
void refreshHeader();
|
||||
|
||||
/**
|
||||
* Sets a predicate evaluated on every action-button rebuild. It receives
|
||||
* the name of the user the popup currently shows; when it returns true an
|
||||
* "Invite" button is shown. The popup itself never resolves the invite
|
||||
* link, it just forwards the request.
|
||||
*/
|
||||
void setGameInviteAvailable(std::function<bool(const QString &userName)> available)
|
||||
{
|
||||
gameInviteAvailable = std::move(available);
|
||||
}
|
||||
|
||||
signals:
|
||||
void mouseEnteredPopup();
|
||||
void mouseLeftPopup();
|
||||
|
|
@ -159,6 +171,7 @@ signals:
|
|||
|
||||
// ── Action signals — connect to UserContextMenu::exec*() ──────────────────
|
||||
void chatRequested(const QString &userName);
|
||||
void inviteRequested(const QString &userName);
|
||||
void detailsRequested(const QString &userName);
|
||||
void showGamesRequested(const QString &userName);
|
||||
void addBuddyRequested(const QString &userName);
|
||||
|
|
@ -200,6 +213,7 @@ private:
|
|||
QString currentUser;
|
||||
ServerInfo_User currentUserInfo;
|
||||
bool currentOnline = false;
|
||||
std::function<bool(const QString &userName)> gameInviteAvailable;
|
||||
|
||||
UserInfoHeaderWidget *header;
|
||||
QWidget *actionArea; ///< rebuilt per user
|
||||
|
|
|
|||
|
|
@ -345,6 +345,11 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
|||
&cardArtProvider->cache(), &cardArtParamsMap,
|
||||
window()); // parented to main window so it floats above siblings
|
||||
|
||||
// The invite availability is scoped to the room this list belongs to,
|
||||
// and gated on the room's buddy-only setting for the hovered user.
|
||||
userInfoPopup->setGameInviteAvailable(
|
||||
[this](const QString &userName) { return userContextMenu->hasGameInviteLink(userName); });
|
||||
|
||||
userInfoPopup->hide();
|
||||
userInfoPopup->setWindowOpacity(0.0);
|
||||
userInfoPopup->installEventFilter(this);
|
||||
|
|
@ -662,6 +667,8 @@ void UserListWidget::connectPopupSignals()
|
|||
|
||||
// Wire all action signals to UserContextMenu::exec*()
|
||||
connect(userInfoPopup, &UserInfoPopup::chatRequested, userContextMenu, &UserContextMenu::execChat);
|
||||
connect(userInfoPopup, &UserInfoPopup::inviteRequested, this,
|
||||
[this](const QString &userName) { userContextMenu->execInvite(userName); });
|
||||
connect(userInfoPopup, &UserInfoPopup::detailsRequested, userContextMenu, &UserContextMenu::execDetails);
|
||||
connect(userInfoPopup, &UserInfoPopup::showGamesRequested, userContextMenu, &UserContextMenu::execShowGames);
|
||||
connect(userInfoPopup, &UserInfoPopup::addBuddyRequested, userContextMenu, &UserContextMenu::execAddToBuddy);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
#include <QTextEdit>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <functional>
|
||||
#include <libcockatrice/network/server/remote/user_level.h>
|
||||
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
|
||||
|
||||
class QTreeWidget;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
#include "card_database_display_model.h"
|
||||
#include "card_database_model.h"
|
||||
#include "tab_supervisor.h"
|
||||
#include "tournament_tab_game_extension.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
|
|
@ -134,6 +135,10 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor,
|
|||
createMenuItems();
|
||||
createViewMenuItems();
|
||||
|
||||
if (game->getGameMetaInfo()->isTournament()) {
|
||||
tournamentExtension = new TournamentTabGameExtension(this);
|
||||
}
|
||||
|
||||
connectToGameState();
|
||||
connectToPlayerManager();
|
||||
connectToGameEventHandler();
|
||||
|
|
@ -150,6 +155,10 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor,
|
|||
gameTypes.append(game->getGameMetaInfo()->findRoomGameType(i));
|
||||
}
|
||||
|
||||
if (tournamentExtension) {
|
||||
tournamentExtension->initializeTournamentMode();
|
||||
}
|
||||
|
||||
QTimer::singleShot(0, this, &TabGame::loadLayout);
|
||||
}
|
||||
|
||||
|
|
@ -400,6 +409,10 @@ void TabGame::retranslateUi()
|
|||
}
|
||||
|
||||
scene->retranslateUi();
|
||||
|
||||
if (tournamentExtension) {
|
||||
tournamentExtension->retranslateUi();
|
||||
}
|
||||
}
|
||||
|
||||
void TabGame::refreshShortcuts()
|
||||
|
|
@ -928,8 +941,22 @@ void TabGame::stopGame()
|
|||
}
|
||||
}
|
||||
|
||||
bool TabGame::switchToGameTab(int gameId)
|
||||
{
|
||||
return tabSupervisor->switchToGameTabIfAlreadyExists(gameId);
|
||||
}
|
||||
|
||||
void TabGame::closeGame()
|
||||
{
|
||||
int parentId = game->getGameMetaInfo()->parentGameId();
|
||||
if (parentId >= 0) {
|
||||
if (switchToGameTab(parentId)) {
|
||||
close();
|
||||
}
|
||||
// If the parent tab is gone, fall through to the normal leave-game
|
||||
// flow instead of stranding the user on a dead sub-game tab.
|
||||
}
|
||||
|
||||
gameMenu->clear();
|
||||
gameMenu->addAction(aLeaveGame);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,11 +19,13 @@
|
|||
#include <QCompleter>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
#include <QPointer>
|
||||
#include <QStringListModel>
|
||||
|
||||
class CardMenu;
|
||||
class ServerInfo_PlayerProperties;
|
||||
class TabbedDeckViewContainer;
|
||||
class TournamentTabGameExtension;
|
||||
inline Q_LOGGING_CATEGORY(TabGameLog, "tab_game");
|
||||
|
||||
class UserListProxy;
|
||||
|
|
@ -92,6 +94,8 @@ private:
|
|||
QList<QAction *> phaseActions;
|
||||
QAction *aCardMenu;
|
||||
|
||||
QPointer<TournamentTabGameExtension> tournamentExtension;
|
||||
|
||||
/**
|
||||
* @brief The actions associated with managing a QDockWidget
|
||||
*/
|
||||
|
|
@ -189,6 +193,8 @@ public:
|
|||
void connectToGameEventHandler();
|
||||
void connectMessageLogToGameEventHandler();
|
||||
void connectPlayerListToGameEventHandler();
|
||||
/*! Brings the tab of the given game to front if it is open in this session. */
|
||||
bool switchToGameTab(int gameId);
|
||||
TabGame(TabSupervisor *_tabSupervisor, GameReplay *replay);
|
||||
~TabGame() override;
|
||||
void retranslateUi() override;
|
||||
|
|
@ -202,6 +208,19 @@ public:
|
|||
return game;
|
||||
}
|
||||
|
||||
[[nodiscard]] QStackedWidget *getMainWidget() const
|
||||
{
|
||||
return mainWidget;
|
||||
}
|
||||
[[nodiscard]] QVBoxLayout *getDeckViewContainerLayout() const
|
||||
{
|
||||
return deckViewContainerLayout;
|
||||
}
|
||||
[[nodiscard]] QWidget *getDeckViewContainerWidget() const
|
||||
{
|
||||
return deckViewContainerWidget;
|
||||
}
|
||||
|
||||
public slots:
|
||||
void viewCardInfo(const CardRef &cardRef = {}) const;
|
||||
void resetChatAndPhase();
|
||||
|
|
|
|||
|
|
@ -1007,10 +1007,20 @@ void TabSupervisor::replayLeft(TabGame *tab)
|
|||
}
|
||||
|
||||
void TabSupervisor::joinReportGame(const int gameId, const int roomId)
|
||||
{
|
||||
startSpectatorJoin(gameId, roomId, tr("Report joins are only available on a remote server."));
|
||||
}
|
||||
|
||||
void TabSupervisor::spectatorJoinGame(const int gameId, const int roomId)
|
||||
{
|
||||
startSpectatorJoin(gameId, roomId, tr("Spectating is only available on a remote server."));
|
||||
}
|
||||
|
||||
void TabSupervisor::startSpectatorJoin(const int gameId, const int roomId, const QString &unavailableMessage)
|
||||
{
|
||||
auto *remoteClient = qobject_cast<RemoteClient *>(client);
|
||||
if (!remoteClient) {
|
||||
actShowPopup(tr("Report joins are only available on a remote server."));
|
||||
actShowPopup(unavailableMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1091,7 +1101,8 @@ QList<GameInviteOption> TabSupervisor::getGameInviteLinksForRoom(int roomId) con
|
|||
// The inviter may be in several games of the same room (hosting one and
|
||||
// spectating another, for example). Return every game so the caller can
|
||||
// let the user choose which one to invite to.
|
||||
for (TabGame *tab : gameTabs) {
|
||||
for (auto it = gameTabs.cbegin(); it != gameTabs.cend(); ++it) {
|
||||
TabGame *tab = it.value();
|
||||
GameMetaInfo *metaInfo = tab->getGame()->getGameMetaInfo();
|
||||
if (metaInfo->proto().room_id() != roomId) {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -92,6 +92,8 @@ public:
|
|||
};
|
||||
|
||||
private:
|
||||
void startSpectatorJoin(int gameId, int roomId, const QString &unavailableMessage);
|
||||
|
||||
ServerInfo_User *userInfo;
|
||||
AbstractClient *client;
|
||||
UserListManager *userListManager;
|
||||
|
|
@ -189,6 +191,7 @@ public slots:
|
|||
TabEdhRec *addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander = false);
|
||||
void openReplay(GameReplay *replay);
|
||||
void joinReportGame(int gameId, int roomId);
|
||||
void spectatorJoinGame(int gameId, int roomId);
|
||||
void openTabModeration(const QString &userName = {});
|
||||
void switchToFirstAvailableNetworkTab();
|
||||
void maximizeMainWindow();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,206 @@
|
|||
#include "tournament_tab_game_extension.h"
|
||||
|
||||
#include "../../../game/game_event_handler.h"
|
||||
#include "../../../game/player/player_logic.h"
|
||||
#include "../../widgets/dialogs/dlg_tournament_settings.h"
|
||||
#include "../../widgets/draft/tournament_widget.h"
|
||||
#include "tab_game.h"
|
||||
#include "tab_supervisor.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QStackedWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/protocol/pb/command_tournament.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_tournament_state.pb.h>
|
||||
|
||||
TournamentTabGameExtension::TournamentTabGameExtension(TabGame *parent) : QObject(parent), tabGame(parent)
|
||||
{
|
||||
tournamentOverviewWidget = new QWidget(parent);
|
||||
auto *overviewLayout = new QVBoxLayout(tournamentOverviewWidget);
|
||||
|
||||
auto *headerLayout = new QHBoxLayout;
|
||||
backToGameButton = new QPushButton(tournamentOverviewWidget);
|
||||
connect(backToGameButton, &QPushButton::clicked, this, &TournamentTabGameExtension::showDeckViewPage);
|
||||
headerLayout->addWidget(backToGameButton);
|
||||
headerLayout->addStretch();
|
||||
overviewLayout->addLayout(headerLayout);
|
||||
|
||||
tournamentWidget = new TournamentWidget(tournamentOverviewWidget);
|
||||
tournamentWidget->setLocalPlayerId(parent->getGame()->getPlayerManager()->getLocalPlayerId());
|
||||
overviewLayout->addWidget(tournamentWidget);
|
||||
|
||||
parent->getMainWidget()->addWidget(tournamentOverviewWidget);
|
||||
|
||||
deckViewStatusLabel = new QLabel(parent->getDeckViewContainerWidget());
|
||||
deckViewStatusLabel->setStyleSheet("color: palette(placeholderText);");
|
||||
deckViewStatusLabel->setVisible(false);
|
||||
|
||||
standingsButton = new QPushButton(parent->getDeckViewContainerWidget());
|
||||
connect(standingsButton, &QPushButton::clicked, this, &TournamentTabGameExtension::showOverviewPage);
|
||||
standingsButton->setVisible(false);
|
||||
|
||||
connectSignals();
|
||||
}
|
||||
|
||||
bool TournamentTabGameExtension::isLocalPlayerHost() const
|
||||
{
|
||||
return tabGame->getGame()->getPlayerManager()->getLocalPlayerId() ==
|
||||
tabGame->getGame()->getGameState()->getHostId();
|
||||
}
|
||||
|
||||
void TournamentTabGameExtension::connectSignals()
|
||||
{
|
||||
auto *handler = tabGame->getGame()->getGameEventHandler();
|
||||
connect(handler, &GameEventHandler::tournamentStateChanged, this,
|
||||
&TournamentTabGameExtension::onTournamentStateChanged);
|
||||
connect(tournamentWidget, &TournamentWidget::openMatchGameRequested, this,
|
||||
&TournamentTabGameExtension::openMatchGame);
|
||||
}
|
||||
|
||||
void TournamentTabGameExtension::initializeTournamentMode()
|
||||
{
|
||||
if (!tabGame) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto *deckLayout = tabGame->getDeckViewContainerLayout();
|
||||
int index = 0;
|
||||
deckLayout->insertWidget(index++, deckViewStatusLabel);
|
||||
if (isLocalPlayerHost()) {
|
||||
settingsButton = new QPushButton(tabGame->getDeckViewContainerWidget());
|
||||
connect(settingsButton, &QPushButton::clicked, this, &TournamentTabGameExtension::showTournamentSettingsDialog);
|
||||
deckLayout->insertWidget(index++, settingsButton);
|
||||
}
|
||||
deckLayout->insertWidget(index++, standingsButton);
|
||||
deckLayout->insertSpacing(index, 4);
|
||||
}
|
||||
|
||||
void TournamentTabGameExtension::retranslateUi()
|
||||
{
|
||||
backToGameButton->setText(tr("Back to game view"));
|
||||
standingsButton->setText(tr("Tournament standings"));
|
||||
if (settingsButton) {
|
||||
settingsButton->setText(tr("Tournament Settings"));
|
||||
}
|
||||
tournamentWidget->retranslateUi();
|
||||
}
|
||||
|
||||
void TournamentTabGameExtension::updateDeckViewStrip(const Event_TournamentState &state)
|
||||
{
|
||||
int submitted = 0;
|
||||
for (int i = 0; i < state.players_size(); ++i) {
|
||||
if (state.players(i).deck_submitted()) {
|
||||
++submitted;
|
||||
}
|
||||
}
|
||||
|
||||
QString phaseText;
|
||||
switch (state.phase()) {
|
||||
case Event_TournamentState::PHASE_DECK_BUILDING:
|
||||
phaseText = tr("Deck building");
|
||||
break;
|
||||
case Event_TournamentState::PHASE_PLAYING:
|
||||
phaseText = tr("Round in progress");
|
||||
break;
|
||||
case Event_TournamentState::PHASE_FINISHED:
|
||||
phaseText = tr("Finished");
|
||||
break;
|
||||
default:
|
||||
phaseText = tr("Unknown phase");
|
||||
break;
|
||||
}
|
||||
|
||||
// The submission count only matters while players are still picking decks.
|
||||
QString text =
|
||||
state.phase() == Event_TournamentState::PHASE_DECK_BUILDING
|
||||
? tr("Tournament: %1 - %2/%3 decks submitted").arg(phaseText).arg(submitted).arg(state.players_size())
|
||||
: tr("Tournament: %1").arg(phaseText);
|
||||
deckViewStatusLabel->setText(text);
|
||||
deckViewStatusLabel->setVisible(true);
|
||||
}
|
||||
|
||||
void TournamentTabGameExtension::updateNavigationButtons(const Event_TournamentState &state)
|
||||
{
|
||||
bool showStandings =
|
||||
state.phase() == Event_TournamentState::PHASE_PLAYING || state.phase() == Event_TournamentState::PHASE_FINISHED;
|
||||
standingsButton->setVisible(showStandings);
|
||||
if (settingsButton) {
|
||||
settingsButton->setVisible(state.phase() == Event_TournamentState::PHASE_DECK_BUILDING);
|
||||
}
|
||||
}
|
||||
|
||||
void TournamentTabGameExtension::onTournamentStateChanged(const Event_TournamentState &state)
|
||||
{
|
||||
if (!tabGame) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateDeckViewStrip(state);
|
||||
updateNavigationButtons(state);
|
||||
tournamentWidget->updateTournamentState(state);
|
||||
|
||||
// Switch to the standings page only when the phase itself changes, so score
|
||||
// updates never yank the user away while they are looking at their deck.
|
||||
// Instant switches are the app-wide baseline today; motion and sound cues
|
||||
// for this transition are deferred to the Game dressing phase.
|
||||
bool overviewPhase =
|
||||
state.phase() == Event_TournamentState::PHASE_PLAYING || state.phase() == Event_TournamentState::PHASE_FINISHED;
|
||||
if (overviewPhase && (!hasLastKnownPhase || state.phase() != lastKnownPhase)) {
|
||||
tabGame->getMainWidget()->setCurrentWidget(tournamentOverviewWidget);
|
||||
}
|
||||
lastKnownPhase = state.phase();
|
||||
hasLastKnownPhase = true;
|
||||
}
|
||||
|
||||
void TournamentTabGameExtension::showOverviewPage()
|
||||
{
|
||||
if (tabGame) {
|
||||
tabGame->getMainWidget()->setCurrentWidget(tournamentOverviewWidget);
|
||||
}
|
||||
}
|
||||
|
||||
void TournamentTabGameExtension::showDeckViewPage()
|
||||
{
|
||||
if (tabGame) {
|
||||
tabGame->getMainWidget()->setCurrentWidget(tabGame->getDeckViewContainerWidget());
|
||||
}
|
||||
}
|
||||
|
||||
void TournamentTabGameExtension::showTournamentSettingsDialog()
|
||||
{
|
||||
DlgTournamentSettings dlg(tabGame);
|
||||
if (dlg.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
DlgTournamentSettingsResult result = dlg.getResult();
|
||||
|
||||
PlayerLogic *localPlayer = tabGame->getGame()->getPlayerManager()->getActiveLocalPlayer(-1);
|
||||
if (!localPlayer) {
|
||||
TabSupervisor::actShowPopup(tr("You are not an active player in this game."));
|
||||
return;
|
||||
}
|
||||
|
||||
Command_TournamentSettingsSelect cmd;
|
||||
cmd.mutable_settings()->set_games_per_match(result.gamesPerMatch);
|
||||
tabGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, localPlayer->getPlayerInfo()->getId());
|
||||
}
|
||||
|
||||
void TournamentTabGameExtension::openMatchGame(int gameId)
|
||||
{
|
||||
if (!tabGame || gameId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Players are auto-joined into their match game by the server, so the tab
|
||||
// usually already exists and switching is enough. Otherwise (e.g. after
|
||||
// leaving the match) fall back to joining as spectator.
|
||||
if (tabGame->switchToGameTab(gameId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int roomId = tabGame->getGame()->getGameMetaInfo()->proto().room_id();
|
||||
tabGame->getTabSupervisor()->spectatorJoinGame(gameId, roomId);
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
#ifndef TOURNAMENT_TAB_GAME_EXTENSION_H
|
||||
#define TOURNAMENT_TAB_GAME_EXTENSION_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <libcockatrice/protocol/pb/event_tournament_state.pb.h>
|
||||
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QWidget;
|
||||
class TournamentWidget;
|
||||
class TabGame;
|
||||
|
||||
/**
|
||||
* @class TournamentTabGameExtension
|
||||
* @ingroup GameViews
|
||||
* @brief Attaches tournament behavior to a TabGame hosting a tournament hub game.
|
||||
*
|
||||
* Owns the tournament overview page inside the tab's stacked main widget,
|
||||
* mirrors tournament state onto the always-visible deck-view status strip,
|
||||
* and routes user intents (open match game, change settings, switch pages)
|
||||
* between the two views.
|
||||
*/
|
||||
class TournamentTabGameExtension : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TournamentTabGameExtension(TabGame *parent);
|
||||
|
||||
void initializeTournamentMode();
|
||||
void retranslateUi();
|
||||
|
||||
private slots:
|
||||
void onTournamentStateChanged(const Event_TournamentState &state);
|
||||
void showTournamentSettingsDialog();
|
||||
void showOverviewPage();
|
||||
void showDeckViewPage();
|
||||
void openMatchGame(int gameId);
|
||||
|
||||
private:
|
||||
void connectSignals();
|
||||
[[nodiscard]] bool isLocalPlayerHost() const;
|
||||
void updateDeckViewStrip(const Event_TournamentState &state);
|
||||
void updateNavigationButtons(const Event_TournamentState &state);
|
||||
|
||||
QPointer<TabGame> tabGame;
|
||||
TournamentWidget *tournamentWidget = nullptr;
|
||||
QWidget *tournamentOverviewWidget = nullptr;
|
||||
QPushButton *backToGameButton = nullptr;
|
||||
QPushButton *standingsButton = nullptr;
|
||||
QPushButton *settingsButton = nullptr;
|
||||
QLabel *deckViewStatusLabel = nullptr;
|
||||
Event_TournamentState::TournamentPhase lastKnownPhase = Event_TournamentState::PHASE_DECK_BUILDING;
|
||||
bool hasLastKnownPhase = false;
|
||||
};
|
||||
|
||||
#endif // TOURNAMENT_TAB_GAME_EXTENSION_H
|
||||
|
|
@ -10,8 +10,6 @@
|
|||
#include "../visual_deck_storage_widget.h"
|
||||
#include "deck_preview_deck_tags_display_widget.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
|
|
@ -499,21 +497,6 @@ void DeckPreviewWidget::actDeleteFile()
|
|||
// The folder widget removes this preview once the row is gone.
|
||||
}
|
||||
|
||||
static bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
|
||||
{
|
||||
QFileInfo fileInfo(filePath);
|
||||
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
|
||||
|
||||
if (QFile::exists(newFileName)) {
|
||||
QMessageBox::StandardButton reply =
|
||||
QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"),
|
||||
QObject::tr("A .cod version of this deck already exists. Overwrite it?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
return reply == QMessageBox::Yes;
|
||||
}
|
||||
return true; // Safe to proceed
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the deck's file format supports tags.
|
||||
* If not, then prompt the user for file conversion.
|
||||
|
|
@ -521,45 +504,8 @@ static bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
|
|||
*/
|
||||
bool DeckPreviewWidget::promptFileConversionIfRequired()
|
||||
{
|
||||
if (DeckFileFormat::getFormatFromName(filePath) == DeckFileFormat::Cockatrice) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Retrieve saved preference if the prompt is disabled
|
||||
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) {
|
||||
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!confirmOverwriteIfExists(this, filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return DialogConvertDeckToCodFormat::promptIfRequired(this, filePath, [this] {
|
||||
model->convertToCockatriceFormat(row());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Show the dialog to the user
|
||||
DialogConvertDeckToCodFormat conversionDialog(this);
|
||||
if (conversionDialog.exec() != QDialog::Accepted) {
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(
|
||||
!conversionDialog.dontAskAgain());
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to convert file
|
||||
if (!confirmOverwriteIfExists(this, filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
model->convertToCockatriceFormat(row());
|
||||
|
||||
if (conversionDialog.dontAskAgain()) {
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false);
|
||||
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,12 @@ set(HEADERS
|
|||
game/server_deck_validation_strategy.h
|
||||
game/server_game.h
|
||||
game/server_game_lifecycle_strategy.h
|
||||
game/server_match_result_strategy.h
|
||||
game/server_match_game_factory.h
|
||||
game/server_match_result_strategy.h
|
||||
game/server_player.h
|
||||
game/server_tournament.h
|
||||
game/server_tournament_lifecycle_strategy.h
|
||||
game/server_tournament_match_result_strategy.h
|
||||
game/server_spectator.h
|
||||
server.h
|
||||
server_abstractuserinterface.h
|
||||
|
|
@ -43,6 +46,9 @@ add_library(
|
|||
game/server_game.cpp
|
||||
game/server_player.cpp
|
||||
game/server_spectator.cpp
|
||||
game/server_tournament.cpp
|
||||
game/server_tournament_lifecycle_strategy.cpp
|
||||
game/server_tournament_match_result_strategy.cpp
|
||||
server.cpp
|
||||
server_abstractuserinterface.cpp
|
||||
server_database_interface.cpp
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_tournament.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_undo_draw.pb.h>
|
||||
#include <libcockatrice/protocol/pb/context_connection_state_changed.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_game_say.pb.h>
|
||||
|
|
@ -536,6 +537,15 @@ Server_AbstractParticipant::processGameCommand(const GameCommand &command, Respo
|
|||
case GameCommand::SET_PLAYMAT:
|
||||
return cmdSetPlaymat(command.GetExtension(Command_SetPlaymat::ext), rc, ges);
|
||||
break;
|
||||
case GameCommand::REPORT_MATCH_RESULT:
|
||||
return cmdReportMatchResult(command.GetExtension(Command_ReportMatchResult::ext), rc, ges);
|
||||
break;
|
||||
case GameCommand::ADVANCE_TOURNAMENT:
|
||||
return cmdAdvanceTournament(command.GetExtension(Command_AdvanceTournament::ext), rc, ges);
|
||||
break;
|
||||
case GameCommand::TOURNAMENT_SETTINGS_SELECT:
|
||||
return cmdTournamentSettingsSelect(command.GetExtension(Command_TournamentSettingsSelect::ext), rc, ges);
|
||||
break;
|
||||
default:
|
||||
return Response::RespInvalidCommand;
|
||||
}
|
||||
|
|
@ -570,7 +580,7 @@ void Server_AbstractParticipant::setUserInterface(Server_AbstractUserInterface *
|
|||
void Server_AbstractParticipant::disconnectClient()
|
||||
{
|
||||
bool isRegistered = userInfo->user_level() & ServerInfo_User::IsRegistered;
|
||||
if (!isRegistered || spectator) {
|
||||
if (!isRegistered || spectator || game->getDisconnectRemovesPlayer()) {
|
||||
game->removeParticipant(this, Event_Leave::USER_DISCONNECTED);
|
||||
} else {
|
||||
setUserInterface(nullptr);
|
||||
|
|
@ -584,3 +594,25 @@ void Server_AbstractParticipant::getInfo(ServerInfo_Player *info,
|
|||
{
|
||||
getProperties(*info->mutable_properties(), withUserInfo);
|
||||
}
|
||||
|
||||
Response::ResponseCode Server_AbstractParticipant::cmdReportMatchResult(const Command_ReportMatchResult & /*cmd*/,
|
||||
ResponseContainer & /*rc*/,
|
||||
GameEventStorage & /*ges*/)
|
||||
{
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
Response::ResponseCode Server_AbstractParticipant::cmdAdvanceTournament(const Command_AdvanceTournament & /*cmd*/,
|
||||
ResponseContainer & /*rc*/,
|
||||
GameEventStorage & /*ges*/)
|
||||
{
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
Response::ResponseCode
|
||||
Server_AbstractParticipant::cmdTournamentSettingsSelect(const Command_TournamentSettingsSelect & /*cmd*/,
|
||||
ResponseContainer & /*rc*/,
|
||||
GameEventStorage & /*ges*/)
|
||||
{
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ class Command_DeckSelect;
|
|||
class Command_SetSideboardLock;
|
||||
class Command_ChangeZoneProperties;
|
||||
class Command_SetPlaymat;
|
||||
class Command_ReportMatchResult;
|
||||
class Command_AdvanceTournament;
|
||||
class Command_TournamentSettingsSelect;
|
||||
|
||||
class Server_AbstractParticipant : public Server_ArrowTarget, public ServerInfo_User_Container
|
||||
{
|
||||
|
|
@ -175,6 +178,13 @@ public:
|
|||
cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode
|
||||
cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, ResponseContainer &rc, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode
|
||||
cmdReportMatchResult(const Command_ReportMatchResult &cmd, ResponseContainer &rc, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode
|
||||
cmdAdvanceTournament(const Command_AdvanceTournament &cmd, ResponseContainer &rc, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode cmdTournamentSettingsSelect(const Command_TournamentSettingsSelect &cmd,
|
||||
ResponseContainer &rc,
|
||||
GameEventStorage &ges);
|
||||
|
||||
Response::ResponseCode processGameCommand(const GameCommand &command, ResponseContainer &rc, GameEventStorage &ges);
|
||||
void sendGameEvent(const GameEventContainer &event);
|
||||
|
|
|
|||
|
|
@ -1663,3 +1663,9 @@ void Server_AbstractPlayer::getPlayerProperties(ServerInfo_PlayerProperties &res
|
|||
playmatParams->set_zoom(playmat.params.zoom);
|
||||
}
|
||||
}
|
||||
|
||||
void Server_AbstractPlayer::setDeck(DeckList *_deck)
|
||||
{
|
||||
delete deck;
|
||||
deck = _deck;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ public:
|
|||
{
|
||||
return deck;
|
||||
}
|
||||
void setDeck(DeckList *_deck);
|
||||
bool getReadyStart() const
|
||||
{
|
||||
return readyStart;
|
||||
|
|
|
|||
|
|
@ -30,10 +30,14 @@
|
|||
#include "server_cardzone.h"
|
||||
#include "server_player.h"
|
||||
#include "server_spectator.h"
|
||||
#include "server_tournament.h"
|
||||
#include "server_tournament_lifecycle_strategy.h"
|
||||
#include "server_tournament_match_result_strategy.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QRegularExpression>
|
||||
#include <QTimer>
|
||||
#include <algorithm>
|
||||
#include <google/protobuf/descriptor.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/protocol/pb/context_connection_state_changed.pb.h>
|
||||
|
|
@ -62,8 +66,9 @@ Server_Game::Server_Game(const GameConfig &config, Server_Room *_room)
|
|||
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),
|
||||
deckValidationStrategy(new Server_DefaultDeckValidationStrategy),
|
||||
turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr), isTournament(false),
|
||||
tournament(nullptr), tournamentParentGame(nullptr), tournamentMatchPlayer1Id(-1), tournamentMatchPlayer2Id(-1),
|
||||
disconnectRemovesPlayer(false), deckValidationStrategy(new Server_DefaultDeckValidationStrategy),
|
||||
lifecycleStrategy(new Server_DefaultLifecycleStrategy), matchResultStrategy(new Server_NullMatchResultStrategy),
|
||||
gameMutex()
|
||||
{
|
||||
|
|
@ -265,6 +270,12 @@ void Server_Game::createGameStateChangedEvent(Event_GameStateChanged *event,
|
|||
event->set_game_started(false);
|
||||
}
|
||||
|
||||
event->set_is_tournament(isTournament);
|
||||
|
||||
if (tournamentParentGame) {
|
||||
event->set_parent_game_id(tournamentParentGame->getGameId());
|
||||
}
|
||||
|
||||
for (Server_AbstractParticipant *participant : participants.values()) {
|
||||
participant->getInfo(event->add_player_list(), recipient, omniscient, withUserInfo);
|
||||
}
|
||||
|
|
@ -313,7 +324,7 @@ void Server_Game::doStartGameIfReady(bool forceStartGame)
|
|||
Server_DatabaseInterface *databaseInterface = room->getServer()->getDatabaseInterface();
|
||||
QMutexLocker locker(&gameMutex);
|
||||
|
||||
if (getPlayerCount() < maxPlayers && !forceStartGame) {
|
||||
if (!isTournament && getPlayerCount() < maxPlayers && !forceStartGame) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -861,6 +872,7 @@ void Server_Game::getInfo(ServerInfo_Game &result) const
|
|||
result.set_share_decklists_on_load(shareDecklistsOnLoad);
|
||||
result.set_spectators_count(getSpectatorCount());
|
||||
result.set_start_time(startTime.toSecsSinceEpoch());
|
||||
result.set_is_tournament(isTournament);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -911,3 +923,86 @@ void Server_Game::setDeckValidationStrategy(Server_DeckValidationStrategy *strat
|
|||
{
|
||||
deckValidationStrategy.reset(strategy);
|
||||
}
|
||||
|
||||
void Server_Game::setMatchResultStrategy(Server_MatchResultStrategy *strategy)
|
||||
{
|
||||
matchResultStrategy.reset(strategy);
|
||||
}
|
||||
|
||||
void Server_Game::setIsTournamentGame(bool _isTournament)
|
||||
{
|
||||
isTournament = _isTournament;
|
||||
if (isTournament) {
|
||||
tournament = new Server_Tournament(this, this, this);
|
||||
lifecycleStrategy.reset(new Server_TournamentLifecycleStrategy);
|
||||
matchResultStrategy.reset(new Server_TournamentMatchResultStrategy);
|
||||
} else if (tournament) {
|
||||
delete tournament;
|
||||
tournament = nullptr;
|
||||
lifecycleStrategy.reset(new Server_DefaultLifecycleStrategy);
|
||||
matchResultStrategy.reset(new Server_NullMatchResultStrategy);
|
||||
}
|
||||
}
|
||||
|
||||
void Server_Game::startTournament()
|
||||
{
|
||||
if (!tournament) {
|
||||
tournament = new Server_Tournament(this, this, this);
|
||||
}
|
||||
|
||||
if (!tournament->isStarted()) {
|
||||
// Add all current players to the tournament
|
||||
auto players = getPlayers();
|
||||
for (auto *player : players.values()) {
|
||||
tournament->addPlayer(player->getPlayerId(), QString::fromStdString(player->getUserInfo()->name()));
|
||||
}
|
||||
|
||||
tournament->startTournament();
|
||||
}
|
||||
|
||||
GameEventStorage ges;
|
||||
tournament->broadcastTournamentState(ges);
|
||||
ges.sendToGame(this);
|
||||
}
|
||||
|
||||
void Server_Game::setPlayerTournamentDeck(int playerId, DeckList *deck)
|
||||
{
|
||||
if (tournament) {
|
||||
tournament->setPlayerDeck(playerId, deck);
|
||||
}
|
||||
}
|
||||
|
||||
void Server_Game::setTournamentMatchInfo(Server_Game *parentGame, int p1Id, int p2Id)
|
||||
{
|
||||
tournamentParentGame = parentGame;
|
||||
tournamentMatchPlayer1Id = p1Id;
|
||||
tournamentMatchPlayer2Id = p2Id;
|
||||
}
|
||||
|
||||
Server_Game *Server_Game::createMatchGame(const GameConfig &config, int &outGameId)
|
||||
{
|
||||
Server_DatabaseInterface *databaseInterface = room->getServer()->getDatabaseInterface();
|
||||
outGameId = databaseInterface->getNextGameId();
|
||||
if (outGameId == -1) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
GameConfig matchConfig = config;
|
||||
matchConfig.gameId = outGameId;
|
||||
auto *game = new Server_Game(matchConfig, room);
|
||||
// Sub-games carry the tournament flag (for protocol fields) but keep the default
|
||||
// strategies; the parent tournament drives them through the match result strategy
|
||||
// installed by Server_Tournament::createMatchGame.
|
||||
game->isTournament = true;
|
||||
return game;
|
||||
}
|
||||
|
||||
Server_AbstractUserInterface *Server_Game::getUserInterface(const QString &playerName)
|
||||
{
|
||||
return room->getUserInterfaceByName(playerName);
|
||||
}
|
||||
|
||||
void Server_Game::addGameToRoom(Server_Game *game)
|
||||
{
|
||||
room->addGame(game);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
#include "game_config.h"
|
||||
#include "server_deck_validation_strategy.h"
|
||||
#include "server_game_lifecycle_strategy.h"
|
||||
#include "server_match_game_factory.h"
|
||||
#include "server_match_result_strategy.h"
|
||||
|
||||
#include <QDateTime>
|
||||
|
|
@ -34,21 +35,26 @@
|
|||
#include <QSet>
|
||||
#include <QStringList>
|
||||
#include <libcockatrice/protocol/pb/event_leave.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_tournament_state.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
|
||||
|
||||
class QTimer;
|
||||
class DeckList;
|
||||
class GameEventContainer;
|
||||
class GameEventStorage;
|
||||
class GameReplay;
|
||||
class Server_Room;
|
||||
class Server_AbstractPlayer;
|
||||
class Server_AbstractParticipant;
|
||||
class Server_Card;
|
||||
class Server_Tournament;
|
||||
class ServerInfo_User;
|
||||
class ServerInfo_Game;
|
||||
class Server_AbstractUserInterface;
|
||||
class Event_GameStateChanged;
|
||||
|
||||
class Server_Game : public QObject
|
||||
class Server_Game : public QObject, public Server_MatchGameFactory
|
||||
{
|
||||
Q_OBJECT
|
||||
private:
|
||||
|
|
@ -83,6 +89,14 @@ private:
|
|||
QList<GameReplay *> replayList;
|
||||
GameReplay *currentReplay;
|
||||
|
||||
bool isTournament;
|
||||
TournamentSettings tournamentSettings;
|
||||
Server_Tournament *tournament;
|
||||
Server_Game *tournamentParentGame;
|
||||
int tournamentMatchPlayer1Id;
|
||||
int tournamentMatchPlayer2Id;
|
||||
bool disconnectRemovesPlayer;
|
||||
|
||||
QScopedPointer<Server_DeckValidationStrategy> deckValidationStrategy;
|
||||
|
||||
QScopedPointer<Server_GameLifecycleStrategy> lifecycleStrategy;
|
||||
|
|
@ -220,6 +234,45 @@ public:
|
|||
void returnCardsFromPlayer(GameEventStorage &ges, Server_AbstractPlayer *player);
|
||||
|
||||
/** @brief Get the current deck validation strategy (non-owning). */
|
||||
bool getIsTournamentGame() const
|
||||
{
|
||||
return isTournament;
|
||||
}
|
||||
void setIsTournamentGame(bool _isTournament);
|
||||
bool getIsTournament() const
|
||||
{
|
||||
return tournament != nullptr;
|
||||
}
|
||||
Server_Tournament *getTournament() const
|
||||
{
|
||||
return tournament;
|
||||
}
|
||||
void startTournament();
|
||||
void setPlayerTournamentDeck(int playerId, DeckList *deck);
|
||||
void setTournamentMatchInfo(Server_Game *parentGame, int p1Id, int p2Id);
|
||||
Server_Game *getTournamentParentGame() const
|
||||
{
|
||||
return tournamentParentGame;
|
||||
}
|
||||
bool getDisconnectRemovesPlayer() const
|
||||
{
|
||||
return disconnectRemovesPlayer;
|
||||
}
|
||||
|
||||
// Server_MatchGameFactory implementation
|
||||
Server_Game *createMatchGame(const GameConfig &config, int &outGameId) override;
|
||||
Server_AbstractUserInterface *getUserInterface(const QString &playerName) override;
|
||||
void addGameToRoom(Server_Game *game) override;
|
||||
|
||||
const TournamentSettings &getTournamentSettings() const
|
||||
{
|
||||
return tournamentSettings;
|
||||
}
|
||||
void setTournamentSettings(const TournamentSettings &settings)
|
||||
{
|
||||
tournamentSettings = settings;
|
||||
}
|
||||
|
||||
Server_DeckValidationStrategy *getDeckValidationStrategy() const
|
||||
{
|
||||
return deckValidationStrategy.data();
|
||||
|
|
@ -232,6 +285,8 @@ public:
|
|||
{
|
||||
return lifecycleStrategy.data();
|
||||
}
|
||||
/** @brief Replace the match result strategy; takes ownership of @p strategy. */
|
||||
void setMatchResultStrategy(Server_MatchResultStrategy *strategy);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -0,0 +1,556 @@
|
|||
#include "server_tournament.h"
|
||||
|
||||
#include "../server_abstractuserinterface.h"
|
||||
#include "../server_response_containers.h"
|
||||
#include "../serverinfo_user_container.h"
|
||||
#include "game_config.h"
|
||||
#include "server_abstract_player.h"
|
||||
#include "server_game.h"
|
||||
#include "server_match_game_factory.h"
|
||||
#include "server_player.h"
|
||||
#include "server_tournament_match_result_strategy.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <algorithm>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/protocol/pb/event_tournament_state.pb.h>
|
||||
#include <libcockatrice/protocol/pb/game_event_container.pb.h>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(TournamentLog, "tournament");
|
||||
|
||||
Server_Tournament::Server_Tournament(Server_Game *_parentGame, Server_MatchGameFactory *_factory, QObject *parent)
|
||||
: QObject(parent), parentGame(_parentGame), matchGameFactory(_factory), currentRound(0), totalRounds(0),
|
||||
started(false)
|
||||
{
|
||||
}
|
||||
|
||||
Server_Tournament::~Server_Tournament()
|
||||
{
|
||||
qDeleteAll(submittedDecks);
|
||||
}
|
||||
|
||||
void Server_Tournament::addPlayer(int playerId, const QString &playerName)
|
||||
{
|
||||
TournamentPlayerData data;
|
||||
data.playerId = playerId;
|
||||
data.playerName = playerName;
|
||||
players[playerId] = data;
|
||||
}
|
||||
|
||||
void Server_Tournament::setPlayerDeck(int playerId, DeckList *deck)
|
||||
{
|
||||
QMutexLocker locker(&tournamentMutex);
|
||||
delete submittedDecks.value(playerId, nullptr);
|
||||
submittedDecks[playerId] = deck;
|
||||
if (players.contains(playerId)) {
|
||||
players[playerId].deckSubmitted = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Server_Tournament::removePlayer(int playerId)
|
||||
{
|
||||
QMutexLocker locker(&tournamentMutex);
|
||||
players.remove(playerId);
|
||||
}
|
||||
|
||||
void Server_Tournament::startTournament()
|
||||
{
|
||||
{
|
||||
QMutexLocker locker(&tournamentMutex);
|
||||
if (started) {
|
||||
return;
|
||||
}
|
||||
totalRounds = calculateTotalRounds();
|
||||
started = true;
|
||||
currentRound = 0;
|
||||
generateSwissPairings();
|
||||
}
|
||||
|
||||
// Spawn the first round's match games
|
||||
enqueueMatchGameCreation();
|
||||
}
|
||||
|
||||
bool Server_Tournament::isAllDecksSubmitted() const
|
||||
{
|
||||
QMutexLocker locker(&tournamentMutex);
|
||||
for (auto it = players.constBegin(); it != players.constEnd(); ++it) {
|
||||
if (!it->deckSubmitted) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int Server_Tournament::getTournamentPlayerIdByName(const QString &name) const
|
||||
{
|
||||
QMutexLocker locker(&tournamentMutex);
|
||||
for (auto it = players.constBegin(); it != players.constEnd(); ++it) {
|
||||
if (it->playerName == name) {
|
||||
return it->playerId;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void Server_Tournament::generateSwissPairings()
|
||||
{
|
||||
currentPairings.clear();
|
||||
QList<int> available;
|
||||
for (auto it = players.constBegin(); it != players.constEnd(); ++it) {
|
||||
available.append(it->playerId);
|
||||
}
|
||||
|
||||
// Sort by wins descending (and by record for tie-breaking)
|
||||
std::sort(available.begin(), available.end(), [this](int a, int b) {
|
||||
const auto &pa = players[a];
|
||||
const auto &pb = players[b];
|
||||
if (pa.wins != pb.wins) {
|
||||
return pa.wins > pb.wins;
|
||||
}
|
||||
if (pa.losses != pb.losses) {
|
||||
return pa.losses < pb.losses;
|
||||
}
|
||||
return a < b;
|
||||
});
|
||||
|
||||
// Simple greedy Swiss pairing
|
||||
QSet<int> paired;
|
||||
for (int i = 0; i < available.size(); ++i) {
|
||||
if (paired.contains(available[i])) {
|
||||
continue;
|
||||
}
|
||||
for (int j = i + 1; j < available.size(); ++j) {
|
||||
if (paired.contains(available[j])) {
|
||||
continue;
|
||||
}
|
||||
if (!havePlayed(available[i], available[j])) {
|
||||
TournamentPairingData pairing;
|
||||
pairing.player1Id = available[i];
|
||||
pairing.player2Id = available[j];
|
||||
currentPairings.append(pairing);
|
||||
paired.insert(available[i]);
|
||||
paired.insert(available[j]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bye for unpaired player if odd count
|
||||
for (int i = 0; i < available.size(); ++i) {
|
||||
if (!paired.contains(available[i])) {
|
||||
// Player gets a bye (auto-win)
|
||||
TournamentPairingData bye;
|
||||
bye.player1Id = available[i];
|
||||
bye.player2Id = -1;
|
||||
bye.winnerId = available[i];
|
||||
bye.player1MatchWins = gamesPerMatch; // Match immediately decided
|
||||
currentPairings.append(bye);
|
||||
players[available[i]].wins += 1;
|
||||
allPreviousPairings.append(qMakePair(available[i], -1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int Server_Tournament::calculateTotalRounds() const
|
||||
{
|
||||
int n = players.size();
|
||||
if (n <= 1) {
|
||||
return 0;
|
||||
}
|
||||
// Standard Swiss rounds: ceil(log2(n))
|
||||
int rounds = 0;
|
||||
while ((1 << rounds) < n) {
|
||||
++rounds;
|
||||
}
|
||||
return rounds;
|
||||
}
|
||||
|
||||
bool Server_Tournament::havePlayed(int p1, int p2) const
|
||||
{
|
||||
for (const auto &pair : allPreviousPairings) {
|
||||
if ((pair.first == p1 && pair.second == p2) || (pair.first == p2 && pair.second == p1)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Server_Tournament::advanceRound(GameEventStorage &ges)
|
||||
{
|
||||
{
|
||||
QMutexLocker locker(&tournamentMutex);
|
||||
++currentRound;
|
||||
if (currentRound >= totalRounds) {
|
||||
broadcastTournamentState(ges);
|
||||
return;
|
||||
}
|
||||
generateSwissPairings();
|
||||
}
|
||||
|
||||
enqueueMatchGameCreation();
|
||||
broadcastTournamentState(ges);
|
||||
}
|
||||
|
||||
bool Server_Tournament::allPairingsDecided() const
|
||||
{
|
||||
for (const auto &pairing : currentPairings) {
|
||||
if (pairing.winnerId == -2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Server_Tournament::enqueueMatchGameCreation()
|
||||
{
|
||||
QList<QPair<int, int>> planned;
|
||||
{
|
||||
QMutexLocker locker(&tournamentMutex);
|
||||
for (const auto &pairing : currentPairings) {
|
||||
if (pairing.player2Id != -1 && pairing.winnerId == -2 &&
|
||||
pairing.matchGameIds.size() < static_cast<int>(gamesPerMatch)) {
|
||||
planned.append(qMakePair(pairing.player1Id, pairing.player2Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (planned.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the games from the event loop instead of the caller's stack: command
|
||||
// processing holds game mutexes, and room registration takes gamesLock, so spawning
|
||||
// synchronously would nest lock orders. The queued job runs once this object's
|
||||
// owning thread returns to its event loop with no locks held; it is dropped if this
|
||||
// tournament is destroyed first.
|
||||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
[this, planned] {
|
||||
for (const auto &pair : planned) {
|
||||
createMatchGame(pair.first, pair.second);
|
||||
}
|
||||
|
||||
GameEventStorage ges;
|
||||
broadcastTournamentState(ges);
|
||||
ges.sendToGame(parentGame);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void Server_Tournament::createMatchGame(int player1Id, int player2Id)
|
||||
{
|
||||
if (!matchGameFactory || player2Id == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString player1Name;
|
||||
QString player2Name;
|
||||
QString deck1Native;
|
||||
QString deck2Native;
|
||||
int round = 0;
|
||||
int gameNumber = 1;
|
||||
{
|
||||
QMutexLocker locker(&tournamentMutex);
|
||||
player1Name = players.value(player1Id).playerName;
|
||||
player2Name = players.value(player2Id).playerName;
|
||||
|
||||
if (submittedDecks.contains(player1Id)) {
|
||||
deck1Native = submittedDecks.value(player1Id)->writeToString_Native();
|
||||
}
|
||||
if (submittedDecks.contains(player2Id)) {
|
||||
deck2Native = submittedDecks.value(player2Id)->writeToString_Native();
|
||||
}
|
||||
|
||||
round = currentRound;
|
||||
for (const auto &pairing : currentPairings) {
|
||||
if (pairing.player1Id == player1Id && pairing.player2Id == player2Id) {
|
||||
gameNumber = pairing.matchGameIds.size() + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Defense in depth: never exceed the configured series length
|
||||
for (const auto &pairing : currentPairings) {
|
||||
if (pairing.player1Id == player1Id && pairing.player2Id == player2Id &&
|
||||
pairing.matchGameIds.size() >= static_cast<int>(gamesPerMatch)) {
|
||||
qCWarning(TournamentLog) << "Refusing to exceed series length for pairing" << player1Id << player2Id;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a sub-game for this match via the factory
|
||||
ServerInfo_User creatorInfo;
|
||||
creatorInfo.set_name(player1Name.toStdString());
|
||||
creatorInfo.set_user_level(ServerInfo_User::IsAdmin | ServerInfo_User::IsRegistered);
|
||||
|
||||
QString gameDesc = gamesPerMatch > 1
|
||||
? QString("R%1 Match - Game %2 of %3").arg(round).arg(gameNumber).arg(gamesPerMatch)
|
||||
: QString("Tournament Round %1").arg(round);
|
||||
|
||||
GameConfig matchConfig;
|
||||
matchConfig.creatorInfo = creatorInfo;
|
||||
matchConfig.description = gameDesc;
|
||||
matchConfig.maxPlayers = 2;
|
||||
matchConfig.startingLifeTotal = parentGame->getStartingLifeTotal();
|
||||
|
||||
int matchGameId = -1;
|
||||
auto *matchGame = matchGameFactory->createMatchGame(matchConfig, matchGameId);
|
||||
if (!matchGame || matchGameId == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
matchGame->setTournamentMatchInfo(parentGame, player1Id, player2Id);
|
||||
matchGame->setMatchResultStrategy(new Server_TournamentMatchResultStrategy);
|
||||
matchGameFactory->addGameToRoom(matchGame);
|
||||
|
||||
// Store the game ID in the pairing
|
||||
{
|
||||
QMutexLocker locker(&tournamentMutex);
|
||||
for (auto &pairing : currentPairings) {
|
||||
if (pairing.player1Id == player1Id && pairing.player2Id == player2Id) {
|
||||
pairing.gameId = matchGameId;
|
||||
pairing.matchGameIds.append(matchGameId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-join both players, sending the join event directly through their UIs.
|
||||
QMap<int, QPair<Server_AbstractUserInterface *, ResponseContainer *>> joiners;
|
||||
|
||||
auto joinAndSetupPlayer = [&](int pid, const QString &name) {
|
||||
Server_AbstractUserInterface *ui = matchGameFactory->getUserInterface(name);
|
||||
if (ui) {
|
||||
auto *rc = new ResponseContainer(0);
|
||||
matchGame->addPlayer(ui, *rc, false, false, false);
|
||||
joiners[pid] = qMakePair(ui, rc);
|
||||
}
|
||||
};
|
||||
|
||||
joinAndSetupPlayer(player1Id, player1Name);
|
||||
joinAndSetupPlayer(player2Id, player2Name);
|
||||
|
||||
// Now send the enqueued GameJoined + GameStateChanged events to each player's client.
|
||||
for (auto it = joiners.constBegin(); it != joiners.constEnd(); ++it) {
|
||||
it.value().first->sendResponseContainer(*it.value().second, Response::RespNothing);
|
||||
delete it.value().second;
|
||||
}
|
||||
joiners.clear();
|
||||
|
||||
// Set decks and mark players as ready in the match game
|
||||
auto matchPlayers = matchGame->getPlayers();
|
||||
for (auto *matchPlayer : matchPlayers) {
|
||||
const QString name = QString::fromStdString(matchPlayer->getUserInfo()->name());
|
||||
QString deckNative;
|
||||
if (name == player1Name) {
|
||||
deckNative = deck1Native;
|
||||
} else if (name == player2Name) {
|
||||
deckNative = deck2Native;
|
||||
}
|
||||
|
||||
if (!deckNative.isEmpty()) {
|
||||
matchPlayer->setDeck(new DeckList(deckNative));
|
||||
matchPlayer->setReadyStart(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Start the match game
|
||||
matchGame->startGameIfReady(true);
|
||||
}
|
||||
|
||||
void Server_Tournament::recordMatchResult(int playerId1, int playerId2, int winnerId, GameEventStorage &ges)
|
||||
{
|
||||
QMutexLocker locker(&tournamentMutex);
|
||||
|
||||
// Find the pairing and set the winner
|
||||
for (auto &pairing : currentPairings) {
|
||||
if ((pairing.player1Id == playerId1 && pairing.player2Id == playerId2) ||
|
||||
(pairing.player1Id == playerId2 && pairing.player2Id == playerId1)) {
|
||||
if (pairing.winnerId != -2) {
|
||||
return; // Already recorded — defense in depth against double-call
|
||||
}
|
||||
pairing.winnerId = winnerId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update player records
|
||||
if (winnerId == -1) {
|
||||
// Draw
|
||||
players[playerId1].draws += 1;
|
||||
players[playerId2].draws += 1;
|
||||
} else if (winnerId == playerId1) {
|
||||
players[playerId1].wins += 1;
|
||||
players[playerId2].losses += 1;
|
||||
} else if (winnerId == playerId2) {
|
||||
players[playerId2].wins += 1;
|
||||
players[playerId1].losses += 1;
|
||||
}
|
||||
|
||||
// Store for future pairing avoidance
|
||||
allPreviousPairings.append(qMakePair(playerId1, playerId2));
|
||||
|
||||
// Check if all pairings in current round have results
|
||||
bool allDecided = true;
|
||||
for (const auto &pairing : currentPairings) {
|
||||
if (pairing.winnerId == -2) {
|
||||
allDecided = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
broadcastTournamentState(ges);
|
||||
|
||||
if (allDecided) {
|
||||
advanceRound(ges);
|
||||
}
|
||||
}
|
||||
|
||||
bool Server_Tournament::recordMatchResultByGameId(int gameId, int winnerId, GameEventStorage &ges)
|
||||
{
|
||||
bool matchDecided = false;
|
||||
bool seriesContinues = false;
|
||||
int p1 = -1;
|
||||
int p2 = -1;
|
||||
{
|
||||
QMutexLocker locker(&tournamentMutex);
|
||||
|
||||
// Find the pairing that owns this game
|
||||
TournamentPairingData *pairingPtr = nullptr;
|
||||
for (auto &pairing : currentPairings) {
|
||||
if (pairing.matchGameIds.contains(gameId)) {
|
||||
pairingPtr = &pairing;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pairingPtr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the match is already decided, ignore further sub-game results
|
||||
if (pairingPtr->winnerId != -2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Increment per-match wins
|
||||
if (winnerId == pairingPtr->player1Id) {
|
||||
pairingPtr->player1MatchWins += 1;
|
||||
} else if (winnerId == pairingPtr->player2Id) {
|
||||
pairingPtr->player2MatchWins += 1;
|
||||
}
|
||||
// Draw (winnerId == -1): no match wins incremented
|
||||
|
||||
// Check if match is decided
|
||||
const int gamesNeeded = static_cast<int>(gamesPerMatch);
|
||||
matchDecided = (pairingPtr->player1MatchWins >= gamesNeeded) || (pairingPtr->player2MatchWins >= gamesNeeded);
|
||||
|
||||
if (matchDecided) {
|
||||
// Determine match winner
|
||||
int matchWinnerId;
|
||||
if (pairingPtr->player1MatchWins >= gamesNeeded) {
|
||||
matchWinnerId = pairingPtr->player1Id;
|
||||
} else {
|
||||
matchWinnerId = pairingPtr->player2Id;
|
||||
}
|
||||
|
||||
// Set the match winner on the pairing
|
||||
pairingPtr->winnerId = matchWinnerId;
|
||||
|
||||
// Update tournament-level player records
|
||||
if (matchWinnerId == pairingPtr->player1Id) {
|
||||
players[pairingPtr->player1Id].wins += 1;
|
||||
players[pairingPtr->player2Id].losses += 1;
|
||||
} else {
|
||||
players[pairingPtr->player2Id].wins += 1;
|
||||
players[pairingPtr->player1Id].losses += 1;
|
||||
}
|
||||
|
||||
// Store for future pairing avoidance
|
||||
allPreviousPairings.append(qMakePair(pairingPtr->player1Id, pairingPtr->player2Id));
|
||||
} else {
|
||||
// Match not decided — spawn the next sub-game outside all locks
|
||||
seriesContinues = true;
|
||||
p1 = pairingPtr->player1Id;
|
||||
p2 = pairingPtr->player2Id;
|
||||
}
|
||||
|
||||
broadcastTournamentState(ges);
|
||||
}
|
||||
|
||||
if (seriesContinues) {
|
||||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
[this, p1, p2] {
|
||||
createMatchGame(p1, p2);
|
||||
|
||||
GameEventStorage nextGes;
|
||||
broadcastTournamentState(nextGes);
|
||||
nextGes.sendToGame(parentGame);
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
checkAndAdvanceRound(ges);
|
||||
|
||||
return matchDecided;
|
||||
}
|
||||
|
||||
void Server_Tournament::checkAndAdvanceRound(GameEventStorage &ges)
|
||||
{
|
||||
bool roundComplete = false;
|
||||
{
|
||||
QMutexLocker locker(&tournamentMutex);
|
||||
roundComplete = allPairingsDecided();
|
||||
}
|
||||
|
||||
if (roundComplete) {
|
||||
advanceRound(ges);
|
||||
}
|
||||
}
|
||||
|
||||
void Server_Tournament::broadcastTournamentState(GameEventStorage &ges)
|
||||
{
|
||||
QMutexLocker locker(&tournamentMutex);
|
||||
|
||||
Event_TournamentState state;
|
||||
|
||||
if (started && currentRound >= totalRounds) {
|
||||
state.set_phase(Event_TournamentState::PHASE_FINISHED);
|
||||
} else if (started) {
|
||||
state.set_phase(Event_TournamentState::PHASE_PLAYING);
|
||||
} else {
|
||||
state.set_phase(Event_TournamentState::PHASE_DECK_BUILDING);
|
||||
}
|
||||
|
||||
state.set_current_round(currentRound);
|
||||
state.set_total_rounds(totalRounds);
|
||||
|
||||
// Settings
|
||||
TournamentSettings *settings = state.mutable_settings();
|
||||
settings->set_games_per_match(gamesPerMatch);
|
||||
|
||||
for (auto it = players.constBegin(); it != players.constEnd(); ++it) {
|
||||
TournamentPlayer *p = state.add_players();
|
||||
p->set_player_id(it->playerId);
|
||||
p->set_player_name(it->playerName.toStdString());
|
||||
p->set_wins(it->wins);
|
||||
p->set_losses(it->losses);
|
||||
p->set_draws(it->draws);
|
||||
p->set_deck_submitted(it->deckSubmitted);
|
||||
}
|
||||
|
||||
for (const auto &pairing : currentPairings) {
|
||||
TournamentPairing *p = state.add_pairings();
|
||||
p->set_player1_id(pairing.player1Id);
|
||||
p->set_player2_id(pairing.player2Id);
|
||||
p->set_game_id(pairing.gameId);
|
||||
// Map internal sentinel: -2 (undecided) -> -1 (no winner yet in proto)
|
||||
p->set_winner_id(pairing.winnerId == -2 ? -1 : pairing.winnerId);
|
||||
p->set_player1_match_wins(pairing.player1MatchWins);
|
||||
p->set_player2_match_wins(pairing.player2MatchWins);
|
||||
}
|
||||
|
||||
ges.enqueueGameEvent(state, -1);
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
#ifndef SERVER_TOURNAMENT_H
|
||||
#define SERVER_TOURNAMENT_H
|
||||
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
#include <QRecursiveMutex>
|
||||
#include <QSet>
|
||||
#include <libcockatrice/protocol/pb/event_tournament_state.pb.h>
|
||||
|
||||
class DeckList;
|
||||
class Server_Game;
|
||||
class Server_MatchGameFactory;
|
||||
class Server_AbstractParticipant;
|
||||
class Server_AbstractUserInterface;
|
||||
class GameEventStorage;
|
||||
|
||||
/** @brief Maximum number of games per match a tournament can be configured with. */
|
||||
constexpr int MAX_GAMES_PER_MATCH = 5;
|
||||
|
||||
class Server_Tournament : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit Server_Tournament(Server_Game *_parentGame, Server_MatchGameFactory *_factory, QObject *parent = nullptr);
|
||||
~Server_Tournament() override;
|
||||
|
||||
void addPlayer(int playerId, const QString &playerName);
|
||||
void removePlayer(int playerId);
|
||||
void startTournament();
|
||||
void advanceRound(GameEventStorage &ges);
|
||||
void recordMatchResult(int playerId1, int playerId2, int winnerId, GameEventStorage &ges);
|
||||
bool recordMatchResultByGameId(int gameId, int winnerId, GameEventStorage &ges);
|
||||
void broadcastTournamentState(GameEventStorage &ges);
|
||||
|
||||
bool isStarted() const
|
||||
{
|
||||
return started;
|
||||
}
|
||||
bool isAllDecksSubmitted() const;
|
||||
int getPlayerCount() const
|
||||
{
|
||||
return players.size();
|
||||
}
|
||||
int getTournamentPlayerIdByName(const QString &name) const;
|
||||
void setPlayerDeckSubmitted(int playerId)
|
||||
{
|
||||
if (players.contains(playerId)) {
|
||||
players[playerId].deckSubmitted = true;
|
||||
}
|
||||
}
|
||||
void setPlayerDeck(int playerId, DeckList *deck);
|
||||
|
||||
void setGamesPerMatch(uint32_t n)
|
||||
{
|
||||
gamesPerMatch = n;
|
||||
}
|
||||
uint32_t getGamesPerMatch() const
|
||||
{
|
||||
return gamesPerMatch;
|
||||
}
|
||||
|
||||
struct TournamentPlayerData
|
||||
{
|
||||
int playerId;
|
||||
QString playerName;
|
||||
int wins = 0;
|
||||
int losses = 0;
|
||||
int draws = 0;
|
||||
bool deckSubmitted = false;
|
||||
};
|
||||
|
||||
struct TournamentPairingData
|
||||
{
|
||||
int player1Id;
|
||||
int player2Id;
|
||||
int gameId = -1;
|
||||
int winnerId = -2; // -2 = undecided, -1 = draw, >= 0 = winner player id
|
||||
int player1MatchWins = 0;
|
||||
int player2MatchWins = 0;
|
||||
QList<int> matchGameIds;
|
||||
};
|
||||
|
||||
private:
|
||||
Server_Game *parentGame;
|
||||
Server_MatchGameFactory *matchGameFactory;
|
||||
mutable QRecursiveMutex tournamentMutex;
|
||||
QMap<int, TournamentPlayerData> players;
|
||||
QMap<int, DeckList *> submittedDecks;
|
||||
QList<TournamentPairingData> currentPairings;
|
||||
QList<QPair<int, int>> allPreviousPairings;
|
||||
int currentRound;
|
||||
int totalRounds;
|
||||
bool started;
|
||||
uint32_t gamesPerMatch = 1;
|
||||
|
||||
void generateSwissPairings();
|
||||
int calculateTotalRounds() const;
|
||||
bool havePlayed(int p1, int p2) const;
|
||||
bool allPairingsDecided() const;
|
||||
void createMatchGame(int player1Id, int player2Id);
|
||||
void enqueueMatchGameCreation();
|
||||
void checkAndAdvanceRound(GameEventStorage &ges);
|
||||
};
|
||||
|
||||
#endif // SERVER_TOURNAMENT_H
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
#include "server_tournament_lifecycle_strategy.h"
|
||||
|
||||
#include "server_abstract_player.h"
|
||||
#include "server_game.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(TournamentLifecycleLog, "tournament_lifecycle");
|
||||
|
||||
Server_GameLifecycleStrategy::StartAction Server_TournamentLifecycleStrategy::onGameStarting(Server_Game *game)
|
||||
{
|
||||
// Match sub-games start through the normal flow; only the tournament hub game is
|
||||
// managed by this lifecycle.
|
||||
if (game->getTournamentParentGame() != nullptr) {
|
||||
return StartAction::ProceedNormal;
|
||||
}
|
||||
|
||||
for (auto *player : game->getPlayers().values()) {
|
||||
if (!player->getDeckList()) {
|
||||
qCWarning(TournamentLifecycleLog)
|
||||
<< "Tournament cannot start: player" << player->getUserInfo()->name().c_str() << "has no deck";
|
||||
return StartAction::Handled;
|
||||
}
|
||||
}
|
||||
|
||||
if (!game->getIsTournamentGame()) {
|
||||
qCWarning(TournamentLifecycleLog) << "Tournament lifecycle used for non-tournament game — falling back";
|
||||
return StartAction::ProceedNormal;
|
||||
}
|
||||
|
||||
game->startTournament();
|
||||
return StartAction::Handled;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
#ifndef SERVER_TOURNAMENT_LIFECYCLE_STRATEGY_H
|
||||
#define SERVER_TOURNAMENT_LIFECYCLE_STRATEGY_H
|
||||
|
||||
#include "server_game_lifecycle_strategy.h"
|
||||
|
||||
class Server_TournamentLifecycleStrategy : public Server_GameLifecycleStrategy
|
||||
{
|
||||
public:
|
||||
StartAction onGameStarting(Server_Game *game) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
#include "server_tournament_match_result_strategy.h"
|
||||
|
||||
#include "../server_response_containers.h"
|
||||
#include "server_abstract_player.h"
|
||||
#include "server_game.h"
|
||||
#include "server_tournament.h"
|
||||
|
||||
#include <libcockatrice/protocol/pb/event_game_closed.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_tournament_state.pb.h>
|
||||
|
||||
bool Server_TournamentMatchResultStrategy::onGameFinished(Server_Game *game,
|
||||
int playing,
|
||||
Server_AbstractPlayer *lastPlayer)
|
||||
{
|
||||
auto *parentGame = game->getTournamentParentGame();
|
||||
if (!parentGame || !parentGame->getTournament()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int winnerId;
|
||||
if (playing == 0) {
|
||||
winnerId = -1;
|
||||
} else {
|
||||
QString winnerName = QString::fromStdString(lastPlayer->getUserInfo()->name());
|
||||
auto *tournament = parentGame->getTournament();
|
||||
winnerId = tournament->getTournamentPlayerIdByName(winnerName);
|
||||
}
|
||||
|
||||
GameEventStorage parentGes;
|
||||
bool matchDecided = parentGame->getTournament()->recordMatchResultByGameId(game->getGameId(), winnerId, parentGes);
|
||||
parentGes.sendToGame(parentGame);
|
||||
|
||||
return matchDecided;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
#ifndef SERVER_TOURNAMENT_MATCH_RESULT_STRATEGY_H
|
||||
#define SERVER_TOURNAMENT_MATCH_RESULT_STRATEGY_H
|
||||
|
||||
#include "server_match_result_strategy.h"
|
||||
|
||||
class Server_TournamentMatchResultStrategy : public Server_MatchResultStrategy
|
||||
{
|
||||
public:
|
||||
bool onGameFinished(Server_Game *game, int playing, Server_AbstractPlayer *lastPlayer) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
#include "game/game_config.h"
|
||||
#include "game/server_game.h"
|
||||
#include "game/server_player.h"
|
||||
#include "game/server_tournament.h"
|
||||
#include "server_database_interface.h"
|
||||
#include "server_room.h"
|
||||
|
||||
|
|
@ -916,6 +917,8 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room
|
|||
int startingLifeTotal = cmd.has_starting_life_total() ? cmd.starting_life_total() : 20;
|
||||
|
||||
bool shareDecklistsOnLoad = cmd.has_share_decklists_on_load() ? cmd.share_decklists_on_load() : false;
|
||||
bool isTournament = cmd.has_is_tournament() ? cmd.is_tournament() : false;
|
||||
int gamesPerMatch = cmd.has_games_per_match() ? static_cast<int>(cmd.games_per_match()) : 1;
|
||||
|
||||
const int gameId = databaseInterface->getNextGameId();
|
||||
if (gameId == -1) {
|
||||
|
|
@ -940,6 +943,10 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room
|
|||
.shareDecklistsOnLoad = shareDecklistsOnLoad};
|
||||
|
||||
auto *game = new Server_Game(config, room);
|
||||
game->setIsTournamentGame(isTournament);
|
||||
if (isTournament && game->getTournament()) {
|
||||
game->getTournament()->setGamesPerMatch(static_cast<uint32_t>(qBound(1, gamesPerMatch, MAX_GAMES_PER_MATCH)));
|
||||
}
|
||||
|
||||
game->addPlayer(this, rc, asSpectator, asJudge, false);
|
||||
room->addGame(game);
|
||||
|
|
|
|||
|
|
@ -363,6 +363,18 @@ void Server_Room::broadcastGameListUpdate(const ServerInfo_Game &gameInfo, bool
|
|||
sendRoomEvent(prepareRoomEvent(event), sendToIsl);
|
||||
}
|
||||
|
||||
Server_AbstractUserInterface *Server_Room::getUserInterfaceByName(const QString &name) const
|
||||
{
|
||||
usersLock.lockForRead();
|
||||
auto it = users.constFind(name);
|
||||
Server_AbstractUserInterface *result = nullptr;
|
||||
if (it != users.constEnd()) {
|
||||
result = it.value();
|
||||
}
|
||||
usersLock.unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
void Server_Room::addGame(Server_Game *game)
|
||||
{
|
||||
ServerInfo_Room roomInfo;
|
||||
|
|
|
|||
|
|
@ -135,6 +135,8 @@ public:
|
|||
void addGame(Server_Game *game);
|
||||
void removeGame(Server_Game *game);
|
||||
|
||||
Server_AbstractUserInterface *getUserInterfaceByName(const QString &name) const;
|
||||
|
||||
void sendRoomEvent(RoomEvent *event, bool sendToIsl = true);
|
||||
RoomEvent *prepareRoomEvent(const ::google::protobuf::Message &roomEvent);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ set(PROTO_FILES
|
|||
command_set_sideboard_lock.proto
|
||||
command_set_sideboard_plan.proto
|
||||
command_shuffle.proto
|
||||
command_tournament.proto
|
||||
command_undo_draw.proto
|
||||
commands.proto
|
||||
context_concede.proto
|
||||
|
|
@ -118,6 +119,7 @@ set(PROTO_FILES
|
|||
event_set_card_counter.proto
|
||||
event_set_counter.proto
|
||||
event_shuffle.proto
|
||||
event_tournament_state.proto
|
||||
event_user_joined.proto
|
||||
event_user_left.proto
|
||||
event_user_message.proto
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
syntax = "proto2";
|
||||
import "game_commands.proto";
|
||||
import "event_tournament_state.proto";
|
||||
|
||||
message Command_ReportMatchResult {
|
||||
extend GameCommand {
|
||||
optional Command_ReportMatchResult ext = 1037;
|
||||
}
|
||||
|
||||
optional sint32 game_id = 1 [default = -1];
|
||||
optional sint32 winner_id = 2 [default = -1];
|
||||
}
|
||||
|
||||
message Command_AdvanceTournament {
|
||||
extend GameCommand {
|
||||
optional Command_AdvanceTournament ext = 1038;
|
||||
}
|
||||
}
|
||||
|
||||
message Command_TournamentSettingsSelect {
|
||||
extend GameCommand {
|
||||
optional Command_TournamentSettingsSelect ext = 1039;
|
||||
}
|
||||
|
||||
optional TournamentSettings settings = 1;
|
||||
}
|
||||
|
|
@ -24,4 +24,10 @@ message Event_GameStateChanged {
|
|||
|
||||
// the amount of seconds since the game started
|
||||
optional uint32 seconds_elapsed = 5;
|
||||
|
||||
// whether this game is a tournament game
|
||||
optional bool is_tournament = 9;
|
||||
|
||||
// for tournament sub-games: the ID of the parent tournament game (-1 if not a sub-game)
|
||||
optional sint32 parent_game_id = 10 [default = -1];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
syntax = "proto2";
|
||||
import "game_event.proto";
|
||||
|
||||
message TournamentPlayer {
|
||||
optional sint32 player_id = 1;
|
||||
optional string player_name = 2;
|
||||
optional uint32 wins = 3;
|
||||
optional uint32 losses = 4;
|
||||
optional uint32 draws = 5;
|
||||
optional bool deck_submitted = 6;
|
||||
}
|
||||
|
||||
message TournamentPairing {
|
||||
optional sint32 player1_id = 1;
|
||||
optional sint32 player2_id = 2 [default = -1];
|
||||
optional sint32 game_id = 3;
|
||||
optional sint32 winner_id = 4 [default = -1];
|
||||
optional sint32 player1_match_wins = 5 [default = 0];
|
||||
optional sint32 player2_match_wins = 6 [default = 0];
|
||||
}
|
||||
|
||||
message TournamentSettings {
|
||||
optional uint32 games_per_match = 1 [default = 1];
|
||||
}
|
||||
|
||||
message Event_TournamentState {
|
||||
extend GameEvent {
|
||||
optional Event_TournamentState ext = 2027;
|
||||
}
|
||||
|
||||
enum TournamentPhase {
|
||||
PHASE_DECK_BUILDING = 0;
|
||||
PHASE_PLAYING = 1;
|
||||
PHASE_FINISHED = 2;
|
||||
}
|
||||
|
||||
optional TournamentPhase phase = 1;
|
||||
optional uint32 current_round = 2;
|
||||
optional uint32 total_rounds = 3;
|
||||
repeated TournamentPlayer players = 4;
|
||||
repeated TournamentPairing pairings = 5;
|
||||
optional TournamentSettings settings = 6;
|
||||
}
|
||||
|
|
@ -180,6 +180,15 @@ message GameCommand {
|
|||
/// Server: Server_Player::cmdSetPlaymat
|
||||
/// Client: reflected via player properties changed event
|
||||
SET_PLAYMAT = 1035;
|
||||
|
||||
/// Report the result of a tournament match sub-game.
|
||||
REPORT_MATCH_RESULT = 1037;
|
||||
|
||||
/// Advance the tournament to the next round.
|
||||
ADVANCE_TOURNAMENT = 1038;
|
||||
|
||||
/// Select tournament settings.
|
||||
TOURNAMENT_SETTINGS_SELECT = 1039;
|
||||
}
|
||||
|
||||
extensions 100 to max;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ message GameEvent {
|
|||
CHANGE_ZONE_PROPERTIES = 2020;
|
||||
REVERSE_TURN = 2021;
|
||||
GAME_LOG_NOTICE = 2022;
|
||||
TOURNAMENT_STATE = 2027;
|
||||
}
|
||||
optional sint32 player_id = 1 [default = -1];
|
||||
extensions 100 to max;
|
||||
|
|
|
|||
|
|
@ -69,6 +69,12 @@ message Command_CreateGame {
|
|||
|
||||
// share decklists with all players when selected
|
||||
optional bool share_decklists_on_load = 14;
|
||||
|
||||
// number of games per match in tournament mode (e.g. 3 for best of 3)
|
||||
optional uint32 games_per_match = 15 [default = 1];
|
||||
|
||||
// whether this is a tournament game
|
||||
optional bool is_tournament = 16;
|
||||
}
|
||||
|
||||
message Command_JoinGame {
|
||||
|
|
|
|||
|
|
@ -65,4 +65,7 @@ message ServerInfo_Game {
|
|||
|
||||
// the current host of the game, which may differ from the creator after a host transfer
|
||||
optional ServerInfo_User host_info = 53;
|
||||
|
||||
// whether this game is a tournament game
|
||||
optional bool is_tournament = 54;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue