diff --git a/.ci/release_template.md b/.ci/release_template.md index 23b475150..ac78a193a 100644 --- a/.ci/release_template.md +++ b/.ci/release_template.md @@ -12,9 +12,9 @@ Available pre-compiled binaries for installation: • Windows 10+ macOS - • macOS 15+ Sequoia - • macOS 14+ Sonoma - • macOS 13+ Ventura (x86) + • macOS 15+ Sequoia Apple M + • macOS 14+ Sonoma Apple M + • macOS 13+ Ventura Intel Linux • Ubuntu 26.04 LTS Resolute Racoon @@ -24,10 +24,10 @@ Available pre-compiled binaries for installation: • Fedora 44 • Fedora 43 - General Linux support is available via a flatpak package hosted at Flathub (x86 & ARM)! - Thanks to courtesy of @FFY00, the app is also available in Arch Linux's official extra repository. +We are also packaged in Arch Linux's official extra repository, courtesy of @FFY00. +General Linux support is available via a flatpak package at Flathub! - We maintain a Docker image for "Servatrice" in GHCR (x86 & ARM). You can docker pull it or use our Docker Compose files! +We provide a Docker image for "Servatrice" in GHCR. You can docker pull it or use our Docker Compose files! diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 58ca87573..fee0b34cb 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -9,8 +9,8 @@ name: CodeQL permissions: - contents: read security-events: write # needed to post results + contents: read on: push: diff --git a/.github/workflows/desktop-lint.yml b/.github/workflows/desktop-lint.yml index 93ba79464..5f31ea59c 100644 --- a/.github/workflows/desktop-lint.yml +++ b/.github/workflows/desktop-lint.yml @@ -1,8 +1,5 @@ name: Code Style (C++) -permissions: - contents: read - on: # Push trigger not needed for linting, we do not allow direct pushes to master pull_request: diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index df4fe233c..5384c9e64 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -1,8 +1,8 @@ -name: Build Docker +name: Build Docker Image permissions: - contents: read # needed to checkout repo - packages: write # needed for interacting with GHCR + contents: read + packages: write on: push: @@ -13,10 +13,7 @@ on: - master paths: - '.github/workflows/docker-release.yml' - - '.dockerignore' - 'Dockerfile' - - 'docker-compose.yml' - - 'docker-compose.yml.windows' release: types: - released # publishing of stable releases @@ -26,38 +23,36 @@ concurrency: group: "${{ github.workflow }} @ ${{ github.ref_name }}" cancel-in-progress: ${{ github.event_name != 'release' }} -env: - GHCR_IMAGE: ghcr.io/cockatrice/servatrice - OCI_DESCRIPTION: Server for Cockatrice, a cross-platform virtual tabletop for multiplayer card games - OCI_TITLE: Servatrice - OCI_URL: https://cockatrice.github.io/ - jobs: - # Create one platform-specific image and publish its OCI image manifest per matrix job - build: - name: "Servatrice (${{ matrix.label }})" - if: github.repository_owner == 'Cockatrice' - runs-on: ${{ matrix.runner }} - - strategy: - fail-fast: false - matrix: - include: - - label: x86 - platform: linux/amd64 - runner: ubuntu-latest # https://github.com/actions/runner-images - - - label: arm - platform: linux/arm64 - runner: ubuntu-24.04-arm # https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Arm64-Readme.md, replace with "ubuntu-latest-arm" once available - - env: - CACHE_SCOPE: servatrice-${{ matrix.label }} - + docker: + name: amd64 & arm64 + if: ${{ github.repository_owner == 'Cockatrice' }} + runs-on: ubuntu-latest + steps: - name: "Checkout" uses: actions/checkout@v7 + - name: "Docker metadata" + id: metadata + uses: docker/metadata-action@v6 + env: + DOCKER_METADATA_ANNOTATIONS_LEVELS: index # needed for GHCR + with: + annotations: | + org.opencontainers.image.title=Servatrice + org.opencontainers.image.url=https://cockatrice.github.io/ + org.opencontainers.image.description=Server for Cockatrice, a cross-platform virtual tabletop for multiplayer card games + images: | + ghcr.io/cockatrice/servatrice + labels: | + org.opencontainers.image.title=Servatrice + org.opencontainers.image.url=https://cockatrice.github.io/ + org.opencontainers.image.description=Server for Cockatrice, a cross-platform virtual tabletop for multiplayer card games + + - name: "Set up QEMU" + uses: docker/setup-qemu-action@v4 + - name: "Set up Docker buildx" uses: docker/setup-buildx-action@v4 @@ -66,117 +61,18 @@ jobs: id: login uses: docker/login-action@v4 with: + password: ${{ github.token }} registry: ghcr.io username: ${{ github.actor }} - password: ${{ github.token }} - # Don't push for non-release triggers - - name: "Build image" - if: steps.login.outcome != 'success' + - name: "Build and push Docker image" uses: docker/build-push-action@v7 with: - cache-from: type=gha,scope=${{ env.CACHE_SCOPE }} - cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }} + annotations: ${{ steps.metadata.outputs.annotations }} + cache-from: type=gha,scope=servatrice + cache-to: type=gha,mode=max,scope=servatrice context: . - platforms: ${{ matrix.platform }} - push: false - - # Add OCI labels and push single-platform image by digest (without tags) - - name: "Build image and push by digest" - if: steps.login.outcome == 'success' - id: build - uses: docker/build-push-action@v7 - with: - cache-from: type=gha,scope=${{ env.CACHE_SCOPE }} - cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }} - context: . - labels: | - org.opencontainers.image.description=${{ env.OCI_DESCRIPTION }} - org.opencontainers.image.title=${{ env.OCI_TITLE }} - org.opencontainers.image.url=${{ env.OCI_URL }} - outputs: type=image,name=${{ env.GHCR_IMAGE }},name-canonical=true,push=true,push-by-digest=true - platforms: ${{ matrix.platform }} - provenance: mode=max # Do not pass secrets as build arguments with this option - sbom: true - - - name: "Export digest" - if: steps.login.outcome == 'success' - env: - DIGEST: ${{ steps.build.outputs.digest }} - run: | - mkdir -p "$RUNNER_TEMP/digests" - touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}" - - - name: "Upload digest" - if: steps.login.outcome == 'success' - uses: actions/upload-artifact@v7 - with: - archive: false - if-no-files-found: error - name: digest-${{ matrix.label }} - path: ${{ runner.temp }}/digests/* - retention-days: 1 - - - # Create an OCI image index from the platform-specific image manifests - index: - name: "Publish multi-platform Servatrice image" - if: github.repository_owner == 'Cockatrice' && github.event_name == 'release' && github.event.release.prerelease == false - needs: build - runs-on: ubuntu-slim # https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md - - steps: - - name: "Download digests" - uses: actions/download-artifact@v7 - with: - path: ${{ runner.temp }}/digests - pattern: digest-* - merge-multiple: true - - - name: "Login to GitHub Container Registry (GHCR)" - uses: docker/login-action@v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ github.token }} - - - name: "Docker metadata" - id: metadata - uses: docker/metadata-action@v6 - with: - images: ${{ env.GHCR_IMAGE }} - flavor: | - latest=auto - tags: | - type=ref,event=tag # if semver, also: type=semver,pattern={{version}} / {{major}}.{{minor}} - - # Add OCI annotations to image index and publish tags - - name: "Create image index" - env: - DOCKER_TAGS: ${{ steps.metadata.outputs.tags }} - working-directory: ${{ runner.temp }}/digests - run: | - TAG_ARGS=() - while IFS= read -r tag; do - TAG_ARGS+=(--tag "$tag") - done <<< "$DOCKER_TAGS" - - DIGEST_ARGS=() - for digest in *; do - DIGEST_ARGS+=("$GHCR_IMAGE@sha256:$digest") - done - - docker buildx imagetools create \ - --prefer-index=true \ - --annotation "index:org.opencontainers.image.description=$OCI_DESCRIPTION" \ - --annotation "index:org.opencontainers.image.title=$OCI_TITLE" \ - --annotation "index:org.opencontainers.image.url=$OCI_URL" \ - "${TAG_ARGS[@]}" \ - "${DIGEST_ARGS[@]}" - - - name: "Inspect images" - env: - GITHUB_TAG: ${{ github.ref_name }} - run: | - docker buildx imagetools inspect "$GHCR_IMAGE:latest" - docker buildx imagetools inspect "$GHCR_IMAGE:$GITHUB_TAG" + labels: ${{ steps.metadata.outputs.labels }} + platforms: linux/amd64,linux/arm64 + push: ${{ steps.login.outcome == 'success' }} + tags: ${{ steps.metadata.outputs.tags }} diff --git a/.github/workflows/documentation-build.yml b/.github/workflows/documentation-build.yml index 419cbfbfb..4c06f9ab3 100644 --- a/.github/workflows/documentation-build.yml +++ b/.github/workflows/documentation-build.yml @@ -1,8 +1,5 @@ name: Generate Docs -permissions: - contents: read # write permission to the destination repo come from 'deploy_key' - on: pull_request: paths: diff --git a/.github/workflows/translations-pull.yml b/.github/workflows/translations-pull.yml index 71b0b4c22..a3db5f86d 100644 --- a/.github/workflows/translations-pull.yml +++ b/.github/workflows/translations-pull.yml @@ -1,9 +1,5 @@ name: Update Translations -permissions: - contents: read - pull-requests: write - on: pull_request: paths: diff --git a/.github/workflows/translations-push.yml b/.github/workflows/translations-push.yml index 41a7aef40..c4d3f61fb 100644 --- a/.github/workflows/translations-push.yml +++ b/.github/workflows/translations-push.yml @@ -1,9 +1,5 @@ name: Update Translation Source -permissions: - contents: read - pull-requests: write - on: pull_request: paths: diff --git a/Dockerfile b/Dockerfile index 382309d47..7c5c773c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,45 +3,35 @@ FROM ubuntu:26.04 AS build ARG DEBIAN_FRONTEND=noninteractive -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - build-essential \ - cmake \ - ninja-build \ - file \ - g++ \ - git \ - libmariadb-dev-compat \ - libprotobuf-dev \ - libqt6sql6-mysql \ - qt6-websockets-dev \ - protobuf-compiler \ - qt6-tools-dev \ - qt6-tools-dev-tools +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + file \ + g++ \ + git \ + libmariadb-dev-compat \ + libprotobuf-dev \ + libqt6sql6-mysql \ + qt6-websockets-dev \ + protobuf-compiler \ + qt6-tools-dev \ + qt6-tools-dev-tools WORKDIR /src COPY . . -RUN cmake \ - -S . \ - -B build \ - -G Ninja \ - -DWITH_CLIENT=0 \ - -DWITH_ORACLE=0 \ - -DWITH_SERVER=1 \ - && cmake --build build \ - && cmake --install build +RUN mkdir build && cd build && \ + cmake .. -DWITH_SERVER=1 -DWITH_CLIENT=0 -DWITH_ORACLE=0 && \ + make -j$(nproc) && \ + make install # -------- Runtime Stage (clean) -------- FROM ubuntu:26.04 -ARG DEBIAN_FRONTEND=noninteractive - -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - libprotobuf32t64 \ - libqt6sql6-mysql \ - libqt6websockets6 \ +RUN apt-get update && apt-get install -y --no-install-recommends \ + libprotobuf32t64 \ + libqt6sql6-mysql \ + libqt6websockets6 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/cockatrice/src/game_graphics/player/player_graphics_item.cpp b/cockatrice/src/game_graphics/player/player_graphics_item.cpp index 8bf2703e1..026e00588 100644 --- a/cockatrice/src/game_graphics/player/player_graphics_item.cpp +++ b/cockatrice/src/game_graphics/player/player_graphics_item.cpp @@ -187,8 +187,8 @@ void PlayerGraphicsItem::paint(QPainter *painter, const QStyleOptionGraphicsItem QRectF combinedArea(combinedLeft, combinedTop, combinedRight - combinedLeft, combinedBottom - combinedTop); - const QRectF srcRect = PlaymatUtils::computeArtSourceRect(playmatPixmap.size(), playmatParams); - const QRectF dstRect = PlaymatUtils::coverFitRect(combinedArea, srcRect.size()); + const QRectF srcRect = computeArtSourceRect(playmatPixmap.size(), playmatParams); + const QRectF dstRect = coverFitRect(combinedArea, srcRect.size()); painter->save(); painter->setClipRect(combinedArea); diff --git a/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.cpp b/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.cpp index 52f21f714..dc3afc2cd 100644 --- a/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.cpp +++ b/cockatrice/src/interface/widgets/playmat/playmat_preview_widget.cpp @@ -61,8 +61,8 @@ void PlaymatPreviewWidget::paintEvent(QPaintEvent *) // Stack is ~20% width on the left, table is ~80% on the right const QRectF playArea = cardRect.adjusted(6, 4, -4, -4); - const QRectF srcRect = PlaymatUtils::computeArtSourceRect(sourcePixmap.size(), params); - const QRectF dstRect = PlaymatUtils::coverFitRect(playArea, srcRect.size()); + const QRectF srcRect = computeArtSourceRect(sourcePixmap.size(), params); + const QRectF dstRect = coverFitRect(playArea, srcRect.size()); painter.setClipRect(playArea.toRect()); painter.drawPixmap(dstRect, sourcePixmap, srcRect); diff --git a/cockatrice/src/interface/widgets/playmat/playmat_utils.h b/cockatrice/src/interface/widgets/playmat/playmat_utils.h index 0691a9637..9ab8190b3 100644 --- a/cockatrice/src/interface/widgets/playmat/playmat_utils.h +++ b/cockatrice/src/interface/widgets/playmat/playmat_utils.h @@ -6,96 +6,42 @@ #include #include -namespace PlaymatUtils -{ - -/** @brief Upper bound for zooming into the playmat art. */ -constexpr qreal MAX_ZOOM = 4.0; - /** - * @brief Width of the outer viewing window: full card width trimmed by the - * horizontal margins. Guarded against margins summing to >= 1. - */ -inline qreal playmatVisibleWidth(const QSize &fullCardSize, const PlaymatParams ¶ms) -{ - const qreal srcW = fullCardSize.width(); - const qreal marginL = params.marginPctL * srcW; - const qreal marginR = params.marginPctR * srcW; - return qMax(0.0, srcW - marginL - marginR); -} - -/** - * @brief Zoom clamped to the range where every step renders differently. + * @brief Computes the source region of the full-resolution card image to use as a playmat. * - * The square sampling window is visibleWidth / zoom, zooming out past - * visibleWidth / min(card width, height) would sample beyond the card itself, - * which both looks broken and makes whole ranges of the parameter dead. The - * floor is therefore derived from the actual image instead of a static value, - * and is shared verbatim by the render path and the editor's gesture math so - * the two can never disagree. - */ -inline qreal playmatClampedZoom(const QSize &fullCardSize, const PlaymatParams ¶ms) -{ - const qreal minDim = qMin(fullCardSize.width(), fullCardSize.height()); - const qreal visibleW = playmatVisibleWidth(fullCardSize, params); - const qreal zoomOutFloor = (minDim > 0.0 && visibleW > 0.0) ? visibleW / minDim : 1.0; - // The floor deliberately bypasses MAX_ZOOM: when the art is much wider - // than tall, keeping the square window inside it requires more than 4x - // zoom-out, and honoring that larger floor keeps side within - // min(card width, height). Zooming IN is still capped at MAX_ZOOM. - return qMin(MAX_ZOOM, qMax(params.zoom, zoomOutFloor)); -} - -/** - * @brief Side of the square sampling window actually rendered for these - * parameters. Never exceeds either card dimension, so the source rect - * always lies within the image (vertical travel remains for panning - * whenever the art is taller than it is wide). - */ -inline qreal playmatWindowSide(const QSize &fullCardSize, const PlaymatParams ¶ms) -{ - const qreal visibleW = playmatVisibleWidth(fullCardSize, params); - if (visibleW <= 0.0) { - return 0.0; - } - return visibleW / playmatClampedZoom(fullCardSize, params); -} - -/** - * @brief Computes the source region of the full resolution card image to use as a playmat. - * - * Parameters are relative to the full card image. horizontal margins trim the - * card borders (shifting them pans the window), verticalOffset places the top - * edge of the sampling window within its available travel, and zoom scales - * into the trimmed span. The result always lies within the card image bounds. + * Parameters are relative to the full card image: horizontal margins trim the card + * borders, the vertical offset positions a square viewing window, and zoom scales + * into that window. The result is clamped to the card image bounds. * * @param fullCardSize Size of the full card image. * @param params Positioning parameters. - * @return Source rectangle in full card image pixel coordinates. + * @return Source rectangle in full-card image pixel coordinates. */ inline QRectF computeArtSourceRect(const QSize &fullCardSize, const PlaymatParams ¶ms) { const qreal srcW = fullCardSize.width(); const qreal srcH = fullCardSize.height(); - // Square sampling window, keeps art unskewed, never exceeds the card on - // either axis thanks to the zoom floor in playmatWindowSide(). - const qreal side = playmatWindowSide(fullCardSize, params); + const qreal marginL = params.marginPctL * srcW; + const qreal marginR = params.marginPctR * srcW; + // Guard against margins summing to >= 1 (both are individually in range), + // which would otherwise make the viewing window negative or zero. + const qreal visibleW = qMax(0.0, srcW - marginL - marginR); + const qreal visibleH = visibleW; // square viewing window, keeps art unskewed - // verticalOffset places the TOP edge of the sampling window itself within - // its travel, so the full [0, 1] parameter range is live at every zoom and - // the window can always reach the very top (0.0) and bottom (1.0) of the - // art. - const qreal offset = qBound(0.0, params.verticalOffset, 1.0); - const qreal y = offset * qMax(0.0, srcH - side); + const qreal vCenter = params.verticalOffset * srcH; + qreal srcY = vCenter - visibleH / 2.0; + srcY = qBound(0.0, srcY, srcH - visibleH); - // Horizontally the sampling window sits centered inside the trimmed span - // (margins pan it), zooming out can make it wider than that span, so it - // is then kept within the image, an edge stop, never an invalid rect. - const qreal outerW = playmatVisibleWidth(fullCardSize, params); - const qreal x = qBound(0.0, params.marginPctL * srcW + (outerW - side) / 2.0, qMax(0.0, srcW - side)); + // Guard the zoom divisor; everything that produces params clamps zoom to + // [0.1, 4.0] already, this keeps the render path self-contained. + const qreal zoom = qBound(0.1, params.zoom, 4.0); + const qreal zoomedW = visibleW / zoom; + const qreal zoomedH = visibleH / zoom; + const qreal zoomedX = marginL + (visibleW - zoomedW) / 2.0; + const qreal zoomedY = srcY + (visibleH - zoomedH) / 2.0; - return QRectF(x, y, side, side); + return QRectF(zoomedX, zoomedY, zoomedW, zoomedH); } /** @@ -103,7 +49,7 @@ inline QRectF computeArtSourceRect(const QSize &fullCardSize, const PlaymatParam * ratio into dstArea using "cover" semantics (no distortion, overflows cropped). * * @param dstArea Area to fill. - * @param srcSize Size of the source, only its aspect ratio matters. + * @param srcSize Size of the source; only its aspect ratio matters. * @return Destination rectangle centered in dstArea. */ inline QRectF coverFitRect(const QRectF &dstArea, const QSizeF &srcSize) @@ -120,21 +66,4 @@ inline QRectF coverFitRect(const QRectF &dstArea, const QSizeF &srcSize) return QRectF(dstArea.left(), dstArea.top() + (dstArea.height() - dstH) / 2.0, dstArea.width(), dstH); } -/** - * @brief Fits a rectangle of the given aspect ratio into dstArea, centered, - * touching the constraining dimension ("aspect fit" of the FRAME - * itself, not of a source image). - */ -inline QRectF aspectFitRect(const QRectF &dstArea, qreal aspect) -{ - if (aspect <= 0.0) { - return dstArea; - } - qreal w = qMin(dstArea.width(), dstArea.height() * aspect); - qreal h = w / aspect; - return QRectF(dstArea.left() + (dstArea.width() - w) / 2.0, dstArea.top() + (dstArea.height() - h) / 2.0, w, h); -} - -} // namespace PlaymatUtils - #endif // COCKATRICE_PLAYMAT_UTILS_H diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp index 1d76b2c67..ca32edaf1 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp @@ -112,19 +112,6 @@ UserCardArtSettingsDialog::UserCardArtSettingsDialog(const CardArtParams &initia if (!initial.cardName.isEmpty()) { searchBar->setText(initial.cardName); onCardNameChanged(initial.cardName); - - // onCardNameChanged leaves the printing combo on the first printing in - // the database, which would silently change the stored banner card on - // accept. Restore the stored printing when it resolves locally. - const int storedPrintingIndex = providerComboBox->findData(initial.cardProviderId); - if (storedPrintingIndex != -1) { - providerComboBox->setCurrentIndex(storedPrintingIndex); - } else { - // Stored printing not in the local database: keep it rather than - // silently substituting the first printing. - currentParams.cardProviderId = initial.cardProviderId; - reloadPreview(); - } } marginLSpin->setValue(initial.marginPctL); marginRSpin->setValue(initial.marginPctR); diff --git a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp index 875bdfb05..416cd42e3 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp @@ -319,7 +319,6 @@ void UserInfoBox::actBannerCard() if (hasUserInfo && currentUserInfo.has_card_art_params()) { const auto &cap = currentUserInfo.card_art_params(); initial.cardName = QString::fromStdString(cap.card_name()); - initial.cardProviderId = QString::fromStdString(cap.card_provider_id()); initial.marginPctL = cap.margin_pct_l(); initial.marginPctR = cap.margin_pct_r(); initial.verticalOffset = cap.vertical_offset(); diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp index f6f34a6a5..014d3d4c3 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp @@ -189,31 +189,22 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *) // ── Card art background ─────────────────────────────────────────────────── if (!cardArt.isNull()) { - // Same DPR normalization as UserListPainter::drawCardArt: the cache - // carries screen scaled pixmaps on HiDPI displays, the math below is - // in raw pixels. - QPixmap art = cardArt; - art.setDevicePixelRatio(1.0); - const int w = rect.width(); const int h = rect.height(); const int mL = qRound(w * params.marginPctL); const int mR = qRound(w * params.marginPctR); const int dW = w - mL - mR; - const double base = qMax(double(dW) / art.width(), double(h) / art.height()); + const double base = qMax(double(dW) / cardArt.width(), double(h) / cardArt.height()); const double scale = base * params.zoom; - const int sW = qRound(art.width() * scale); - const int sH = qRound(art.height() * scale); + const int sW = qRound(cardArt.width() * scale); + const int sH = qRound(cardArt.height() * scale); - const QPixmap scaled = art.scaled(sW, sH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - // Clamp against stored zoom < 1, which can push srcX negative and silently - // underfill the strip with transparent padding - const int safeSrcX = qBound(0, (sW - dW) / 2, qMax(0, sW - dW)); - const int safeSrcY = qBound(0, qRound((sH - h) * params.verticalOffset), qMax(0, sH - h)); + const QPixmap scaled = cardArt.scaled(sW, sH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + const int srcX = (sW - dW) / 2; + const int srcY = qBound(0, qRound((sH - h) * params.verticalOffset), qMax(0, sH - h)); - QImage img = - scaled.copy(safeSrcX, safeSrcY, dW, h).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); + QImage img = scaled.copy(srcX, srcY, dW, h).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); { QPainter mask(&img); mask.setCompositionMode(QPainter::CompositionMode_DestinationIn); @@ -370,7 +361,7 @@ void UserInfoPopup::buildUi() header = new UserInfoHeaderWidget(this); root->addWidget(header); - // Action area, rebuilt per user + // Action area — rebuilt per user actionArea = new QWidget(this); root->addWidget(actionArea); @@ -411,7 +402,7 @@ void UserInfoPopup::buildUi() root->addWidget(gamesView); - // Close button, positioned absolutely in the top right corner + // Close button — positioned absolutely in the top-right corner closeBtn = new QPushButton(QStringLiteral("✕"), this); closeBtn->setFixedSize(22, 22); closeBtn->setFlat(true); @@ -682,7 +673,7 @@ void UserInfoPopup::showForUser(const QString &userName, gamesStatus->setText(tr("Loading games…")); gamesStatus->show(); - // Close button, top right corner, above everything + // Close button — top-right corner, above everything closeBtn->move(PopupWidth - closeBtn->width() - 6, 6); closeBtn->raise(); @@ -711,7 +702,7 @@ void UserInfoPopup::fetchGames() void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser) { if (forUser != currentUser) { - return; // stale response, different user showing now + return; // stale response — different user showing now } gamesModel->clear(); @@ -772,4 +763,4 @@ void UserInfoPopup::leaveEvent(QEvent *e) { QFrame::leaveEvent(e); emit mouseLeftPopup(); -} +} \ No newline at end of file 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..5a4723065 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp @@ -155,12 +155,6 @@ void UserListPainter::drawCardArt(QPainter *painter, return; } - // CardPictureLoader::getPixmap tags its output with the screen's - // devicePixelRatio on HiDPI displays. Every calculation below is in raw - // pixels, so normalize to 1.0 or the crop renders at 1/dpr scale anchored - // to the top left corner of the row. - art.setDevicePixelRatio(1.0); - const int cardH = rect.height() - 4; const int totalW = cardRight - rect.left(); const int marginL = qRound(totalW * params.marginPctL); @@ -178,14 +172,11 @@ void UserListPainter::drawCardArt(QPainter *painter, const int srcX = (scaledW - drawW) / 2; const int srcY = qRound((scaledH - cardH) * params.verticalOffset); - // Clamp so we never copy outside the pixmap bounds. srcX can go negative - // for stored zoom values below 1, which would silently underfill the - // strip with transparent padding. - const int safeSrcX = qBound(0, srcX, qMax(0, scaledW - drawW)); + // Clamp srcY so we never copy outside the pixmap bounds const int safeSrcY = qBound(0, srcY, qMax(0, scaledH - cardH)); QImage img = - scaled.copy(safeSrcX, safeSrcY, drawW, cardH).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); + scaled.copy(srcX, safeSrcY, drawW, cardH).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); { QPainter mask(&img); @@ -412,4 +403,4 @@ void UserListPainter::paint(QPainter *painter, drawBadges(painter, option, rect, cardRight, badges, online, style); painter->restore(); -} +} \ No newline at end of file diff --git a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt index 60760b5bd..8389fcf10 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt +++ b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt @@ -14,7 +14,6 @@ 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_player.h game/server_spectator.h server.h diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp index 43209e994..425fefcb3 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -63,9 +63,7 @@ Server_Game::Server_Game(const GameConfig &config, Server_Room *_room) 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), - lifecycleStrategy(new Server_DefaultLifecycleStrategy), matchResultStrategy(new Server_NullMatchResultStrategy), - gameMutex() + lifecycleStrategy(new Server_DefaultLifecycleStrategy), gameMutex() { currentReplay = new GameReplay; currentReplay->set_replay_id(room->getServer()->getDatabaseInterface()->getNextReplayId()); @@ -393,12 +391,10 @@ void Server_Game::stopGameIfFinished() QMutexLocker locker(&gameMutex); int playing = 0; - Server_AbstractPlayer *lastPlayer = nullptr; auto players = getPlayers(); for (auto *player : players.values()) { if (!player->getConceded()) { ++playing; - lastPlayer = player; } } if (playing > 1) { @@ -414,16 +410,6 @@ void Server_Game::stopGameIfFinished() sendGameStateToPlayers(); - bool matchDecided = matchResultStrategy->onGameFinished(this, playing, lastPlayer); - if (matchDecided) { - locker.unlock(); - - sendGameEventContainer(prepareGameEvent(Event_GameClosed(), -1)); - gameClosed = true; - deleteLater(); - return; - } - locker.unlock(); ServerInfo_Game gameInfo; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h index 1b9f651bd..8ed0769a6 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h @@ -24,7 +24,6 @@ #include "game_config.h" #include "server_deck_validation_strategy.h" #include "server_game_lifecycle_strategy.h" -#include "server_match_result_strategy.h" #include #include @@ -87,8 +86,6 @@ private: QScopedPointer lifecycleStrategy; - QScopedPointer matchResultStrategy; - void createGameStateChangedEvent(Event_GameStateChanged *event, Server_AbstractParticipant *recipient, bool omniscient, diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_match_result_strategy.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_match_result_strategy.h deleted file mode 100644 index 51c696db1..000000000 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_match_result_strategy.h +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef SERVER_MATCH_RESULT_STRATEGY_H -#define SERVER_MATCH_RESULT_STRATEGY_H - -class Server_AbstractPlayer; -class Server_Game; - -/** - * @brief Strategy hook invoked when a game has finished to decide the match result. - * - * Subclasses can report the match outcome (e.g. to a tournament backend) and decide - * whether the game should be closed permanently; the default implementation never - * closes the game, preserving the normal return-to-lobby behavior. - */ -class Server_MatchResultStrategy -{ -public: - virtual ~Server_MatchResultStrategy() = default; - - /** - * @brief Called when a game has finished. - * @return Whether the game has been decided and should be closed. - */ - virtual bool onGameFinished(Server_Game *game, int playing, Server_AbstractPlayer *lastPlayer) = 0; -}; - -/** - * @brief Default match result strategy that never closes the game. - */ -class Server_NullMatchResultStrategy : public Server_MatchResultStrategy -{ -public: - bool onGameFinished(Server_Game *, int, Server_AbstractPlayer *) override - { - return false; - } -}; - -#endif