diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml
index 92695a9d1..744f9e70a 100644
--- a/.github/workflows/desktop-build.yml
+++ b/.github/workflows/desktop-build.yml
@@ -161,14 +161,11 @@ jobs:
uses: actions/checkout@v7
- name: "Restore compiler cache (ccache)"
- id: ccache_restore
+ id: restore_ccache
uses: actions/cache/restore@v6
- env:
- BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
with:
- key: ccache-${{ matrix.distro }}${{ matrix.version }}-${{ env.BRANCH_NAME }}
+ key: ccache-${{ matrix.distro }}${{ matrix.version }}
path: ${{ env.CACHE }}
- restore-keys: ccache-${{ matrix.distro }}${{ matrix.version }}-
- name: "Build ${{ matrix.distro }} ${{ matrix.version }} Docker image"
shell: bash
@@ -203,10 +200,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.ccache_restore.outputs.cache-hit
+ if: github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
continue-on-error: true
env:
- CACHE_PRIMARY_KEY: ${{ steps.ccache_restore.outputs.cache-primary-key }}
+ CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.outputs.cache-primary-key }}
GH_TOKEN: ${{ github.token }}
run: |
if gh cache delete --repo "$GITHUB_REPOSITORY" "$CACHE_PRIMARY_KEY"; then
@@ -217,7 +214,7 @@ jobs:
if: github.ref == 'refs/heads/master'
uses: actions/cache/save@v6
with:
- key: ${{ steps.ccache_restore.outputs.cache-primary-key }}
+ key: ${{ steps.restore_ccache.outputs.cache-primary-key }}
path: ${{ env.CACHE }}
- name: "Upload artifact"
@@ -264,13 +261,14 @@ 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.0
+ qt_version: 6.11.1
qt_modules: qtimageformats qtmultimedia qtwebsockets
soc: Intel
type: Release
@@ -279,47 +277,48 @@ jobs:
- os: macOS
target: 14 # EOL 2026-??
- runner: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md
+ runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
ccache_eviction_age: 7d
cmake_generator: Ninja
make_package: 1
override_target: 14
package_suffix: "-macOS14"
- qt_version: 6.11.0
+ qt_version: 6.11.1
qt_modules: qtimageformats qtmultimedia qtwebsockets
soc: Apple
type: Release
use_ccache: 1
- xcode: "26.3"
+ xcode: "26.6"
- os: macOS
target: 15
- runner: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md
+ runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
ccache_eviction_age: 7d
cmake_generator: Ninja
make_package: 1
+ override_target: 15
package_suffix: "-macOS15"
- qt_version: 6.11.0
+ qt_version: 6.11.1
qt_modules: qtimageformats qtmultimedia qtwebsockets
soc: Apple
type: Release
use_ccache: 1
- xcode: "26.3"
+ xcode: "26.6"
- os: macOS
- target: 15
- runner: macos-15 # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md
+ target: 26
+ runner: macos-26 # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md
ccache_eviction_age: 7d
cmake_generator: Ninja
- qt_version: 6.11.0
+ qt_version: 6.11.1
qt_modules: qtimageformats qtmultimedia qtwebsockets
soc: Apple
type: Debug
use_ccache: 1
- xcode: "26.3"
+ xcode: "26.6"
- os: Windows
target: 10
@@ -329,7 +328,7 @@ jobs:
cmake_generator_platform: x64
make_package: 1
package_suffix: "-Win10"
- qt_version: 6.11.0
+ qt_version: 6.11.1
qt_modules: qtimageformats qtmultimedia qtwebsockets
type: Release
@@ -349,7 +348,6 @@ jobs:
- name: "[Windows] Add msbuild to PATH"
if: matrix.os == 'Windows'
- id: add-msbuild
uses: microsoft/setup-msbuild@v3
with:
msbuild-architecture: x64
@@ -360,14 +358,11 @@ jobs:
- name: "[macOS] Restore compiler cache (ccache)"
if: matrix.os == 'macOS' && matrix.use_ccache == 1
- id: ccache_restore
+ id: restore_ccache
uses: actions/cache/restore@v6
- env:
- BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
with:
- key: ccache-${{ matrix.runner }}-${{ matrix.soc }}-${{ matrix.type }}-${{ env.BRANCH_NAME }}
+ key: ccache-${{ matrix.runner }}_${{ matrix.override_target }}-Xcode${{ matrix.xcode }}
path: ${{ env.CCACHE_DIR }}
- restore-keys: ccache-${{ matrix.runner }}-${{ matrix.soc }}-${{ matrix.type }}-
- name: "Install aqtinstall"
run: pipx install aqtinstall
@@ -385,7 +380,7 @@ jobs:
id: restore_qt
uses: actions/cache/restore@v6
with:
- key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
+ key: Qt-${{ steps.resolve_qt_version.outputs.version }}-macOS-${{ matrix.soc }}-${{ matrix.qt_modules }}-thin
path: ${{ github.workspace }}/Qt
# Using jurplel/install-qt-action to install Qt without using brew
@@ -407,7 +402,7 @@ jobs:
if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
with:
- key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }}
+ key: ${{ steps.restore_qt.outputs.cache-primary-key }}
path: ${{ github.workspace }}/Qt
- name: "[Windows] Install Qt ${{ matrix.qt_version }}"
@@ -417,6 +412,7 @@ 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 }}
@@ -457,21 +453,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.ccache_restore.outputs.cache-hit
+ if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
continue-on-error: true
env:
- CACHE_PRIMARY_KEY: ${{ steps.ccache_restore.outputs.cache-primary-key }}
+ CACHE_PRIMARY_KEY: ${{ steps.restore_ccache.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] Save updated compiler cache (ccache)"
+ - name: "[macOS] Cache 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.ccache_restore.outputs.cache-primary-key }}
+ key: ${{ steps.restore_ccache.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 f510cf049..45b608127 100644
--- a/cockatrice/resources/countries/cn.svg
+++ b/cockatrice/resources/countries/cn.svg
@@ -52,8 +52,7 @@
id="defs8">
-
+
diff --git a/cockatrice/src/interface/pixel_map_generator.cpp b/cockatrice/src/interface/pixel_map_generator.cpp
index d3b0252a6..5bfba1c8a 100644
--- a/cockatrice/src/interface/pixel_map_generator.cpp
+++ b/cockatrice/src/interface/pixel_map_generator.cpp
@@ -14,6 +14,32 @@
#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.
*
@@ -35,6 +61,9 @@ 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);
@@ -247,7 +276,9 @@ static QIcon loadAndColorSvg(const QString &iconPath,
QSvgRenderer svgRenderer(doc.toByteArray());
- QPixmap pix(svgRenderer.defaultSize().expandedTo(QSize(minSize, minSize)));
+ const QSize pixmapSize =
+ capRenderSize(svgRenderer.defaultSize().expandedTo(QSize(minSize, minSize)), QSize(minSize, minSize));
+ QPixmap pix(pixmapSize);
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 67fb4f684..3a1876fa1 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,7 +39,10 @@ void UserCardArtProvider::requestCardArt(const QString &userName, const QString
const QString key = makeKey(userName, cardName, providerId);
- if (cardArtCache.contains(key) || pending.contains(key)) {
+ if (pending.contains(key)) {
+ return;
+ }
+ if (cardArtCache.contains(key) && !cardArtCache.value(key).isNull()) {
return;
}
@@ -63,6 +66,10 @@ 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) {
@@ -129,8 +136,6 @@ 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 fb2f37812..2592237c4 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 = 300;
+ static constexpr int MaxCacheEntries = 1024;
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 82f2887c8..5c65b090d 100644
--- a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp
@@ -101,23 +101,21 @@ 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 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));
+ // 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));
}
painter->setPen(Qt::NoPen);
painter->setBrush(bg);
painter->drawRoundedRect(cardRect, 6, 6);
- if (style.dark || hasRole || selected) {
- painter->setBrush(accentColor);
- painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
- }
+ // 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);
}
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 2534ee62c..e71eac23b 100644
--- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp
+++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp
@@ -1372,9 +1372,13 @@ 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)