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)