The check's server_time became the new baseline even when the run could
not apply a server change (a failed download, or a mirrored delete whose
local copy could not be removed), and manual Sync now goes through the
same fast path, so the change never came back. Track those skips and
keep the previous baseline so the next check reports them again.
Also disable Sync now while any run is in progress, drop the redundant
baseline assignment in onCheckFinished, and tell the user that only
two-way sync checks for changes automatically.
- Fail the run when the connection drops while a sync is running, and clear
the active conflict, so the queue is not sent on a dead client and a
re-login as another account never receives this profile's uploads.
- Backfill last_modified from upload_time in the 37 -> 38 migration and drop
the "last_modified is null" arm from the sync-check query, so rows that
predate the column stop reporting as changed on every poll.
- Skip conflicts during auto runs and report how many were skipped, instead
of opening the modal conflict dialog from a timer or a file-watcher event.
The first-sync verdict is stored in the sidecar, so "Upload mine" and
"Download server" apply to every later sync, not just the first one.
Say so in the dialog, label the one-way options as one-way, and add a
"Reset sync direction" action to the deck storage tab that writes the
linked (two-way) mode back to the sidecar.
- Remove the duplicated download loop and the stray '<' plus extra brace
that left the head unbuildable.
- Skip the fallback upload under DownloadMode so a local-only deck is not
pushed when the user asked for downloads only.
- Treat a server row without a hash as unknown rather than changed, so
pre-migration decks are not re-downloaded on every sync.
The unmatched-server-rows pass queued a download for every row with no
local file, including rows whose sidecar record is being deleted in the
same run. The delete ran first, so the download then failed with
NameNotFound and the summary reported a clean delete as removed plus
skipped. Cancel the pending download for the deleted row id instead; a
failed delete keeps it, which preserves the restore-the-file fallback.
- Treat a record whose server row is gone as a remote delete only when the
local file still matches the sidecar's base hash. An edit made after the
remote delete is newer than the delete, so it uploads and revives the row
instead of being removed.
- Revive a tombstone with the same user and guid in cmdDeckUpload instead of
inserting a second row next to it, so the deck keeps its id and version
history and the promise in servatrice.ini.example holds.
- Run tombstone purging on its own clock, driven by the new
[deck_sync] purge_interval option, so disabling share cleanup no longer
disables purging silently.
- Write the base snapshot through QSaveFile and log a failed commit, so a
crash or a full disk cannot leave a truncated base for DeckDiff::merge().
Sync keyed local files to server rows by path, but the server names a row
after the deck, not after the file, so any deck whose file name and deck name
differ uploaded again on every run. A second pass then brought the server copy
back as an extra local file.
- Match by the sidecar's row id, then guid, then content, and only then by the
server path, so renaming either side no longer duplicates a deck.
- Key the server tree by row id: the server allows several rows with the same
name in one folder, and keying by path silently dropped all but one.
- Stop discarding rows whose name contains '/' ("U/W Control"). They stay
matchable, and only the download file name is sanitised.
- Build the download path from the folder plus a sanitised row name, including
every folder segment, so a hostile server name cannot escape the deck folder.
- Download a colliding server row under a unique name and count it, instead of
skipping it without a trace.
- Hash the whole deck instead of the card hash: SHA-1 over the native deck
string covers name, printings, banner, tokens and sideboard plans. The client
and the server hash the exact bytes they exchange, so both agree.
Adds deck_content_hash_test for the client/server hashing invariant and for the
name-only change the card hash cannot see.
- Emit SyncSummary from syncFinished() instead of four positional ints, and
build it from a single currentSummary() helper, so the counts cannot drift
between the status message and the signal.
- Use a designated initializer for LocalFile, like the other records.
- Drop setClient() and setColorIdentityProvider(): both fields are already
constructor parameters and the setters had no callers. setProfile() and
setDeckPath() stay, since the tab re-reads them before every run.
- Fix the include paths in tab_deck_storage.h and the narrowing conversion of
the tree item id, so the branch builds again.
Player menu shortcuts were bound as soon as a local player was added in the deck-selection lobby. Defer full activation until GameMetaInfo reports the game has started, and bind only Player/aRollDie while in the lobby. Counters follow the deactivation signal, so they cannot re-enable shortcuts before the game starts.
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>
Holding or spamming the draw-arrow shortcut fires the action again
before the previous local arrow has been placed. Every new arrow takes
the mouse grab from the previous one, so the previous arrow never
receives the release that deletes it. If the cursor had passed over a
card meanwhile, the orphan stayed visible on the board.
Track the in-progress arrow on the CardItem that started it and cancel
it, together with any child arrows, before starting a new one. The
attach-arrow shortcut (Ctrl+Alt+A) uses the same path, so it no longer
leaks arrows either.
Fixes#4329.
Co-authored-by: mb <mbaecker@unforce.de>
The Accept header added in ef87b54b was missing asterisks in two
MIME type wildcards: "image/" and "/*" instead of "image/*" and
"*/*". Servers may reject or mishandle requests with invalid MIME
types, preventing card images from loading.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The "from …" fragments built by getFromStr() carried a leading space so
the sentence templates could concatenate them directly (%2%3). That blank
is invisible on Transifex and most translations dropped it, merging the
card name and the zone phrase in the log (#4943).
Drop the blank from the tr() strings and add the separator in code where
the fragment is built. The space is only prepended when the fragment is
non-empty, so messages that put the zone in the card name instead — "the
top card of their library." — keep their punctuation tight.
The templates stay at %2%3, since %3 is empty on those paths.
- Span chat and alert-word checkboxes across the full grid width
- Add right-aligned Color labels and HEX placeholders to the mention,
highlight and channel color fields
- Reword the horizontal hand option to "requires more space"
Build the card menu's related card actions from complete strings with %1
placeholders instead of appending translated fragments. This also fixes
tr(cond ? "Transform into " : "Attach to "), which lupdate cannot extract:
neither string appears in cockatrice_en@source.ts, so neither was ever
translated.
Give the EDHRec tab title a %1 placeholder so French can render
"EDHRec : %1" instead of concatenating into "EDHRec :CardName".
Keep the colon in tr("Token:") and the leading space in tr(" seconds"),
since that separator is language-specific, and attach //: translator
comments so they are no longer shown out of context on Transifex.
* Parent card menu submenus to the menu that owns them
QMenu::addMenu(QMenu *) does not take ownership of the submenu, and these
submenus were created without a parent. Since CardMenu is rebuilt from
scratch on every right-click, each rebuild leaked the MoveMenu, PtMenu,
card counters and related-cards submenus.
Pass the owning CardMenu as the QWidget parent so Qt's parent-child
mechanism frees them with it.
* Delete the replaced card menu in TabGame to fix menu rebuild leak
PlayerMenu creates a new CardMenu on every right-click and TabGame installs
it with QAction::setMenu(), which does not take ownership, so the previous
menu was never freed. TabGame now deletes the menu it replaces and reuses a
single empty placeholder menu instead of allocating one per update. The
installed menu is also released when the tab is destroyed.
The downloader wrote to a predictable /tmp/<url-basename> path, ignored the
result of write(), and leaked the previous reply when a check was retried. It
also connected no error handler to the reply and had no transfer timeout.
Create the file inside a private QTemporaryDir, write it with QSaveFile and
commit it only after every byte was accepted, report reply errors, give the
request a 30 s transfer timeout, and delete the superseded reply. Ignore a
queued finished()/errorOccurred() from a reply that has already been replaced,
and drop the manual redirect branch because Qt follows redirects by default.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [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>
* [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>
The badge rendered `theme:icons/pin` through the pixmap generator, which
resolved `currentColor` once and cached the result, so the glyph stayed at its
first palette and was rasterized at DPR 1.
- Render the glyph through `loadThemedIcon`, at the label's device pixel ratio,
and re-render it on palette and theme changes.
- Compose it onto a rounded backdrop filled with `Window` and outlined with
`WindowText`. The badge floats on card art rather than on a palette surface,
so a bare outline vanishes into art of the same lightness; the backdrop keeps
it readable on every printing, and both roles still follow the palette.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Settings] Delete update channels at shutdown
SettingsCache owns the stable and beta update channels as raw pointers, and
each owns a QNetworkAccessManager, so the process leaves QtNetwork objects
alive past QApplication teardown.
Delete them from a QCoreApplication::aboutToQuit handler instead of the
destructor: SettingsCache is a Q_GLOBAL_STATIC, so ~SettingsCache() runs
during static teardown, after QApplication is gone, and destroying the
network managers at that point is the post-application window that ASan
reports. The hook is installed on the first instance() call that has an
application object, since the first access may predate it.
* [Game] Follow the palette in judge message tags
Judge tags hardcoded a black prefix and a `theme:icons/scales` image inside rich
text, where `currentColor` cannot resolve and a palette change leaves the tag
unreadable. Render the scales glyph through the themed icon engine, register it
as a document image resource, and repaint the log when the palette or theme
changes.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [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>
A QPixmap snapshot cannot follow palette changes, so expose a QIcon whose
engine resolves the `currentColor` sentinel from the live palette on every
pixmap request. Icons stay legible across light/dark switches without
recreating actions or buttons.
- Resolve the asset, its bytes and its intrinsic size once, and drop them on
ThemeManager::themeChanged/paletteChanged. A repaint does no file I/O and
no theme.cfg parse; the palette color stays part of the render cache key.
- Centre the glyph on QRectF(rect).center(): QRect::center() floors and uses
right = left + width - 1, which shifted every 16x16 item-view glyph up and
left by a logical pixel and clipped its last row and column.
- Take a raster override's intrinsic size from QImageReader instead of the
24x24 SVG default, so a non-square override reports the box it draws.
- Set the device pixel ratio before inserting into the render cache, so a
cache hit no longer detaches the pixmap on every paint.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>