Compare commits

...

5 commits

Author SHA1 Message Date
BruebachL
b01e107908
[Client] Show horizontal art for plane and siege cards in the profile banner (#7118) (#7250)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
Use the landscape orientation flag to rotate sideways-layout card art
upright before cropping, so planes/sieges show their horizontal art as
the server profile banner card instead of a rotated full card.

- Curve cropCardArt around the card's landscapeOrientation flag with
  landscape-specific art margins (mirrors CardInfoPictureWidget)
- Add shared CardArtUtils::rotateSidewaysLayoutArt helper and apply it
  to the playmat (game render and settings preview) and card info widget,
  replacing three duplicate 90-degree rotation blocks

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-07 02:20:54 +02:00
BruebachL
8d30ac54f0
[Game] Prevent spectator duplication when replaying joined events (#7248)
* [Game] Prevent spectator duplication when replaying joined events

The spectator branch of eventJoin emitted spectatorJoined unconditionally
even when the spectator was already present (e.g. replayed during a rewind).
Guard it like the player branch and eventGameStateChanged, and make
PlayerListWidget::addPlayer idempotent as defense in depth.

* In resetChatAndPhase() (the rewound() handler), also clear all spectators from both PlayerManager and PlayerListWidget before the replay rebuilds from event 0. The forward replay then re-adds exactly the spectators whose join events fall within the new time range via eventGameStateChanged/eventJoin.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-09-07 01:57:35 +02:00
tooomm
fb482f037e
550 MB --> 600 MB (#7257) 2026-09-06 23:13:34 +02:00
tooomm
0f0e46a177
[Oracle] Have space between size value and unit (#7254) 2026-09-06 22:19:13 +02:00
tooomm
2fe59d6326
fix table formatting (#7256) 2026-09-06 22:18:36 +02:00
18 changed files with 120 additions and 34 deletions

View file

@ -152,7 +152,7 @@ jobs:
env:
CACHE: ${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache
CCACHE_EVICTION_AGE: 7d
CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
CMAKE_GENERATOR: 'Ninja'
NAME: ${{ matrix.distro }}${{ matrix.version }}
@ -342,7 +342,7 @@ jobs:
timeout-minutes: 100
env:
CCACHE_DIR: ${{ github.workspace }}/.cache/
CCACHE_SIZE: 550M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
CCACHE_SIZE: 600M # space of all repo is 10Gi: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
steps:
- name: "Checkout"

View file

@ -149,15 +149,15 @@ You can then
<br>
The following flags (with their non-default values) can be passed to `cmake`:
| Flag | Description |
| --- | --- |
| `-DWITH_SERVER=1` | Build <kbd>Servatrice</kbd> server |
| `-DWITH_CLIENT=0` | Don't build <kbd>Cockatrice</kbd> client |
| `-DWITH_ORACLE=0` | Don't build <kbd>Oracle</kbd> card database tool |
| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode<br> Enables extra logging output, debug symbols, and much more verbose compiler warnings |
| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br> **Note:** `make clean` will remove the .ts files |
| `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available |
| Flag | Description |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `-DWITH_SERVER=1` | Build <kbd>Servatrice</kbd> server |
| `-DWITH_CLIENT=0` | Don't build <kbd>Cockatrice</kbd> client |
| `-DWITH_ORACLE=0` | Don't build <kbd>Oracle</kbd> card database tool |
| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode<br> Enables extra logging output, debug symbols, and much more verbose compiler warnings |
| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br> **Note:** `make clean` will remove the .ts files |
| `-DTEST=1` | Enable regression tests<br> **Note:** `make test` to run tests, *googletest* will be downloaded if not available |
# Run

View file

@ -166,6 +166,7 @@ set(cockatrice_SOURCES
src/interface/widgets/cards/additional_info/mana_cost_widget.cpp
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp
src/interface/widgets/cards/art_crop_attribution.cpp
src/interface/widgets/cards/card_art_utils.cpp
src/interface/widgets/cards/card_group_display_widgets/card_group_display_widget.cpp
src/interface/widgets/cards/card_group_display_widgets/flat_card_group_display_widget.cpp
src/interface/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp

View file

@ -430,12 +430,13 @@ void GameEventHandler::eventJoin(const Event_Join &event, int /*eventPlayerId*/,
QString playerName = QString::fromStdString(playerInfo.user_info().name());
emit addPlayerToAutoCompleteList(playerName);
if (game->getPlayerManager()->getPlayers().contains(playerId)) {
PlayerManager *playerManager = game->getPlayerManager();
if (playerManager->getPlayers().contains(playerId) || playerManager->getSpectators().contains(playerId)) {
return;
}
if (playerInfo.spectator()) {
game->getPlayerManager()->addSpectator(playerId, playerInfo);
playerManager->addSpectator(playerId, playerInfo);
emit logJoinSpectator(playerName);
emit spectatorJoined(playerInfo);
} else {

View file

@ -75,6 +75,14 @@ PlayerLogic *PlayerManager::getPlayer(int playerId) const
return player;
}
void PlayerManager::clearSpectators()
{
const QList<int> spectatorIds = spectators.keys();
for (int spectatorId : spectatorIds) {
removeSpectator(spectatorId);
}
}
void PlayerManager::onPlayerConceded(int playerId, bool conceded)
{
// Everything else cares about this

View file

@ -100,6 +100,9 @@ public:
emit spectatorRemoved(spectatorId, spectatorInfo);
}
/** @brief Remove all spectators, emitting the removal signal for each. */
void clearSpectators();
[[nodiscard]] AbstractGame *getGame() const
{
return game;

View file

@ -3,6 +3,7 @@
#include "../../game/player/player_actions.h"
#include "../../interface/card_picture_loader/card_picture_loader.h"
#include "../../interface/widgets/cards/art_crop_attribution.h"
#include "../../interface/widgets/cards/card_art_utils.h"
#include "../../interface/widgets/playmat/playmat_utils.h"
#include "../../interface/widgets/tabs/tab_game.h"
#include "../board/abstract_card_item.h"
@ -442,7 +443,7 @@ void PlayerGraphicsItem::updatePlaymat()
hasPlaymat = true;
emit playmatChanged(true);
}
playmatPixmap = fullRes;
playmatPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card);
update();
}

View file

@ -92,6 +92,11 @@ void PlayerListWidget::retranslateUi()
void PlayerListWidget::addPlayer(const ServerInfo_PlayerProperties &player)
{
if (players.contains(player.player_id())) {
updatePlayerProperties(player);
return;
}
QTreeWidgetItem *newPlayer = new PlayerListTWI;
players.insert(player.player_id(), newPlayer);
updatePlayerProperties(player);
@ -176,6 +181,17 @@ void PlayerListWidget::removePlayer(int playerId)
delete takeTopLevelItem(indexOfTopLevelItem(player));
}
void PlayerListWidget::clearSpectators()
{
const QList<int> playerIds = players.keys();
for (int playerId : playerIds) {
QTreeWidgetItem *player = players.value(playerId, 0);
if (player && !player->data(1, Qt::UserRole).toBool()) {
removePlayer(playerId);
}
}
}
void PlayerListWidget::setActivePlayer(int playerId)
{
QMapIterator<int, QTreeWidgetItem *> i(players);

View file

@ -66,6 +66,7 @@ public slots:
void addPlayer(const ServerInfo_PlayerProperties &player);
void removePlayer(int playerId);
void updatePlayerProperties(const ServerInfo_PlayerProperties &prop, int playerId = -1);
void clearSpectators();
};
#endif

View file

@ -0,0 +1,18 @@
#include "card_art_utils.h"
#include <QTransform>
#include <libcockatrice/card/printing/exact_card.h>
namespace CardArtUtils
{
QPixmap rotateSidewaysLayoutArt(const QPixmap &art, const ExactCard &card)
{
if (!card.getInfo().getUiAttributes().landscapeOrientation) {
return art;
}
QTransform transform;
transform.rotate(90);
return art.transformed(transform, Qt::SmoothTransformation);
}
} // namespace CardArtUtils

View file

@ -0,0 +1,25 @@
#ifndef CARD_ART_UTILS_H
#define CARD_ART_UTILS_H
#include <QPixmap>
class ExactCard;
namespace CardArtUtils
{
/**
* @brief Rotates a card's art upright when its layout shows sideways.
*
* Sideways-layout cards (planes, sieges/battles, split cards) store their
* landscape artwork rotated 90° inside a portrait frame. Art-crop displays,
* playmat art, and the card-info picture must show such art upright before
* sampling or painting. Portrait cards are returned unchanged.
*
* @param art The card pixmap to orient.
* @param card The card describing the art orientation.
* @return @p art rotated 90° clockwise for sideways-layout cards, else @p art.
*/
QPixmap rotateSidewaysLayoutArt(const QPixmap &art, const ExactCard &card);
} // namespace CardArtUtils
#endif // CARD_ART_UTILS_H

View file

@ -5,6 +5,7 @@
#include "../../../interface/card_picture_loader/card_picture_loader.h"
#include "../../../interface/widgets/tabs/tab_supervisor.h"
#include "../../window_main.h"
#include "card_art_utils.h"
#include <QMenu>
#include <QMouseEvent>
@ -193,12 +194,7 @@ void CardInfoPictureWidget::paintEvent(QPaintEvent *event)
QPixmap transformedPixmap = resizedPixmap; // Default pixmap
if (SettingsCache::instance().cardsDisplay().getAutoRotateSidewaysLayoutCards()) {
if (exactCard.getInfo().getUiAttributes().landscapeOrientation) {
// Rotate pixmap 90 degrees to the left
QTransform transform;
transform.rotate(90);
transformedPixmap = resizedPixmap.transformed(transform, Qt::SmoothTransformation);
}
transformedPixmap = CardArtUtils::rotateSidewaysLayoutArt(resizedPixmap, exactCard);
}
// Handle DPI scaling

View file

@ -2,6 +2,7 @@
#include "../../card_picture_loader/card_picture_loader.h"
#include "../cards/art_crop_attribution.h"
#include "../cards/card_art_utils.h"
#include "../utility/completer_utils.h"
#include "card_database_display_model.h"
#include "card_database_model.h"
@ -276,7 +277,7 @@ void PlaymatSettingsDialog::reloadPreview()
return;
}
currentPixmap = fullRes;
currentPixmap = CardArtUtils::rotateSidewaysLayoutArt(fullRes, card);
preview->setPixmap(currentPixmap);
preview->setParams(currentParams);
preview->setAttribution(buildArtAttribution(card));

View file

@ -1,6 +1,7 @@
#include "user_card_art_provider.h"
#include "../../../card_picture_loader/card_picture_loader.h"
#include "../../cards/card_art_utils.h"
#include <QPointer>
#include <libcockatrice/card/database/card_database_manager.h>
@ -52,16 +53,25 @@ void UserCardArtProvider::requestCardArt(const QString &userName, const QString
processQueue();
}
QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes)
QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes, const ExactCard &card)
{
const QSize sz = fullRes.size();
QPixmap source = fullRes;
// Sideways-layout cards (plane, siege/battle, split) store their landscape
// artwork rotated 90° inside a portrait frame. Rotate it upright first so
// the crop below lands on the horizontal art, mirroring the way
// CardInfoPictureWidget displays these cards.
const bool landscape = card.getInfo().getUiAttributes().landscapeOrientation;
source = CardArtUtils::rotateSidewaysLayoutArt(source, card);
const QSize sz = source.size();
const int marginX = sz.width() * 0.07;
const int topMargin = sz.height() * 0.11;
const int bottomMargin = sz.height() * 0.45;
const int topMargin = landscape ? sz.height() * 0.05 : sz.height() * 0.11;
const int bottomMargin = landscape ? sz.height() * 0.42 : sz.height() * 0.45;
const QRect foilRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin);
const QRect artRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin);
return fullRes.copy(foilRect.intersected(fullRes.rect()));
return source.copy(artRect.intersected(source.rect()));
}
void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap)
@ -111,7 +121,7 @@ void UserCardArtProvider::processQueue()
// Synchronous hit (already loaded/on disk)
if (!fullRes.isNull()) {
insertIntoCache(key, cropCardArt(fullRes));
insertIntoCache(key, cropCardArt(fullRes, card));
pending.remove(key);
emit cardArtUpdated(userName);
@ -135,7 +145,7 @@ void UserCardArtProvider::processQueue()
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
if (!fullRes.isNull()) {
self->insertIntoCache(key, self->cropCardArt(fullRes));
self->insertIntoCache(key, self->cropCardArt(fullRes, card));
}
self->pending.remove(key);

View file

@ -6,6 +6,7 @@
#include <QPixmap>
#include <QQueue>
#include <QSet>
#include <libcockatrice/card/printing/exact_card.h>
class UserCardArtProvider : public QObject
{
@ -16,7 +17,7 @@ public:
void requestCardArt(const QString &userName, const QString &cardName, const QString &providerId);
const QMap<QString, QPixmap> &cache() const;
static QPixmap cropCardArt(const QPixmap &fullRes);
static QPixmap cropCardArt(const QPixmap &fullRes, const ExactCard &card);
signals:
void cardArtUpdated(const QString &userName);

View file

@ -560,7 +560,7 @@ void UserCardArtSettingsDialog::reloadPreview()
return;
}
currentPixmap = UserCardArtProvider::cropCardArt(fullRes);
currentPixmap = UserCardArtProvider::cropCardArt(fullRes, card);
preview->setPixmap(currentPixmap);
preview->setParams(currentParams);

View file

@ -266,6 +266,10 @@ void TabGame::resetChatAndPhase()
// reset phase markers
game->getGameState()->setCurrentPhase(-1);
// reset spectator state so the replay can rebuild it from the start
game->getPlayerManager()->clearSpectators();
playerListWidget->clearSpectators();
}
void TabGame::emitUserEvent()

View file

@ -260,7 +260,7 @@ bool LoadSetsPage::validatePage()
return false;
}
progressLabel->setText(tr("Downloading (0MB)"));
progressLabel->setText(tr("Downloading (0 MB)"));
// show an infinite progressbar
progressBar->setMaximum(0);
progressBar->setMinimum(0);
@ -343,7 +343,7 @@ void LoadSetsPage::actDownloadProgressSetsFile(qint64 received, qint64 total)
progressBar->setMaximum(static_cast<int>(total));
progressBar->setValue(static_cast<int>(received));
}
progressLabel->setText(tr("Downloading (%1MB)").arg((int)received / (1024 * 1024)));
progressLabel->setText(tr("Downloading (%1 MB)").arg((int)received / (1024 * 1024)));
}
void LoadSetsPage::actDownloadFinishedSetsFile()
@ -743,4 +743,4 @@ void LoadSpoilersPage::retranslateUi()
pathLabel->setText(tr("The spoiler database will be saved at the following location:") + "<br>" +
SettingsCache::instance().getSpoilerCardDatabasePath());
defaultPathCheckBox->setText(tr("Save to a custom path (not recommended)"));
}
}