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