Commit graph

232 commits

Author SHA1 Message Date
Bryce!
3d8c729517
[Client] Preview token edits live and preserve custom drafts (#7737)
* Preview token edits live and preserve custom drafts

* Restore the upstream reveal-random-hand-cards action

The rebase onto upstream/master took the PR side of player_actions.cpp and
dropped PlayerActions::actRevealRandomHandCards, which upstream added after
the PR merge base. Re-apply the face-down P/T fix on the current file so the
branch keeps both changes.

* Draw the token preview P/T as the hover-panel pill

The preview painted the name and P/T unconditionally: the name duplicated the
artwork, and the raw P/T text was drawn even when it matched the printed value,
which looked wrong at non-default dialog sizes.

- Suppress the card name over a token picture via a small AbstractCardItem hook;
  cards without art still show it.
- Draw only a P/T that differs from the printed getPowTough() as the #7700
  hover-panel pill: dark rounded scrim, bold white text, anchored on the printed
  P/T box and clamped inside the card.
- Use designated CardRef initializers in the new code.

The face-up draft still previews while face-down creation is selected.

---------

Co-authored-by: Lukas Brübach <44814898+BruebachL@users.noreply.github.com>
2026-11-09 12:59:53 +01:00
sentry[bot]
9793a0df4a
[Crash] Add lifecycle breadcrumbs for unattributable native crashes (#7813)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / vcpkg baseline (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Fedora 45 release (push) Blocked by required conditions
Build Desktop / Fedora 45 debug (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 sanitize (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 27 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Crash] Add lifecycle breadcrumbs for unattributable native crashes

Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>

* Restore the native split host button on non-macOS styles

The merged quick-host control composed a QPushButton and a separate autoRaise arrow QToolButton, which Fusion draws as two adjacent controls. Version one of the quick-host branch used a single QToolButton with MenuButtonPopup, which every style draws as one native split button.

Use that split button on every style that can draw one, and keep the plain primary + settings button pair as the native macOS fallback.

* Enable hover zoom and the hover panel by default

Both hover triggers now default to Hover, with the existing 400 ms zoom delay and 200 ms panel delay. The Off trigger remains available for users who want the old behavior.

---------

Co-authored-by: Lukas Brübach <44814898+BruebachL@users.noreply.github.com>
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
2026-11-08 23:06:21 +01:00
BruebachL
40ebd22549
[Lobby] Add instant quick host to the game room (#7802)
* Add instant quick host to the game lobby

* Use a native pull-down on macOS and a split button elsewhere

- Gate on the active application style instead of the platform, so
  choosing Fusion on macOS still gets the split button: QMacStyle ignores
  autoRaise/flat and draws a QToolButton menu as a separate square.
- Rebuild the host control on QEvent::StyleChange, because the appearance
  settings switch styles while the room is open.
- Keep the review action, its arrow and the host button disabled while a
  host or join command is still in flight.
- Announce inline quick-host failures to screen readers.

* Address review findings

* Replace the macOS pull-down with a sibling settings button

QMacStyle has no split-button rendering: a menu-bearing QToolButton is drawn as a second square bezel. Keep the primary Host game button as the one-click quick host on every platform, and show a plain Game settings button as the secondary action on macOS instead; other styles keep the split arrow.

Rename the secondary action from "Review settings first..." to "Game settings..." so the label says what it opens, and update the shortcuts page label to match.
2026-11-08 22:12:29 +01:00
BruebachL
b1c8f156dc
[Game] Add hover zoom and a tabbed card panel to the board (#7700)
* [Settings] Add hover zoom/panel triggers, delays, and double-confirm

* [Game] Smooth board hover-zoom with fade helpers

* [Game] Add hover card panel with tabbed actions

* [Game] Wire hover zoom/panel triggers through the scene

* [Game] Repaint cached zones when the hover zoom moves

`TableZone` cuts the hovered card out of its own paint so zone washes,
indicators, dimming and the land divider never draw over the zoom. It is a
`DeviceCoordinateCache` item, so the card changing geometry never dirtied it
and the stale hole stayed transparent, uncovering the black viewport as soon
as the zoom crossed a zone edge.

- Emit `hoverFootprintChanged(old, new)` from `setCardScale()` and once from
  `setHovered(false)`.
- Repaint every overlapping `TableZone` on that signal.
- Clip with the card's painted outline instead of its bounding rectangle, and
  treat a zoom covering the whole zone as "paint nothing".

* [Interface] Keep the hover panel P/T controls out of the way

The P/T set/inc/dec rows were always visible and sat under the cursor, so
clicking a card to select it could mutate its P/T instead.

- Collapse the controls to a pill on the printed P/T box, anchored on the
  drawn artwork so letterboxed and sideways cards stay aligned.
- Open the rows only from the pill and grow them upward, and re-measure the
  pill on every value so three-digit P/T does not clip.
- Close the panel when another card is hovered, and repaint the area it
  covered so no patch is left behind.

* [Interface] Address hover panel review findings

* [Game] Fix include order flagged by the format gate

The card hover widget header was included before theme_manager.h, which
clang-format rejects; the branch's format CI job failed on it.

* [Interface] Load the hover panel icons through the themed icon engine

The panel still named pre-#7595 assets (arrow_*_green, arrow_right_blue,
cogwheel) and built QIcon snapshots from themePixmap(), so the deleted names
rendered blank and every other glyph froze one palette.

Load every glyph through loadThemedIcon() with the emphasis axis the adoption
plan calls for: the category tabs take the primary level as panel wayfinding,
the action grid the quiet level because its glyphs label the button text.
Semantic tints keep their family and follow the same quiet level; Clone keeps
its sticker treatment.

* [Icons] Add an Informational semantic color family

Face-down play and the exile zone are states that are neither a gain nor a
loss, and the pre-#7595 art painted them blue (arrow_right_blue). #7595 dropped
that hue, and the icon engine has had only Positive and Negative since, so both
states fall back to neutral ink.

Add the family with the same three-level ladder the other semantic roles use:
authored roles per theme, scheme defaults otherwise, an emphasis mapping, the
palette-editor rows and the generator seed. The defaults clear the 4.5:1 label
floor on both schemes and the ladder's saturation ordering; the existing
emphasis, hue and preview tests now cover the new family. Preview sheets that
name roles resolve an unauthored one through the same default the client
paints.

* [Interface] Split the hover panel icons by emphasis and semantics

Category tabs only navigate between pages, so they take the quiet level; the
action grid is the surface the player works in and takes the primary level. Both
stay line glyphs: a filled treatment reads as a second state on every cell.

Hue marks an outcome where the glyph cannot. Play, clone and to hand are
Positive; reduce life and graveyard Negative; play face down and exile
Informational. Direction-only moves (top and bottom of library) stay neutral
because the arrow already carries the meaning.

* [Icons] Add Hidden and Marked semantic color families

Two game concepts had no hue of their own. Hidden information (a face-down
play, peeking at a card face) now has violet, and the marks a player leaves on a
card (annotation, draw arrow) now share amber instead of splitting blue and red
for the same job.

Both families follow the existing three-level ladder: authored roles per theme,
scheme defaults otherwise, an emphasis mapping, palette-editor rows and generator
seeding. The defaults clear the 4.5:1 floor on the representative platform
surfaces and keep the saturation ordering the emphasis tests assert; the hue
tests pin violet's blue-red-green dominance and amber's red-green-blue one, and
every preview sheet now renders the two families.

The hover panel grid preview is added alongside: it renders the real pages
(section headers, two-column cells, glyph over label) so the whole map can be
judged at once instead of glyph by glyph.

* [Icons] Add a Movement semantic color family

A card changing position inside the library is neither a gain, a loss nor
information, so it had no hue. Movement names it with teal and covers the three
library placements (top, bottom, X from top).

It follows the existing ladder: authored roles per theme, scheme defaults
otherwise, an emphasis mapping, palette-editor rows and generator seeding. The
defaults clear the 4.5:1 floor on the representative platform surfaces, keep
strong > base > soft saturation, and keep green > blue > red so teal cannot be
mistaken for the green gain family.

* [Interface] Give every hover panel action a semantic color

The quick actions are the panel's whole point, so a neutral glyph made the
most-used buttons the hardest to scan. Every action now names a game fact with
its hue: gain, loss, information, hidden information, a mark on the card or a
move. The line glyph stays identical, so the colour is the only difference
between neighbours.

The grid is also ordered by family so a row reads as one idea: the two state
toggles, the two gains, the two marks, the two hidden-information actions and
the two losses each share a row. Conditional actions (Peek, Unattach) sit at the
end of their pair, so a missing one leaves a hole at the section edge instead of
splitting a row. In a zone, Play and Play Face Down stay adjacent on purpose:
the same action, one of them hiding information. The move tab leads with the
three library placements (Movement), then the destinations.

actionGridIcon is removed; semanticActionIcon is now the only grid ink. The
preview renders the real pages so the whole map can be judged at once.

* [Icons] Use quiet emphasis for glyphs that label rather than act

The search and info glyphs inside the search fields, and the combo-box
chevron, label or afford an action rather than performing one. At the
default secondary weight they compete with the actual controls next to
them. Paint them at quiet emphasis so they stay legible without claiming
attention.

Sites: deck editor, visual database display, visual deck storage, set
manager, settings search, in-game zone view, and the dropdown chevron.

* [Icons] Restore sticker treatment to the add, remove and delete actions

The pre-#7595 add and delete glyphs were filled shapes with an outline.
After the line migration they read as ordinary toolbar glyphs, losing the
weight their verbs had. Give the add and delete family the sticker
treatment again.

- Deck editor Add card: primary positive sticker. It is the one action
  the database panel exists for.
- Visual database Clear filters: negative sticker, matching the delete
  family.
- Filter dock Clear all: negative sticker, matching Clear one.

* [Icons] Give sharing and pinning their semantic families

Sharing is an informational act: the app already tints reveal and
make-public with the informational family, so the deck storage share
action, the replay share-code action and the visual deck storage share
button follow it.

The printing-selector pin badge marks a card as pinned. Paint it with the
marked family, the same one used for a mark left on a card, instead of
leaving it neutral.

* [Game] Dismiss the hover panel before opening a modal dialog

The hover panel is a Qt::ToolTip window, i.e. a popup: it is always on
top and holds the input grab. Actions on the panel that open a modal
dialog (Set power/toughness, Set annotation, Set counter value, token
creation) therefore opened the dialog behind the panel, where it could
not be seen or activated.

Hide the panel synchronously and defer the action to the next event-loop
turn, so the popup grab is released before the dialog's event loop
starts. The panel asks the scene to dismiss it through a new
dismissRequested signal, keeping hover timers and the covered-card fade
on the scene's side.

Reported from the P/T overlay SET button; the same path covers the
counter tab and the token tab.

* [Icons] Refine VDE and VDD emphasis and semantics

The visual database filter section and the syntax help are information
aids, so paint them with the informational family at the default
emphasis: the filter section glyph and the search field's help glyph.
The help glyph leaves quiet emphasis because it is a clickable action,
not a field label.

The visual deck editor's display-type toggle is the one control that
changes the whole view, so it takes primary emphasis.

The search glyphs stay quiet; only the help action changes.

* [Icons] Raise the visual database filter controls to primary

The six quick-filter buttons are the controls the filter section exists
for, so paint them at primary emphasis instead of the default weight.

The sort glyph carries the same informational family as the filter
glyph, in both the visual database toolbar label and the visual deck
editor's sort-criteria button.

* [Icons] Keep the display options sort glyph neutral

The sort button beside the display-mode toggle is not part of the
informational family. Only the visual database toolbar's sort glyph
carries informational; the display options button returns to the
default neutral weight.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-08 21:51:31 +01:00
BruebachL
6138fbd0a0
[Card] Add Scryfall Tagger tag search and filtering (#7337)
* [Card] Import Scryfall Tagger tags from MTGJson

* [Card] Search cards by Scryfall Tagger tag

* [Card] Add a Tag attribute to the card filter tree

* [Card] Add a Tags filter to the visual card database

* [Oracle] Add a script to inject Scryfall tags into AllPrintings

* [Card] Elide long tag lists in the card info and drop the Scryfall label from the tag filter tooltip

* [Oracle] Load card tags from a tag database during import

Add a TagDatabase loader that understands Scryfall's Oracle Tags bulk data and user-supplied tag files (tag objects, flat oracle-id or name maps, optional manifest, JSON or JSONL, gzip/xz/zip). The importer joins the slugs on identifiers.scryfallOracleId while the identifiers are still available, falls back to the card name, and unions them with any tags the source already provides. Split-card faces merge through the existing tag union path.

This replaces oracle/scripts/inject_scryfall_tags.py, which patched AllPrintings.json before import. Loading during import keeps tags on by default for every source and lets users pair Oracle with any tag database.

* [Oracle] Add a tag data step to the import wizard

Insert a Tags page between set selection and the import. It downloads Scryfall's Oracle Tags by default (or a user-supplied file/URL), can include parent tags, and persists tagsenabled, tagssource and tagsparents in oracle.ini. The page loads asynchronously and soft-fails in background mode, so a tag download or parse error never aborts the card database import.

* [Help] Describe tag search without naming Scryfall

Tags can come from Scryfall's Oracle Tags or any user-supplied tag database, so the search help should not claim a single source.

* [Oracle] Fix Scryfall tag requests and the retry loop

api.scryfall.com rejects requests that lack an Accept header with HTTP 400, and QNetworkAccessManager does not add one. Send application/json for the bulk metadata request and */* for the tag file, and surface the URL plus Scryfall's JSON error body when a request fails.

A failed tag load used to call next(), which re-ran validatePage() and started the same failing download again, trapping the user in an error loop. Stay on the page instead, drop the unusable database, and tell the user they can retry, change the source, or untick the option to import without tags. Also explain parent tags inline with a real Scryfall hierarchy example.

* [Oracle] Give the parent-tag explainer room to breathe

The wrapped explainer sat flush under the checkbox with no separation from the status label, which made the paragraph look cramped. Indent it under the checkbox and add a little vertical margin.

* [Oracle] Stop the parent-tag explainer from being clipped

A word-wrapped QLabel reports a one-line sizeHint, so the wizard page reserved too little height and the explanation lost its top and bottom lines. Pin the vertical size policy to Minimum on the explainer and on the status label, so the wrapped height becomes part of the page minimum. Verified with a layout probe: the label went from 28 px tall (needed 56) to its full height.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-08 20:57:45 +01:00
BruebachL
c7e8422808
[Icons] Author icon emphasis colors as palette roles (#7789)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / vcpkg baseline (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Fedora 45 release (push) Blocked by required conditions
Build Desktop / Fedora 45 debug (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 sanitize (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 27 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Icons] Author icon emphasis colors as palette roles

The emphasis levels were computed from the accent at paint time, so a
theme could not vary them: every theme got the same derived strong and
soft tones, and a semantic hue was pushed toward the accent whether the
author wanted that or not.

Give each family three authored roles instead. Icon/IconStrong/IconSoft
carry the neutral glyph ink and Positive/Negative gain the same Strong
and Soft companions. Emphasis becomes a lookup inside the family, and a
caller that names a variant pins that level at every emphasis. The paint
path returns the authored color verbatim; only the filled treatments keep
their saturation floor, because carrying a disc or a fill is a treatment
decision.

Shipped palettes author the full ladder for both schemes; the palette
editor exposes the new rows and the generator writes them. System keeps
deriving its neutral ink from the platform palette, since it ships no
palette of its own.

* [Icons] Keep the authored emphasis in filled treatments

The saturation floor was added while the emphasis levels were still
computed: it rescued the pale dark-scheme tones once a whole shape
carried them. With the levels authored, the floor overrides the author
instead: a quiet level jumped to the secondary saturation as soon as a
sticker or a badge painted it, so the ladder only existed for line
glyphs.

The authored tone is now the top of the fill gradient and the treatment
only darkens the bottom for depth, so every treatment renders exactly the
level the palette stores. The badge glyph ink no longer guesses the
scheme from the tone's value: an authored quiet level is a dark color on
the dark scheme too, so the scheme is passed in from the surface palette.
The preview sheet renders every shipped palette theme, three glyphs per
family, and each row prints the authored tone and the darker bottom.

* [Icons] Order the emphasis ladder by chroma, not contrast

Contrast cannot order the ladder. On a dark surface the highest-contrast
tone is the palest one, so a 7:1 primary came out near-white while the
secondary kept the family's color; the ladder read inverted, and for the
shipped semantic greens and reds the primary level was already satisfied
by the secondary, so the two levels resolved to the same hex.

Emphasis is chromatic intensity now. Quiet recedes toward the surface at
the 3:1 non-text floor, secondary keeps the family color at the 4.5:1
text floor, and primary is the most chromatic tone the same floor allows.
Every level stays legible on the window and the button surface, and the
primary is a strictly more saturated member of its family on both
schemes. The reformatted palettes move the two upper levels onto the
shared floor and leave the quiet level alone.

* [Icons] Make the emphasis ladder structural on every theme

The chroma ladder landed with four P1s from review: reachContrast picked
its pole by background lightness instead of by what each pole can reach,
the generated neutral ink was solved on the window surface only, Fusion's
neutral family was still the green accent, and quiet sat below the text
floor while disabled reused the palette gray.

Pick the pole by achievable contrast, solve fallback and preview inks on
the harder of the window and button surfaces, re-author Fusion's neutral
trio in slate blue, and keep quiet on the shared text floor by muting its
saturation to 45% instead of blending it into the surface. Disabled is now
derived from the quiet ink, so it is structurally dimmer than every
enabled level on every theme.

Re-author all soft roles at a 4.60 target with a rounding margin, rebuild
the per-theme preview generator the rebase dropped, and add a Disabled row
to every treatment sheet.

* [Icons] Render preview glyphs at the sheet's device pixel ratio

The preview sheets paint on a 2x painter but rasterized each glyph at its
logical size, so every icon was upscaled on the way into the file and the
sheets read as blurry. Render each glyph at device size and tag the pixmap
with the ratio, so one source pixel lands on one file pixel.

* [Icons] Keep the System semantic ladder on the text floor

System authors no palette of its own, so its semantic family is fixed data
measured against whatever surfaces the platform provides. The ladder that
landed with the authored roles reused the old contrast-ordered values:
PositiveSoft and NegativeSoft sat at 2.76–3.90:1 on common light and dark
platform surfaces, and the strong level was less saturated than the
secondary, which is the inversion the chroma rule removes.

Re-author the six default semantic colors per scheme at the same 4.60
target the shipped themes use, against representative platform surfaces,
and mirror them in System's palette files. The new
DefaultSemanticFamilyMeetsTheLadderOnPlatformSurfaces test holds the floor
and the saturation order there, and shippedPalette's early return now says
why System is outside the authored-palette assertions.

* [Icons] Apply the CI clang-format output to the new tests

* [Icons] Adapt the treatment preview to the authored roles

The preview file is inherited from the treatment PR. Here the palette already
authors the fill tone and the badge takes an explicit scheme, so the generator
stops re-saturating the role and passes the scheme the sheet renders.

* [Icons] Author neutral System icon ink

The System theme derived icon ink from the platform palette, which rendered blue under the default palette. Author Icon/IconStrong/IconSoft explicitly: a black ladder in light mode, a white ladder in dark mode. Pin authorship, achromaticity and ordering in the render test.

* Address review findings
2026-11-08 19:09:02 +01:00
BruebachL
3e2884e6e1
[Icons] Paint neutral glyphs with icon ink and add an emphasis axis (#7788)
* [Icons] Paint neutral glyphs with icon ink instead of body-text ink

Neutral icons resolved `currentColor` straight to `QPalette::WindowText`. A
glyph is not a word: it is a 16-24px mark drawn with a 2-unit stroke, so
antialiasing thins it until it reads lighter than the text beside it, in the
one color the palette reserves for reading. The result is that icons recede
into the chrome exactly where they should anchor it.

Add `theme_icon_ink`, one rule with one job: mix a share of the theme accent
into the neutral text color, then hold the result to a contrast floor. The
accent share gives the glyph chroma the text does not have, so it reads as an
icon rather than as leftover text, and it is derived from `QPalette::Highlight`
rather than a new token. The floor keeps a theme with a weak text color from
producing an illegible glyph.

Only the neutral label roles (`WindowText`, `Text`) are inked. Semantic tints
are hues the call site chose on purpose, disabled uses the disabled group, and
the selected-row swap to `HighlightedText` is unchanged.

The WCAG contrast math moves from `theme_icon_badge` to the new unit, so badge
ink decisions and the floor share one implementation.

Tests: the ink rule against every shipped palette, the weak-text floor, and the
palette-following render tests now ask the rule instead of restating its
output. A preview sheet sweeps the accent share at 0-35% over window and button
surfaces for both schemes.

* [Icons] Add an emphasis axis for icon ink

Tint says which hue, style says how much shape, and nothing said how loud.
Call sites that have a hierarchy (a card hover panel's nav strip, the visual
database filter toolbar) could only pick between "the theme color" and
"another theme color", so "this is the important one" had no way to be
expressed and every glyph in a surface arrived with the same weight.

Add `IconEmphasis` as the third axis: Quiet, Secondary, Primary. It travels
inside `IconTint` instead of becoming a fourth `loadThemedIcon` parameter, so
every existing call site keeps working and its look, and emphasis reads as
part of the same "how should this glyph be inked" decision as the tint.

- Primary lifts a neutral glyph to `QPalette::Highlight`, so the surface's own
  action color names the primary action instead of a second gray token, and
  pushes both neutral and tinted glyphs to a 7:1 floor.
- Quiet blends toward the surface and stops at the 3:1 WCAG non-text floor,
  for glyphs that label a control rather than act (the magnifier in a search
  field, a section marker).
- Secondary is the previous color verbatim.

The default ink borrows enough accent to sit off the palette's pole (5.4:1 on
light, 6.4:1 on dark), so the 7:1 primary floor is measurably above it in both
schemes instead of being lost against a near-black or near-white default. The
tests pin that order where it exists, and the hue shift still separates the
levels at a glance.

Disabled glyphs and the Selected -> HighlightedText swap are unchanged at
every emphasis level.

* [Icons] Render the emphasis sheet for every shipped theme

The preview only rendered one theme, so a reviewer could not see that the
emphasis levels behave the same way across Fabric, Fusion, Leather,
Plasma and VelvetMarble. It now writes one sheet per theme and scheme:
the neutral, positive and negative families at all three levels, three
glyphs per family, as line, sticker and badge at 16 and 24 px, on the
window and button surfaces.

* Address review findings
2026-11-08 19:09:01 +01:00
BruebachL
e81ee02c24
[Icons] Tint semantic glyphs and add badge and sticker treatments (#7775)
* [Icons] Keep semantic red and green on themed glyphs

The one-glyph cutover recolored every icon from a single palette role, so a
disabled deck, a not-ready player and a remove button all lost the red or green
that carried their meaning.

Add Positive and Negative application colors beside the accents, ship
scheme-appropriate defaults for every theme, and resolve them through
ThemeManager. Icons now take an IconTint: a palette role for structural glyphs,
or a semantic role for states and add/remove actions. A disabled semantic icon
falls back to the disabled foreground; on a selected row a semantic tint keeps
its color only while it stays legible on the highlight, and a palette tint keeps
the existing WindowText -> HighlightedText swap.

* [Icons] Render a semantic-tint sheet for every shipped theme

The PR showed two sheets for one theme, which cannot show that the shipped
reds and greens stay legible on every palette. Each shipped theme and scheme
now gets its own sheet: both families, three glyphs, at 16 and 24 px, on the
window and button surfaces the contrast is measured against, with the
disabled tone beside each family.

The generator lives in its own test file so the preview work does not have to
move the branches above it.

* [Icons] Add a badge icon treatment

Restore the filled-disc look the ready/not-ready icons had before the
one-glyph cutover, without bringing back palette-locked art.

IconStyle::Badge composes the existing sentinel SVG at paint time: a
gradient disc, a dark ring, and the glyph re-weighted and inked for
contrast against the tone. Line stays the default and is unchanged.

Only the two state icons opt in. The add/remove icons keep the line
treatment because their old art was a filled glyph with an outline and
no disc, so badging them would invent a look the UI never had. Disabled
badges fall back to the line glyph so an unavailable control still reads
as unavailable.

* [Icons] Add a sticker icon treatment

The pre-#7595 add, remove and delete icons were filled shapes with a black
outline and no disc. A tint cannot bring that back, and the badge disc is a
different icon language, so add a third value to the existing style axis.

IconStyle::Sticker draws the sentinel glyph twice: a near-black rim at the
band width plus one rim on each side, then the tone pass clipped to the glyph
and filled with a vertical gradient. Both treatments keep the disabled
fallback to the line glyph.

The fill rule moves into theme_icon_fill: the semantic tones are tuned for
text, so a filled shape gives its tone a saturation floor, and a dark-scheme
tone is pulled down rather than up. IconTint::resolve() applies the same floor
to line glyphs, and the badge disc uses the same fill, so a line, a badge and
a sticker next to each other carry the same green or red instead of a pale
line beside a saturated glyph.

The badge's glyph ink follows the scheme: white on light-scheme tones, as in
the pre-#7595 art, and the saturated tone darkened to an outline-dark ink on
dark-scheme tones, where the shared saturated disc is light enough that white
would sit near 2.7:1. The new test pins both cases so the glyph cannot
silently flip.

resolveSvgStrokeWidth() moves the stroke-width rewrite into
libcockatrice_utility so the badge and the sticker share it; the badge's local
copy is gone.

The sticker is opted into at 17 call sites: increment, decrement and
remove_row in the deck editor, settings pages, token dialog, server dialogs
and replay/deck storage tabs. The visual database's clear-filter button keeps
its trash can (icons/delete) in the line treatment: that glyph is new in
#7595, but it is the clearer metaphor, and a sticker rim closes its lid gap
at toolbar sizes.

The preview generators also gain a semantic-tint sheet for #7775 and a
line/badge/sticker family strip, so the three PR descriptions can show the
same rendered colors.

* [Icons] Render authored theme SVG art without the SVG image plugin

`QPixmap("theme:file.svg")` needs Qt's SVG *image-format* plugin. Debian
builds that as the separate `qt6-svg-plugins` package, which is only a
Recommends of `qt6-svg-dev`, so a `--no-install-recommends` install (CI,
minimal user installs) does not have it. Authored theme art then loaded as
a null pixmap: the two authored-art render tests failed on Debian 13 and on
the Ubuntu sanitize leg while passing on a developer machine whose Qt
bundles the format.

Render authored SVG assets through QSvgRenderer instead, which is already
linked for the sentinel path and needs no plugin. Raster assets keep the
QPixmap loader. The test suite now hides the plugin directories, so the
dependency cannot come back unnoticed on platforms that ship the plugin.

* [Icons] Render a badge/sticker sheet for every shipped theme

The PR showed one theme's badge and sticker sheets, which cannot show that the
filled treatments read on every palette. Each shipped theme and scheme now
gets its own sheet: both semantic families, three glyphs, as line glyphs,
stickers and badges, at 16 and 24 px, on the window and button surfaces the
contrast is measured against, with the disabled fallback under each family.

The generator lives in its own test file so the preview work does not have to
move the branches above it.

* [Icons] Show the trash can with the sticker treatment again

The sticker sheet kept the delete glyph out of the sticker rows and explained
why: at toolbar sizes the rim closed the lid gap. The icon is used with the
sticker treatment again for destructive actions, so the sheet now shows it and
the exception note is gone.

* [Icons] Apply the CI clang-format output to the treatment preview

clang-format 18 wraps the family label and the glyph x offset differently from
the local clang-format 23. The preview test takes the CI output so the bottom
of the stack passes the style job on its own.

* [Icons] Rewrite the icon role docs in plain language

Address review: drop the AI-cadence prose from the IconStyle/IconTint and treatment headers, use ///< for the enum member docs, and move theme_icon.cpp's anonymous namespace above the IconTint definitions. No behavior change.

* Address review findings

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-08 19:09:01 +01:00
Bryce!
1ed7e45357
[PictureLoader] Improve image negotiation, cache recovery and redirects (#7703)
* Fix picture-loader cache formats, redirects and offline queue handling

* Format picture-loader changes using project style

* Address review: name decoded image fields, test cancelled requests

Use designated initializers for DecodedImage so a field reorder cannot
silently swap the image and its error string.

Extract the cancelled-request sweep shared by the dispatcher and the
single-request path into takeCancelledRequests(), and cover it: one status
retirement per vanished work item, in queue order, with live entries kept.

---------

Co-authored-by: Lukas Brübach <44814898+BruebachL@users.noreply.github.com>
2026-11-08 19:02:07 +01:00
Bryce!
078d0858a3
[Game] Reveal multiple distinct random hand cards to a recipient (#7753)
* Add distinct random hand reveals with private recipient delivery

* Route random hand reveals through PlayerActions

Keep command creation, dispatch and response handling in the game action
layer. HandMenu requests the typed operation and presents the count and
failure dialogs. Show the chosen recipient, distinguish the count action
from the immediate single-card reveal, and use the game window as parent.

* Apply project formatting to random-reveal warning construction

* Simplify random reveal to follow existing command patterns

Forward server dispatch through cmdRevealRandomCards and use ordinary PlayerActions command sending. Remove the bespoke server-version error UI and long-lived-object guards.

Reduce the test from 246 to 150 lines with two focused cases using the current RoleInfo player API, preserving distinct-card, private-event and rejected-count coverage.

* Extract the shared reveal helper and name the test's CardRef fields

RickyRister asked for cmdRevealCards and cmdRevealRandomCards to share a
helper instead of threading a randomCount parameter through the reveal
virtual. Restore the three-argument cmdRevealCards signature, resolve the
reveal preconditions and zone in resolveRevealZone, and let both commands
build their card list and hand it to revealCards, which owns the private and
public Event_RevealCards construction and the enqueue. The random command no
longer fabricates a Command_RevealCards with the -2 sentinel to re-enter the
legacy path; its behaviour is unchanged.

Name the CardRef fields in random_reveal_test instead of relying on
declaration order, and state in the user documentation that a rejected
reveal is not reported in the client, matching what the client does.

---------

Co-authored-by: Lukas Brübach <44814898+BruebachL@users.noreply.github.com>
2026-11-08 16:41:14 +01:00
BruebachL
39220a2ea9
[Refactor] Split CreateGameFormWidget out of DlgCreateGame (#7801)
* Extract CreateGameFormWidget from DlgCreateGame

* Address review findings
2026-11-08 16:22:31 +01:00
BruebachL
554368967d
[Refactor] Extract GameCreationOptions from DlgCreateGame (#7800)
* Extract GameCreationOptions from DlgCreateGame

* Address review findings
2026-11-08 16:22:30 +01:00
tooomm
aa0c8dc7cb
[CI] Pin used GH Actions (#7716)
* pin used actions

plus some reordering and harmonization across files

* [CI] Link the version string and RNG stub into the test targets

The distribution libgtest.a now used by the Linux images makes the linker
pull more translation units out of libcockatrice_network than the fetched
googletest did. Those units reference VERSION_STRING and the global rng,
which several test targets never defined, so the instrumented Ubuntu 26.04
legs failed at link:

  undefined reference to `VERSION_STRING'
  undefined reference to `rng'

Add the generated version string source wherever a test links the network
umbrella, and define the rng stub in the tests that pull the server game
classes without declaring it. This matches what the neighbouring server
tests already do.

A full Debug build and ctest suite in the ci-ubuntu2604 image (system
libgtest.a) is green: 841 tests pass.

Fixes the nightly failures introduced by #6894.

---------

Co-authored-by: Lukas Brübach <44814898+BruebachL@users.noreply.github.com>
2026-11-08 15:45:54 +01:00
tooomm
fb3f895cfd
[CI] Update Linux Dockerfiles (#6894)
- Install the distribution's gtest (libgtest-dev / gtest-devel) in the
  Debian, Ubuntu and Fedora images, so configuring the test suite no
  longer downloads googletest; Arch already had it.
- Drop the toolchain meta packages. g++, Ninja and cmake are all that is
  used, and make is not: Ninja is the generator on every Linux leg. The
  DEB legs keep dpkg-dev, which CPack needs for dpkg-shlibdeps when
  CPACK_DEBIAN_PACKAGE_SHLIBDEPS is on.
- Use the slim Debian bases and install Fedora packages with --nodocs.
- Define the global rng in the tests that pull the server game classes
  out of server_remote, as the other server tests already do. Debian's
  libgtest.a leaves a std::string reference undefined, so the linker
  drags server_protocolhandler.cpp and the classes behind it into any
  test that links the library, and those need the symbol.

git, ca-certificates, libcurl and OpenSSL stay: sentry-native is cloned
over git at configure time on every packaging leg, and OpenSSL also backs
CryptoUtil. mariadb-devel stays on Fedora for the same reason it is
installed elsewhere: without it Servatrice configures successfully but
silently loses its MySQL connector.

Co-authored-by: Lukas Brübach <44814898+BruebachL@users.noreply.github.com>
2026-11-08 09:01:56 +01:00
scotland0208
d9cd308a2e
Show the library edge on the card in limited deck views (#6741) (#6741)
Limited deck views (view top/bottom N cards, scry, surveil) made it
ambiguous which card was the top or bottom of the library. Label the card
itself with a small in-view marker instead of placing text above or below
the whole zone, which became ambiguous once the grid used several columns
or the window was resized.

The marker stays on the part of its card that the column leaves visible:
for a top view, the right end of the exposed strip of the first card; for
a bottom view, the bottom-left corner of the last, fully visible card.

The view logic now answers whether it is a limited library-end view and
which edge it shows. The graphics zone draws and positions a marker on
the first or last displayed card, and re-translates its tooltip with the
rest of the zone view.

Co-authored-by: Lukas Brübach <44814898+BruebachL@users.noreply.github.com>
2026-11-07 17:24:40 +01:00
ebbit1q
4b224f7fe5
Translate the filter string error at display time (#5283)
Keep the parser message untranslated so FilterStringLog stays in the source
language, and format the user-facing text in error() instead. That gives both
consumers what they need without localizing the log.

Q_DECLARE_TR_FUNCTIONS provides the tr() context without deriving from QObject,
which would have deleted copy/move assignment and broken the by-value uses in
PlayerActions and ViewZone. Drop the unreachable line-number branch; peglib
reports 1-based lines, so every message keeps master's "position" wording.

Co-authored-by: Lukas Brübach <44814898+BruebachL@users.noreply.github.com>
2026-11-07 16:26:31 +01:00
Jurriaan
74686d1736
[Client] Added an exclude and color identity filter for the visual database display (#7587)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / vcpkg baseline (push) Waiting to run
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 sanitize (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 27 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Client] Add exclude and color identity filters to the visual database display

* [Filters] Extract the color filter mode derivation into libcockatrice_filters

The mode table and the 'which mode produced this filter set' derivation are
filter semantics, not widget code, so they belong next to CardFilter rather
than in an anonymous namespace in the client. Moving them also makes them
testable: filter_mode_test pins every mode, the Custom fallback and the
'keep the live mode when it still fits' rule.

* [Tests] Cover the negated color identity query

ci! read the camelCase colorIdentity property instead of the lowercase
coloridentity key, so it matched the empty string and rejected every card.
The new case uses a card whose colors and color identity differ, so it fails
on the old lookup and passes on the fixed one.

---------

Co-authored-by: Lukas Brübach <44814898+BruebachL@users.noreply.github.com>
2026-11-05 12:40:32 +01:00
sentry[bot]
64c7809332
[DlgCreateGame] Heap-allocate create game dialog to prevent crash on parent deletion (#7777)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / vcpkg baseline (push) Waiting to run
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 sanitize (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 27 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [DlgCreateGame] Heap-allocate create game dialog to prevent crash on parent deletion

* [Tests] Run the short-write cases without forking a death test

The three cases that force a write failure - CardDatabaseTest.Xml4ShortWriteKeepsInstalledFile,
OracleDatabaseWriter.ShortWriteKeepsPreviouslyInstalledDatabase and
SpoilerFileTest.ShortWriteLeavesExistingFileIntact - all forked a child, lowered
RLIMIT_FSIZE and ignored SIGXFSZ. That child is killed by SIGSEGV on the macOS
runners, so all three have been red on every macOS leg of master since they landed,
while the same cases pass on Linux.

Cap the soft limit in the test process instead: ctest already runs one case per
process, and the guard restores the limit and the SIGXFSZ handler, so a whole binary
keeps working. Same real EFBIG failure, no fork, and macOS keeps its coverage of the
writers.

* [CI] Derive DEBUG_TEST and RUN_TESTS in the nightly job

The shared steps gate on both, but build-linux-nightly never set them, so its
coverage leg skipped "Build debug and test" and then failed collecting a report
from an empty build/ directory on every nightly and tag run. continue-on-error
kept the run green, so the coverage artifact silently never existed.

Derive both exactly as the pull request job does.

---------

Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
Co-authored-by: Lukas Brübach <44814898+BruebachL@users.noreply.github.com>
2026-11-05 07:49:20 +01:00
BruebachL
e22a75974a
[Icons] Add an icon asset ratchet test (#7596)
* [Icons] Add an icon asset ratchet test

The icon rules were enforced by review only, so the next Inkscape export could
reintroduce a baked fill or a physical size without anyone noticing.

Add a test that rejects non-24 viewBoxes, physical sizes, editor metadata,
hardcoded paints, missing tokens, color-coded filenames, and rasters. Keep the
legacy baseline empty so the ratchet can only tighten.

* [Tests] Drop the empty icon baseline

The legacy icon set is fully paid down, so the ratchet had nothing left to
ratchet: BaselineIsWellFormed required the baseline to be empty, which made
the baseline file, the stale-entry diff and the entry-format regex
unreachable. The rules are enforced directly now.

- AssetsFollowTheRules fails on any violation of collectViolations().
- tests/icon_assets/icon-baseline.txt is gone with its reader.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-04 18:39:42 +01:00
BruebachL
627c7a83aa
[Icons] Migrate UI glyphs to palette-token SVGs (#7595)
* [Icons] Migrate UI glyphs to palette-token SVGs

Runtime inversion is not a theming system: it rotates hue, breaks duotone art
and mishandles alpha, and a QPixmap snapshot cannot follow a palette change.
Redraw the top-level UI glyph set on a 24x24 grid with `currentColor` as the
paint token, drop Inkscape/Sodipodi cruft, and delete the icons whose metaphor
was duplicated (cogwheel/gear -> settings, scale_balanced -> scales,
update.png -> reload).

Load the glyphs through `loadThemedIcon` so the engine resolves the token from
the palette on every request, and let `themePixmap` use the same resolution for
one-shot pixmaps. `loadColorAdjustedPixmap` is gone; every caller now keeps a
QIcon, which `SettingsButtonWidget::setButtonIcon` accepts instead of a QPixmap.

Content art (mana, counters, countries) keeps its authored colors.

Add a render test covering aspect ratio, palette flips, icon modes and
high-DPI pixmaps.

* [Tests] Keep the icon render test QApplication alive until exit

Creating it as a function-local static tears it down before Qt's library statics
at exit, and libQt6Network then runs QObject destructors against freed memory —
a SEGV the sanitizer leg reports after the tests have already passed. Leak the
application instead, the usual pattern for a test binary.

* [Icons] Render lock and dropdown glyphs through the icon engine

Both generators rendered the sentinel SVG to a fixed height x height pixmap
at DPR 1. The pre-migration path rendered larger and let QIcon scale it, so
on a 2x display the games-list lock (13px) and the banner dropdown arrow
(24px) came back crisp; the new path hands the widget a DPR-1 bitmap that
gets upscaled and blurred.

- The games list asks the themed engine for `icons/lock` directly, so the
  item view paints it at the device pixel ratio and the lock also follows
  HighlightedText on a selected row.
- The dropdown goes through `loadThemedIcon(...).pixmap(size, dpr)`, which
  keeps the glyph's aspect ratio and renders at the requested ratio.
- LockPixmapGenerator has no consumer left, so it and its cache are gone.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-04 18:39:41 +01:00
BruebachL
b04d8b88ac
[Icons] Add SVG currentColor sentinel resolver (#7593)
Qt's SVG renderer has no CSS cascade, so a `currentColor` fill has to be
substituted before the bytes reach QSvgRenderer.

- Match and replace on bytes: both the sentinel and the replacement are
  ASCII, so the document never goes through a UTF-16 round trip, and files
  that are not valid UTF-8 survive unchanged.
- Document that the match is a plain case-insensitive substring, so an
  identifier that merely starts with the sentinel gets rewritten too.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-04 18:39:40 +01:00
Bryce!
0de92c06f8
[Spoilers] Preserve spoiler files through update and reload failures (#7707)
* Preserve spoiler files and serialize update reloads

* Surface spoiler removal reload failures and allow disabled-state retry

* Address review feedback on spoiler update lifecycle

Only remove the spoiler file when the user unchecks the setting; opening
Settings no longer queues an unrequested reload for a leftover file.
Remove the spoilersUpdatedSuccessfully signal, which had no receiver.

* Fix spoiler settings state after reopen and while disabled

Reopen Settings while an application-owned update is running: the new page
now observes that updater, so the button reads Updating... and re-enables
on completion instead of offering a second update that cancels the first.
Observation does not take ownership: only updaters the page creates are
retired with deleteLater, because the launch check is stack-allocated.

When spoilers are off, the hint now describes what Reload Card Database
actually does, stays readable, and wraps instead of clipping.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-04 18:24:19 +01:00
Bryce!
3a656c9c4a
Preserve installed card databases when Oracle refresh fails (#7739)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / vcpkg baseline (push) Waiting to run
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 sanitize (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 27 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* Preserve installed card databases when Oracle refresh fails

* Fix the Oracle refresh verdict and cover the parser write path

A background refresh that already installed cards.xml exited 1 when the
token save afterwards failed, so the client kept its old in-memory
database and reported the update as failed. Return the same verdict
main() already computes: a reject after the database was written is a
successful update.

Cover the writer Oracle actually uses: add a short-write death test for
CockatriceXml4Parser::saveToFile, which reverted to QFile would truncate
an installed database without any test noticing.

Route a failed background token download through reject() instead of a
modal box on a hidden wizard, which left the client waiting with no
timeout and blocked later updates. Test the first non-space byte in
place rather than copying the payload, and report an import refusal with
the importer's own message.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-04 17:33:49 +01:00
Bryce!
e558c0dffa
[Banner] Keep horizontal user banner crops inside the artwork (#7742) 2026-11-04 17:08:54 +01:00
BruebachL
5ea16c805f
[Server/Client/Protocol] Shuffle or reorder player seats before a game starts (#7398)
* [Protocol] Add shuffle players option and player order command/event

Adds a 'Shuffle players at start' option to game creation plus a local
player id on the game state so clients can reconcile seat changes, and a
SET_PLAYER_ORDER command (1206) with a PLAYER_ORDER_CHANGED event (2023)
so the game host can set or randomize the seating order.

Took 6 minutes

* [Server] Shuffle and reorder player seats before a game starts

Auto-shuffles the seats at game start when enabled (now for games with
two or more players instead of three or more) and handles the new
SET_PLAYER_ORDER command so the game host or a moderator can reorder or
randomize the seats before the game starts. Re-seating re-keys the
player ids while keeping the per-user game/seat routing and persistent
player records in sync, then broadcasts the new order to everyone.

* [Client] Add shuffle players option and turn order dialog

Adds the 'Shuffle players at start' checkbox to game creation and a
host-only 'Set turn order...' action that opens a dialog to reorder or
randomize the seats before the game starts. Seating changes are shown in
the game's message log.

* [Client] Move the turn order dialog strings into retranslateUi

Follows the DlgInviteToGame convention so the dialog can be retranslated
instead of freezing its strings at construction time.

* [Server] Keep player ids stable across seat shuffles

* [Client] Lay out table and turn order dialog in seat order

* [Server] Send game-joined player list in seat order

* [Client] Reword the shuffle option to match its behaviour

* [Client] Address the seat-order review comments

- Give the game setup checkboxes a row of their own so they no longer
  collide in the create-game dialog
- Rename DlgTurnOrder::randomize() to isRandomizeRequested()
- Document what the ids in GameState::seatOrder mean
- Keep "Set turn order..." in sync with the game: hidden for non-hosts
  and once the game has started, re-shown when the game ends or hosting
  moves to the local player (new GameState::hostChanged signal)

* [Client] Do not preview a randomized turn order

Randomizing is a request to the server, not a local shuffle. Shuffling the
list locally meant the dialog showed an order the server then discarded, and
let the host re-open it until a favourable order appeared. The dialog now
switches to a mode that says the server will randomize and toggles back to the
manual list on request.

* [Client] Let moderators set the turn order

The server accepts SET_PLAYER_ORDER from the host or a moderator, but the
client hid the action from everyone except the host, making the moderator
branch unreachable. Visibility and the handler now share canSetTurnOrder().

* [Server] Rely on the player order event instead of a full state resend

cmdSetPlayerOrder re-sent the whole game state after reseating, so every
client reloaded its deck view mid-lobby for no reason. The new seating reaches
clients through the Event_PlayerOrderChanged that reseatPlayers broadcasts.

* [Client] Show random turn order in the game selector

The restrictions column listed "open decklists" but not the new seating
option, so a joining player had no way to see that seats will be randomized
each game. Game info already shows the disabled checkbox.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-04 15:44:50 +01:00
Bryce!
ca2301bc96
Fix deck export and mana group ordering (#7729) 2026-11-04 15:43:38 +01:00
BruebachL
d8700b2947
[Fonts] Add AppFont roles and FontConfig fonts.toml round-trip (#7534)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-04 07:57:52 +01:00
Bryce!
94f7de42f2
Fix user card games for offline users and stale requests (#7738) 2026-11-04 05:29:51 +01:00
Bryce!
dc4aa19d23
Import Moxfield signature spells into the deck [skip ci] (#7751)
Co-authored-by: Codex <codex@openai.com>
2026-11-03 10:40:28 +01:00
Jurriaan
bc09ec6afe
[Client] Make DeckStateManager::decrementCard remove tokens from the token zone if the token is not in the given zone (#7588)
* Fixed decrementCard for tokens not using the token zone

* Removed dead function actDecrementCard

Updated documentation

* Fixed no-op, added documentation

* Removed stray blank line

* Allow finding card nodes even if no database entry exists for the given card

* Added DeckStateManager tests

* Added tokens on mainboard test

* Added if checks and sorted sources

Added if checks

* Library include fix

* Renamed zone to targetZone

* Revert "Allow finding card nodes even if no database entry exists for the given card"

This reverts commit 88578cd0d9.

* Combined loader_local_matching_test and deck_state_manager_test settings sources

* Added settings sandbox, refactor

* Format

* Renamed return variable

* Flattened if statement

* Style fix
2026-11-03 10:37:17 +01:00
sentry[bot]
9f30892dff
[Client] Fix crash when local game commands are sent after shutdown (#7714)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / vcpkg baseline (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Client] Fix crash when local game commands are sent after shutdown

* [Network] Fix formatting in local client debug log

* [CI] Keep the vcpkg submodule pointer on the manifest baseline

vcpkg.json asks for builtin-baseline 9eec002 (vcpkg 2026.10.16), but the
submodule gitlink was still at 9e593bb. The runners check submodules out
shallowly, so vcpkg-tool cannot read versions/baseline.json out of the baseline
commit and every vcpkg leg (Windows and all macOS) fails in CMake configure
with:

  failed to `git show` versions/baseline.json

git phrases that as "path ... exists on disk, but not in <sha>" even when the
commit is simply absent from the shallow clone, so it reads like a tooling or
network failure rather than a mismatched pointer. Point the submodule back at
9eec002, and add a cheap job that fails when the gitlink and builtin-baseline
disagree, before the five build legs pay for it.

---------

Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-02 13:28:23 +01:00
RickyRister
62e590d300
[ServerGame] Refactor out spectator/judge bools into RoleInfo (#7761) 2026-11-02 00:37:05 -07:00
BruebachL
a1a8a59f4a
[VisualDatabase] Draw the card grid with a model/view (#7701)
* [VisualDatabase] Draw the card grid with a model/view instead of a widget per card

The visual card database built one CardInfoPictureWithTextOverlayWidget per
card, 100 per page, and tore them down on every search, resize and scroll
page. It also looked each card up again by name from the model row, kept a
shadow QList<ExactCard>, and carried paging state plus a 5 s timer that only
existed to defer the first page.

Replace that with a real view on a flattened model:

- CardDatabaseDisplayModel gains CardInfoRole and cardInfoAt(), so a view can
  get the CardInfoPtr behind a proxy row without a name round trip.
- New CardDatabaseVisualDisplayModel (libcockatrice_models) flattens the
  filtered and sorted proxy into one entry per displayed card picture: the
  preferred printing normally, every printing of the selected sets while set
  filters are active. Proxy changes rebuild it on a 30 ms coalescing timer.
- New CardDatabaseVisualView (QListView IconMode) paints only visible rows
  through the new CardPictureDelegate, keeps the old interaction contract
  (left/right/middle press -> cardClicked, hover -> cardHovered, Ctrl+wheel
  resize, no selection, no focus stealing, scroll to top on a new result) and
  repaints when CardPictureLoader reports an async image.
- CardSizeWidget::enableCtrlScrollResize() also watches scroll-area viewports,
  which is what makes Ctrl+wheel work over a QListView.
- VisualDatabaseDisplayWidget drops the paging, the shadow list, the dead
  members and the 5 s timer.

Behaviour-preserving: no gameplay change, no new user-facing strings.

* [VisualDatabase] Fix the card grid wheel step

QListView hands the delegate's item height to the scroll bar as its single step under ScrollPerPixel, so QAbstractSlider moved wheelScrollLines() whole card rows per notch. Handle the wheel in the view instead: one line is an eighth of the grid cell, so a notch moves about a third of a card row at any card size and still follows the OS line count.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-02 08:35:58 +01:00
BruebachL
3cf8bdb00e
[Tests] Add a headless game logic regression suite (#7702)
* [Tests] Add headless game logic regression suite

Cover CardState, CardZoneLogic and the Command_MoveCard protobufs PlayerActions emits, against libcockatrice_game with a recording AbstractClient and no graphics. The old game_smoke_test cases are migrated into the new suite.

* [Tests] Cover game event application through GameEventHandler

Feeds GameEventContainers through the real dispatcher and asserts the resulting zone and card state: move (including face down), the set-card-attr attributes plus the all-cards and does-not-untap branches, card counters, attach/detach, zone reveal properties, and drawing named cards from the deck.

* [Tests] Replay a synthetic game state through the dispatcher

Builds a GameReplay in code, feeds its containers through the same GameEventHandler path the client uses, and asserts the materialized players, zones, cards and attachments plus event ordering for move, attributes and counters. A small headless zone view stands in for the graphics side where processPlayerInfo asks for card items.

* [Tests] Cover card flip, destroy, token and turn events

Adds the remaining state-mutating event handlers: flip (face down keeps the ref, face up adopts the printing), destroy (removes the card and un-attaches its attached cards), create token (database P/T unless provided or face down) and player properties changed under the concede/unconcede contexts, plus active player and phase.

* [Tests] Cover player lifecycle and counter events

Adds the player-level events that mutate session state: leave removes the player from the manager, kicked emits gameClosed, game closed flips the closed flag and clears the started flag, host changed moves the host role to another player, and create/set/delete update the player counters with unknown ids ignored.

* [Tests] Drive the zone view request through addCard

The test emitted createCardRequested itself and then asserted it was emitted. Registering a ZoneViewZoneLogic on the zone and adding a card exercises the real prepareAddCard -> signal path, so the assertion now fails if addCard stops asking the graphics side to build the card.

* [Tests] Skip the card database when listing tests

gtest_discover_tests runs the binary with --gtest_list_tests during the build. That process built the card database and replaced its default path provider, leaving allocations that the sanitizer leg's default LeakSanitizer check reports at exit, so discovery failed on Ubuntu 26.04. No test runs in list mode, so return before the database setup.

* [Tests] Fix fixture comment nit

Wraps the RecordingClient::sent member name in backticks in the comment.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-02 07:35:41 +01:00
BruebachL
5134e72eb4
[Settings] Store server profiles with QSettings arrays (#7629)
The server list used hand-numbered saveName%1/server%1 keys and an O(n)
shift loop to delete entries. Store the profiles with beginReadArray and
beginWriteArray instead, and migrate the legacy keys once in the
constructor so existing profiles and saved passwords survive.

SettingsManager::batchWrite no longer disables atomic sync, so a batch is
still written through QSettings' atomic save. Add tests for add, update,
remove, password updates and the legacy migration.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-02 07:30:29 +01:00
BruebachL
b37c8b828f
[Utility] Budget scrypt's XY array in the derivation memory limit (#7712)
deriveKey passes maxmem = 128nr + 128rp + 4096 to EVP_PBE_scrypt, but scrypt
also allocates XY at 256r bytes. costParamsAreSane accepts r up to 32, so any
server asking for r >= 17 made derivation fail and the client report a login
abort it could never recover from. Cover the XY array and add a regression test
at the accepted-range boundary.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-02 07:29:15 +01:00
BruebachL
ce543896f5
[Tests] Use QtTest for model contracts and signal spies (#7630)
Qt6::Test was linked into every test but never used. Add a small RAII
QAbstractItemModelTester helper and apply it to the deck list model, the card
database model and display proxy, and the sets model. Add QSignalSpy coverage
for the card-added and download-settings signals, and assert explicitly that a
card row is a leaf with no child columns (the tester does not check that).

The contract checker immediately found four model bugs, fixed here:
- DeckListModel allowed children below non-zero columns.
- CardDatabaseModel reported every card as a child of every card row.
- CardDatabaseModel reported six columns for a valid (leaf) card index.
- CardDatabaseDisplayModel claimed more rows were fetchable while rowCount
  already exposed them all, so fetchMore() inserted duplicates.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-02 07:27:43 +01:00
BruebachL
50a2ae9cc8
[Server/Client] Enforce and complete the forced password change at login (#7680)
checkUserPassword ignored force_password_change on the challenge-response
path, so a reset password stayed valid. Returning PasswordChangeRequired
was not enough on its own: the session was dropped and the client told the
user to ask the administrator for the temporary password they had just used.

The pending session now keeps its identity without registering a user
session or announcing a join, may only ping and change its password, and
the client completes the change in-session before reconnecting.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-02 07:01:29 +01:00
BruebachL
00151380e3
[CardDB] Break the CardInfo <-> CardSet cycle on reload (#7645)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [CardDB] Drop the CardInfo self pointer

The weak self pointer had to be assigned right after every construction,
and a missed assignment silently stopped change notifications. CardInfo
is always owned by a QSharedPointer, so it now derives from
QEnableSharedFromThis<CardInfo> and asks for sharedFromThis() instead:
nothing to initialize, and the CardSet cycle stays broken.

CardSet::newInstance() also loses the commented-out self-pointer
assignment left over from the same design.

* [CardDB] Pin the cardInfoChanged contract without the self pointer

Dropping CardInfo's self pointer moved change notifications onto
QEnableSharedFromThis, so the signal now rests on the card being owned by
a QSharedPointer. Two tests pin that down:

- A property change on a card owned by a CardDatabase still emits
  cardInfoChanged() carrying that card, which is what the card database
  views and models listen to.
- clone() starts out unowned, so a change to the clone is reported as the
  clone's and must not reach the original's observers.

* [CardDB] Treat an unowned CardInfo as a bug, not a no-op

Every CardInfo is built inside a QSharedPointer (both factory overloads
and clone()), and no constructor notifies anyone, so sharedFromThis() is
always available by the time addToSet() or a setter runs. Assert that and
warn in a released build, instead of silently skipping the notification
or the set append.

The copy constructor keeps its explicit QEnableSharedFromThis
initializer: GCC's -Wextra warns about the uninitialized base in every
translation unit that copies a CardInfo otherwise. A comment there
records that the clone deliberately starts unowned.

* [CardDB] Break the CardInfo <-> CardSet cycle on reload

CardSet is a QList<CardInfoPtr> and CardInfo::addToSet() appends the card
to its sets, while a card keeps a CardSetPtr per printing in
setsToPrintings. The sets therefore hold strong references back to the
cards, and the previous database snapshot stayed alive after a reload, so
RSS grew by roughly a second database on every reload.

Empty the sets before the cards are dropped: CardDatabase::clear() and
swapInDatabaseData() clear the sets before replacing the containers, and
removeCard() detaches the card from its sets, so nothing keeps a card
alive on behalf of a set once the database lets go of it.

Two tests pin the behaviour: a reload and a clear() must drop the last
strong reference to a card from the previous snapshot.

* [CardDB] Keep the set detach in one place

clear() and swapInDatabaseData() both emptied every set before dropping the
cards, so share that as detachSetMembership() instead of two copies that have
to stay in sync.

removeCard() now detaches through the first printing of each set directly, the
same way CardInfo::newInstance() appends, instead of an inner loop that always
breaks after the first item.

Because clear() empties the sets up front, its per-card detach was walking
every card's printings to remove it from sets that were already empty. Split
removeCard() into the per-card detach and unindexCard(), and let clear() call
only the latter.

* [CardDB] Test that removeCard detaches the card from its set

removeCard() is the one teardown path a caller can trigger for a single card
(the token editor uses it), and it now detaches the card from its sets as
well. Without that detach the set's strong CardInfoPtr keeps the removed card
alive forever, which is exactly what the reload and clear tests pin for the
whole-snapshot paths.

The test loads the fixture, removes Cat, and requires the card to be gone from
the CAT set and to be destroyed once the test drops its own reference.

* [Servatrice] Drop the settings cache from the logger

#7602 removed #include "settingscache.h" from server_logger.cpp but left
isLoggingEnabled() reading settingsCache, so master no longer compiles: the
servatrice target fails on the undeclared global and server_logger_test, which
compiles the same file without main.cpp, fails to link it.

The same commit made reloadConfig() the single place that stores writelog and
the log filters, precisely so logging threads stop reading the global QSettings.
isLoggingEnabled() now mirrors logMessage() and reads those members instead: the
console is a sink of its own, and the logfile needs an open file plus writelog.
The mutex is mutable so the const query can take it.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 19:37:15 +01:00
BruebachL
825aba953a
[Servatrice] Fail loudly when startup initialization fails (#7602)
* [Servatrice] Exit non-zero when server initialization fails

initServer() reported every startup failure through a zero exit code: the
database could not be opened, the server printed nothing of its own, and the
process looked like a clean shutdown. Log the two refusal reasons at critical
level and return 1 so supervisors and the Docker exit status can tell the
difference.

* [Servatrice] Keep console logging independent of the logfile

logMessage() returned before the console echo unless a logfile was open and
server/writelog was true, so --log-to-console produced nothing when
server/logfile was empty or could not be opened. That is how a refused database
connection could end in a silent exit.

Print to the console before the logfile gates, drop the duplicate echo from
flushBuffer(), and read writelog/logfilters once in main instead of calling the
non-thread-safe global QSettings from every logging thread.

* [Servatrice] Uninstall the log handler before tearing down globals

The handler reaches into the global logger and settingsCache. Leaving it
installed while main deletes those objects turns any Qt warning emitted during
QCoreApplication/QFactoryLoader teardown into a use-after-free; with
QT_DEBUG_PLUGINS=1 that warning exists, and Servatrice died with SIGSEGV in
QSettings::value() on the way out.

* [Servatrice] Buffer console logs on the logger thread

Address review feedback on the startup failure change:

- Append console lines to a buffer drained by the logger thread, so a
  stalled stdout reader cannot block game threads.
- Report islServer->errorString() instead of the literal call text.
- Let the constructor reuse reloadConfig() instead of parsing the filter
  list twice.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 19:02:49 +01:00
BruebachL
52d3978f84
[Build] Let AUTOMOC process library headers (#7638)
The 13 libraries using qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) listed their\nheaders twice and depended on a generated source list. Add the headers to the\ntarget sources directly and delete the qt6_wrap_cpp call; AUTOMOC scans them\nand generates the same moc output.\n\nThe change exposed that server_card_counter_test never defined the rng global\nthat the server remote library references; every sibling server test does.\nGive the test the same definition so the target links.\n\nVerification: full client, Oracle, server and test build passed.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:49:05 +01:00
BruebachL
345b0e8ba9
[Refactor] Move game logic into libcockatrice_game (#7437)
* [Refactor] Move game logic into libcockatrice_game

* [Refactor] Include card_state.h in player_actions.h

* [Game] Inject settings providers into libcockatrice_game, drop Widgets

* [Game] Follow base rename to createCardRequested after rebase

* [Game] Drop unused CardItem forward declaration

* [Game] Fix includes after moving game logic into libcockatrice_game

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:15 +01:00
BruebachL
47c90cda10
[Refactor] Give graphics zones a card item cache and own card placement (#7427)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:12 +01:00
sentry[bot]
bbc7e462ff
[Installer] Graceful close and a way out of a stuck update (#7687)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Installer] Stop calling the payload free while an executable is locked

The guard answered "nothing is holding the payload" from two lookups that can
both miss a running instance: Restart Manager does not reliably report a
program's own image or the modules it mapped, and the image path of a process
running under another account cannot be read from the probe at all. A user log
showed exactly that: the first guard passed while cockatrice.exe was running
from the directory, the old uninstaller silently skipped the file it could not
delete, and the update aborted half an uninstall later with "cancelled by user"
after a close attempt the running window ignored.

Ask the file itself. A running image is opened without write sharing, so the
write open replacing it needs is refused with ERROR_SHARING_VIOLATION for as
long as the program runs, while a file that is merely being read still accepts
it. When neither lookup named anybody and a payload executable refuses that
open, the probe answers that the file is in use and that the program holding it
could not be identified - the guard's "could not confirm" dialog, whose Retry
works once the user has closed it - instead of passing the payload as free.

Only the payload executables are tested, not every file the purge removes: a
locked executable proves a program is running and a mapped DLL belongs to a
running executable, while a locked DLL or qt.conf can be the asking process
itself, which is the loop the guard already had. The open is retried once after
300 ms, and only for the files that refused, so a scanner holding a file for a
moment cannot stop an install for nothing.

The Windows probe check now covers that answer: a payload executable held by
the asking process, so that it cannot be named, has to come back as in use
rather than as free.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>

* [Installer] Close running clients over a local socket

The installer could only ask a running client to close with WM_CLOSE, which an instance hidden in the notification area or sitting on its own confirmation cannot answer usefully. Give every client a per-process local socket and have the holder probe ask it there first, falling back to WM_CLOSE for clients from before the socket.

* [Installer] Offer to check again or update by hand when files stay locked

Replace the two-answer guard prompt with force close, close by hand and check again, or cancel. Retry the automatic close while the set of holders changes, and point a cancelled run at the latest release so a user who does not want anything killed has a way forward that is not cancelling the update again.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 15:32:59 +01:00
sentry[bot]
0369aaffa9
[Installer] Name only the payload programs installed in the directory (#7684)
Some checks failed
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Build Desktop / Configure (push) Has been cancelled
Build Desktop / Prune caches (push) Has been cancelled
Build Docker / Servatrice (arm) (push) Has been cancelled
Build Docker / Servatrice (x86) (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 44 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Servatrice_Debian 12 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / Ubuntu 26.04 cover (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 26 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
Build Docker / Publish multi-platform Servatrice image (push) Has been cancelled
The running-instance lookup named every process whose image path began with the
install directory, subfolders included. Installing into a folder that holds
another application in a subfolder therefore reported that application as
holding the previous version, and nothing the guard offers could clear it: the
close asks cockatrice.exe only, and the end answer refuses anything that is not
a payload program. Both answers were no-ops, so the update could only abort and
tell the user to close a program that was never touching the files.

Name only the programs the payload installs, and only when the image lies in the
install directory itself. A program below the directory that really does hold a
payload file is still named by the Restart Manager lookup, which is the lookup
for that question.

The Windows probe check now covers a payload-named program in a subfolder and a
program the payload does not install; both have to come back as nothing.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-30 13:16:50 +01:00
BruebachL
675e1f7447
[Translation] Name languages without reading the translation files (#7662)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Translation] Name languages without reading the translation files

The language selector derived every label from the language's own .qm file:
it loaded the file and translated the literal "English" in the "i18n"
context. lupdate could never extract that literal from the callers, which
pass the DEFAULT_LANG_NAME macro, so a dummy QT_TRANSLATE_NOOP in
cache_settings.cpp existed to give it something to extract. #7050 deleted
that dummy, the next lupdate push dropped the string from Transifex, and
the following pull rewrote the .ts files without it. lrelease only emits
translated messages, so 22 of 25 .qm files returned an empty name.

The names are data, not translations: a selector has to be readable before
any translation is loaded, and a label must not depend on the translation
of the language it names. TranslationLoader::languageDisplayName now holds
them next to availableLanguages, falling back to Qt's native language name
and then to the code, so a language added later still gets a label.

Also select the configured language by item data instead of by matching the
translated "English" string, which pointed at the wrong row whenever the
lookup came back empty, and restore that English entry's own name: en_US is
"English (United States)" so it can no longer be confused with the
untranslated default.

Oracle shared the code path, so its wizard language page is fixed too.

* [Translation] Name the default language selection once

Review follow-up: log the unresolvable language through the page's own
category, and move the en_US entry both selectors fall back to for an unset
setting into TranslationLoader::defaultLanguageCode.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-26 02:39:26 +01:00
BruebachL
5fbc636f93
[Models] Use QCollator for natural, cached sorting (#7633)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Models] Use QCollator for natural, cached sorting

QString::localeAwareCompare rebuilds its collation data for every
comparison, and it compares numbers as text. Keep one QCollator per model,
enable numeric mode, and use it for the card name, set and user name sorts.

Add a test that sorts "Forest 1", "Forest 2", "Forest 10" through
DeckListSortFilterProxyModel and checks the natural order.

* [Models] Sort naturally under the C locale too

Numeric mode only takes effect through a real locale: under the C locale,
which headless CI processes run in, QCollator silently falls back to string
order and sorts "Forest 10" before "Forest 2". That made collator_sort_test
fail on every Linux leg while passing locally.

Add makeNaturalSortCollator(), which enables numeric mode and pins English
only when the process locale is the degenerate one, and build the models'
collators through it. Users with a real locale keep their own collation.

* [Models] Fix compile of natural sorting (QStringView mismatch)

* nit: initialize collators in member initializer lists

* nit: namespace makeNaturalSortCollator under NaturalSort

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-25 01:01:04 +02:00
BruebachL
518c7e6125
[Network] Add a configured QNetworkAccessManager factory (#7628)
* [Network] Add a configured QNetworkAccessManager factory

Every HTTP caller built its own QNetworkAccessManager, so the system proxy
and a transfer timeout were easy to omit. Add NetworkManagerFactory::create()
as the single place that applies the process-wide proxy configuration and a
transfer timeout, and use it at all 14 construction sites.

Per-site settings such as disk caches and manual redirect policies stay with
their callers.

* [Tests] Link libcockatrice_network into the tests that compile its headers

release_channel.cpp and card_picture_loader_worker.cpp now include
libcockatrice/network/network_manager.h, but the test targets that compile
them only linked libcockatrice_settings, so the include directory never
reached them and every platform failed with "file not found":
loader_local_matching_test and picture_loader_benchmark_test.

libcockatrice_network is an INTERFACE library, so linking it costs these
targets nothing but hands over the include path.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-25 00:33:43 +02:00
BruebachL
4028606bd9
[CardDB] Clear CardInfo relations before deferred deletion (#7615)
removeCard() and swapInDatabaseData() schedule CardRelation deletion with deleteLater(), but the owning CardInfo could outlive them through an external shared pointer and kept returning the freed pointers from getAllRelatedCards(). Add CardInfo::clearDirectRelations() and call it after scheduling deletion so the UI can no longer dereference dangling relations.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-24 10:28:24 +02:00
BruebachL
bbc6003f0a
[CrashReporting] Name the files and processes behind a failed installer purge (#7590)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [CrashReporting] Log which files and processes blocked the installer purge

A purge that fails aborts the upgrade with a message that can only guess
at the cause, and the log only listed which DLLs survived. Record what
Windows reports for each survivor and which processes still have the
install directory mapped, then repeat the holder list in the abort
message.

The diagnostics are additive and run only on the abort path: they open
each survivor for DELETE access and close it again without deleting, so a
failed upgrade can never end up half-removed. The purge itself is
unchanged.

* [CrashReporting] Name the holders of a failed purge with Restart Manager

The process-and-module scan could only name a holder whose image lives in the
install directory or that loaded one of its DLLs, so the case this diagnostic
exists for - an antivirus scanner holding a plain handle - was reported as "no
holder named, only a code". Reaching the Restart Manager from the NSIS script
would have meant marshalling RM_PROCESS_INFO through System::Call as a table of
hand-computed offsets, which cannot be checked without a Windows toolchain, and
that is why it was dropped when this diagnostics was written.

Move the call into tools/installer-holder-probe: a small executable the same
Windows build compiles, linking nothing from this project so it still starts
when the installed runtime is what the purge could not replace. It rides inside
the installer and is extracted to $PLUGINSDIR only when the purge has already
failed, so the successful path is unchanged and nothing new is installed.

.ci/compile.sh runs it against a file held with FileShare.None on the Windows
runners, so the lookup is exercised on real Windows instead of only compiled.

* [CrashReporting] Fix the holder probe check and cap the holder line

The probe block ran after the package steps, where the working directory is
already the build dir, so its $BUILD_DIR-prefixed paths named build/build and
the PowerShell file could not be written. Move it ahead of install/package,
where the build dir is still the cwd, and name both files relative to it.
Guard it on MAKE_NO_CLIENT, since the probe is only built with the client.

Cap the rendered holder line at HOLDER_LINE_CHAR_MAX: the count limit alone
does not keep long service names inside the NSIS string that also carries the
abort text and the log path. Whole entries are kept and the rest counted, with
the first holder always shown.

Also run cmake-format on the probe's CMakeLists so the lint job accepts it.

* [CrashReporting] Report a read-only purge survivor and bound the probe check

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-24 00:46:54 +02:00