diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml
index 744f9e70a..92695a9d1 100644
--- a/.github/workflows/desktop-build.yml
+++ b/.github/workflows/desktop-build.yml
@@ -161,11 +161,14 @@ jobs:
uses: actions/checkout@v7
- name: "Restore compiler cache (ccache)"
- id: restore_ccache
+ id: ccache_restore
uses: actions/cache/restore@v6
+ env:
+ BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
with:
- key: ccache-${{ matrix.distro }}${{ matrix.version }}
+ key: ccache-${{ matrix.distro }}${{ matrix.version }}-${{ env.BRANCH_NAME }}
path: ${{ env.CACHE }}
+ restore-keys: ccache-${{ matrix.distro }}${{ matrix.version }}-
- name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image"
shell: bash
@@ -200,10 +203,10 @@ jobs:
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
- name: "Delete remote compiler cache (ccache)"
- if: github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
+ if: github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit
continue-on-error: true
env:
- CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }}
+ CACHE_PRIMARY_KEY: ${{ steps.ccache_restore.outputs.cache-primary-key }}
GH_TOKEN: ${{ github.token }}
run: |
if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then
@@ -214,7 +217,7 @@ jobs:
if: github.ref == 'refs/heads/master'
uses: actions/cache/save@v6
with:
- key: ${{ steps.restore_ccache.outputs.cache-primary-key }}
+ key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
path: ${{ env.CACHE }}
- name: "Upload artifact"
@@ -261,14 +264,13 @@ jobs:
- os: macOS
target: 13 # EOL 2025-09-15
runner: macos-15-intel # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md
- # macos-26-intel is very slow and fails in CPack during DMG config if not increasing Finder timeout
ccache_eviction_age: 7d
cmake_generator: Ninja
make_package: 1
override_target: 13
package_suffix: "-macOS13_Intel"
- qt_version: 6.11.1
+ qt_version: 6.11.0
qt_modules: qtimageformats qtmultimedia qtwebsockets
soc: Intel
type: Release
@@ -277,48 +279,47 @@ jobs:
- os: macOS
target: 14 # EOL 2026-??
- runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
+ runner: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md
ccache_eviction_age: 7d
cmake_generator: Ninja
make_package: 1
override_target: 14
package_suffix: "-macOS14"
- qt_version: 6.11.1
+ qt_version: 6.11.0
qt_modules: qtimageformats qtmultimedia qtwebsockets
soc: Apple
type: Release
use_ccache: 1
- xcode: "26.6"
+ xcode: "26.3"
- os: macOS
target: 15
- runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
+ runner: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md
ccache_eviction_age: 7d
cmake_generator: Ninja
make_package: 1
- override_target: 15
package_suffix: "-macOS15"
- qt_version: 6.11.1
+ qt_version: 6.11.0
qt_modules: qtimageformats qtmultimedia qtwebsockets
soc: Apple
type: Release
use_ccache: 1
- xcode: "26.6"
+ xcode: "26.3"
- os: macOS
- target: 26
- runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
+ target: 15
+ runner: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md
ccache_eviction_age: 7d
cmake_generator: Ninja
- qt_version: 6.11.1
+ qt_version: 6.11.0
qt_modules: qtimageformats qtmultimedia qtwebsockets
soc: Apple
type: Debug
use_ccache: 1
- xcode: "26.6"
+ xcode: "26.3"
- os: Windows
target: 10
@@ -328,7 +329,7 @@ jobs:
cmake_generator_platform: x64
make_package: 1
package_suffix: "-Win10"
- qt_version: 6.11.1
+ qt_version: 6.11.0
qt_modules: qtimageformats qtmultimedia qtwebsockets
type: Release
@@ -348,6 +349,7 @@ jobs:
- name: "[Windows] Add msbuild to PATH"
if: matrix.os == 'Windows'
+ id: add-msbuild
uses: microsoft/setup-msbuild@v3
with:
msbuild-architecture: x64
@@ -358,11 +360,14 @@ jobs:
- name: "[macOS] Restore compiler cache (ccache)"
if: matrix.os == 'macOS' && matrix.use_ccache == 1
- id: restore_ccache
+ id: ccache_restore
uses: actions/cache/restore@v6
+ env:
+ BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
with:
- key: ccache-${{ matrix.runner }}_${{ matrix.override_target }}-Xcode${{ matrix.xcode }}
+ key: ccache-${{ matrix.runner }}-${{ matrix.soc }}-${{ matrix.type }}-${{ env.BRANCH_NAME }}
path: ${{ env.CCACHE_DIR }}
+ restore-keys: ccache-${{ matrix.runner }}-${{ matrix.soc }}-${{ matrix.type }}-
- name: "Install aqtinstall"
run: pipx install aqtinstall
@@ -380,7 +385,7 @@ jobs:
id: restore_qt
uses: actions/cache/restore@v6
with:
- key: Qt-${{ steps.resolve_qt_version.outputs.version }}-macOS-${{ matrix.soc }}-${{ matrix.qt_modules }}-thin
+ key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
path: ${{ github.workspace }}/Qt
# Using jurplel/install-qt-action to install Qt without using brew
@@ -402,7 +407,7 @@ jobs:
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
with:
- key: ${{ steps.restore_qt.outputs.cache-primary-key }}
+ key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
path: ${{ github.workspace }}/Qt
- name: "[Windows] Install Qt ${{ matrix.qt_version }}"
@@ -412,7 +417,6 @@ jobs:
# Qt 6.11.0 only works with aqtinstall directly from git until aqtinstall 3.4 is released
aqtsource: git+https://github.com/miurahr/aqtinstall.git
cache: true
- cache-key-prefix: Qt
modules: ${{ matrix.qt_modules }}
version: ${{ steps.resolve_qt_version.outputs.version }}
@@ -453,21 +457,21 @@ jobs:
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
- name: "[macOS] Delete remote compiler cache (ccache)"
- if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
+ if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.ccache_restore.outputs.cache-hit
continue-on-error: true
env:
- CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }}
+ CACHE_PRIMARY_KEY: ${{ steps.ccache_restore.outputs.cache-primary-key }}
GH_TOKEN: ${{ github.token }}
run: |
if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then
echo "Cache deleted successfully"
fi
- - name: "[macOS] Cache updated compiler cache (ccache)"
+ - name: "[macOS] Save updated compiler cache (ccache)"
if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master'
uses: actions/cache/save@v6
with:
- key: ${{ steps.restore_ccache.outputs.cache-primary-key }}
+ key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
path: ${{ env.CCACHE_DIR }}
- name: "[macOS] Sign app bundle"
diff --git a/cockatrice/resources/countries/cn.svg b/cockatrice/resources/countries/cn.svg
index 45b608127..f510cf049 100644
--- a/cockatrice/resources/countries/cn.svg
+++ b/cockatrice/resources/countries/cn.svg
@@ -52,7 +52,8 @@
id="defs8">
-
+
diff --git a/cockatrice/src/interface/pixel_map_generator.cpp b/cockatrice/src/interface/pixel_map_generator.cpp
index 5bfba1c8a..d3b0252a6 100644
--- a/cockatrice/src/interface/pixel_map_generator.cpp
+++ b/cockatrice/src/interface/pixel_map_generator.cpp
@@ -14,32 +14,6 @@
#define DEFAULT_COLOR_MODERATOR_RIGHT "#000000";
#define DEFAULT_COLOR_ADMIN "#ff2701";
-/**
- * Clamps an svg render size so that rendering does not exceed a multiple of the requested size.
- *
- * Rendering at the full native size of an svg just to scale it down afterwards wastes memory,
- * and canvases with extreme coordinates can exceed Qt's rasterizer coordinate limit which makes
- * Qt silently drop shapes from the rendered image.
- *
- * @param renderSize The size the svg would be rendered at.
- * @param requestedSize The size that was actually requested.
- *
- * @return A size with the aspect ratio of renderSize whose longest side is at most four times
- * the longest side of requestedSize.
- */
-static QSize capRenderSize(const QSize &renderSize, const QSize &requestedSize)
-{
- const int longestRequestedSide = qMax(requestedSize.width(), requestedSize.height());
- if (longestRequestedSide <= 0) {
- return renderSize;
- }
-
- const int longestRenderSide = qMax(renderSize.width(), renderSize.height());
- const qreal scale = qMin(1.0, static_cast(longestRequestedSide * 4) / longestRenderSide);
- return QSize(qMax(1, static_cast(renderSize.width() * scale)),
- qMax(1, static_cast(renderSize.height() * scale)));
-}
-
/**
* Loads in an svg from file and scales it without affecting image quality.
*
@@ -61,9 +35,6 @@ static QPixmap loadSvg(const QString &svgPath, const QSize &size, bool expandOnl
// If expandOnly, make sure the pixmap is at least as large as the svg, so that we don't lose any detail.
// QIcon.pixmap(size) will automatically scale down the image, but it won't scale it up.
QSize pixmapSize = expandOnly ? svgRenderer.defaultSize().expandedTo(size) : size;
- if (expandOnly) {
- pixmapSize = capRenderSize(pixmapSize, size);
- }
QPixmap pix(pixmapSize);
pix.fill(Qt::transparent);
@@ -276,9 +247,7 @@ static QIcon loadAndColorSvg(const QString &iconPath,
QSvgRenderer svgRenderer(doc.toByteArray());
- const QSize pixmapSize =
- capRenderSize(svgRenderer.defaultSize().expandedTo(QSize(minSize, minSize)), QSize(minSize, minSize));
- QPixmap pix(pixmapSize);
+ QPixmap pix(svgRenderer.defaultSize().expandedTo(QSize(minSize, minSize)));
pix.fill(Qt::transparent);
QPainter pixPainter(&pix);
diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp
index 3a1876fa1..67fb4f684 100644
--- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp
@@ -39,10 +39,7 @@ void UserCardArtProvider::requestCardArt(const QString &userName, const QString
const QString key = makeKey(userName, cardName, providerId);
- if (pending.contains(key)) {
- return;
- }
- if (cardArtCache.contains(key) && !cardArtCache.value(key).isNull()) {
+ if (cardArtCache.contains(key) || pending.contains(key)) {
return;
}
@@ -66,10 +63,6 @@ QPixmap UserCardArtProvider::cropCardArt(const QPixmap &fullRes)
void UserCardArtProvider::insertIntoCache(const QString &key, const QPixmap &pixmap)
{
- if (pixmap.isNull()) {
- return;
- }
-
if (!cardArtCache.contains(key)) {
cacheInsertionOrder.append(key);
while (cacheInsertionOrder.size() > MaxCacheEntries) {
@@ -136,6 +129,8 @@ void UserCardArtProvider::processQueue()
if (!fullRes.isNull()) {
self->insertIntoCache(key, self->cropCardArt(fullRes));
+ } else {
+ self->insertIntoCache(key, QPixmap());
}
self->pending.remove(key);
diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h
index 2592237c4..fb2f37812 100644
--- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h
+++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h
@@ -26,7 +26,7 @@ public slots:
private:
bool dbReady = false;
- static constexpr int MaxCacheEntries = 1024;
+ static constexpr int MaxCacheEntries = 300;
QList cacheInsertionOrder; // FIFO eviction
QMap cardArtCache;
QSet pending;
diff --git a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp
index 5c65b090d..82f2887c8 100644
--- a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp
@@ -101,21 +101,23 @@ void UserListPainter::drawBackground(QPainter *painter,
bg.setColorAt(0, blend(style.cardStart, accentColor, selected ? 0.75 : 0.65));
bg.setColorAt(1, blend(style.cardEnd, accentColor, selected ? 0.18 : 0.10));
} else {
- // Regular users keep a scaled-down accent tint so the banner card art
- // stays legible over a colored backdrop (the pre-branch painter was
- // always dark-styled) while the role hierarchy still reads.
- bg.setColorAt(0, blend(style.cardStart, accentColor, (selected ? 0.75 : 0.65) * 0.7));
- bg.setColorAt(1, blend(style.cardEnd, accentColor, (selected ? 0.18 : 0.10) * 0.7));
+ // Regular users are the light theme's neutral paper cards. A flat
+ // warm card fill (the normal row surface, slightly deepened) keeps
+ // every row clearly visible without borrowing a role color. Selection
+ // shifts the fill toward a soft slate so the highlight still reads.
+ const QColor paper = style.cardEnd.darker(108);
+ bg.setColorAt(0, blend(paper, accentColor, selected ? 0.35 : 0.0));
+ bg.setColorAt(1, blend(paper, accentColor, selected ? 0.25 : 0.0));
}
painter->setPen(Qt::NoPen);
painter->setBrush(bg);
painter->drawRoundedRect(cardRect, 6, 6);
- // The 3px accent bar anchors every row so the banner card art reads as a
- // consistent strip in either scheme (pre-branch parity).
- painter->setBrush(accentColor);
- painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
+ if (style.dark || hasRole || selected) {
+ painter->setBrush(accentColor);
+ painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
+ }
}
static QString makeKey(const QString &user, const QString &card, const QString &providerId)
diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp
index e71eac23b..2534ee62c 100644
--- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp
@@ -1372,13 +1372,9 @@ void UserListWidget::updateCardArtParams(const ServerInfo_User &user, const QStr
params.zoom = cap.zoom();
cardArtParamsMap.insert(userName, params);
cardArtProvider->requestCardArt(userName, params.cardName, params.cardProviderId);
+ } else {
+ cardArtParamsMap.remove(userName); // clear stale params on removal
}
- // Intentionally no removal branch: buddy/ignore list copies never carry
- // card_art_params (the server omits the column), so a params-less copy here
- // means "this snapshot doesn't include it", not "the banner was removed".
- // Removing on such copies would wipe banners that the live online list set.
- // The map is rebuilt from scratch (clear() + repopulate) on every rebuild,
- // which is what actually drops stale entries.
}
void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online)