* [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>
Add the Game History dock to the left of replay tabs, wire it to the replay timeline, and register it with the View menu and layout reset.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Render the event index as a scrollable list of event boxes. Selecting an entry seeks the replay to that event and keeps the current event highlighted while playback advances.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Turn replay protocol events into short localized summaries with an actor, badge, and time so the history UI can render them without retaining the replay or duplicating the chat log.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* 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>
* [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>
* [Fonts] Restore DeckList Serif default and live-update all role readers
* [Fonts] Keep the DeckList fallback on the application font
DeckList is read by the deck editor headers, the filter tree and the deck
view too, not only by the two surfaces that hardcoded Serif. Resolving the
unset role to Serif in appFont() made every user without a fonts.toml see
Serif outside the deck list.
- appFont(DeckList) falls back to the application font again.
- ThemeManager::deckListFont() resolves the role and keeps the Serif
fallback for the two historical sites: the deck view zone headers and
the printed deck list.
- DeckView no longer repaints its viewport on fontsChanged; touching the
scene items already schedules the repaint in every attached view.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Fonts] Migrate DeckList and InterfaceDefault surfaces to theme roles
* [Fonts] Drop unused lambda captures in font-refresh handlers
* [Fonts] Fix pixel sizes and game overlay fonts from review
- Set the sizes the stylesheets carried with setPixelSize() rather than
setPointSize(): home buttons (34), banner (24), home group box (20),
the placeholder labels (14), the analytics drag handle (10), the EDHREC
navigation buttons (24), the user info popup (11/14/11/10/11) and the
game overlays (14/16/12). Points at 96 DPI were ~33% larger.
- The drag/total counters and tally columns keep the monospace family the
stylesheets set, so digit widths stop changing as counts tick and
white-space: pre stays aligned.
- applyDeckListFont() drops the cached tally rows before re-measuring, so
previewing a larger DeckList font no longer leaves the tally clipped.
- Tally labels and the constructor/fontsChanged pairs in the shared-deck
preview, quick setup, analytics panel, home box and public decks build
their font once in a local lambda instead of twice.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Fonts] Migrate CardText surfaces to theme font role
* [Fonts] Keep CardText call sites on the app font and fix px sizes
- CardText's unset fallback is qApp->font() again: only the three counter
painters (paintNumberEllipse, GeneralCounter, PlayerCounter) were Serif
before the role existed, and they now ask for it through
ThemeManager::cardTextFont(). Card names, player names, counters, the
overlay, the Archidekt and EDHREC labels and the completer delegate keep the
app font they used before.
- Use setPixelSize for the EDHREC card name (20px) and commander name (28px)
and details (16px) sizes: the stylesheets said px, and setPointSize made
them about a third larger.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Fonts] Migrate ChatLog surfaces to theme font role
* [Fonts] Keep ChatView on the app font and share the report log font
- ChatLog's unset fallback is qApp->font() again: ChatView never set a font
before the role existed, so an unset role must not switch chat and the game
log to monospace at the system GeneralFont size.
- Move the monospace fallback the report dialogs used before into
report_utils::reportLogFont(), which returns the theme's ChatLog role when it
is set and the old monospace otherwise. tab_report, dlg_my_reports and
dlg_report_user now share it, so they cannot drift and they honour a themed
size instead of overriding it.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Fonts] Add per-purpose font section to palette editor
* [Fonts] Make the editor's fallback and Reset preview-independent
- Add ThemeManager::defaultAppFont(), which resolves an unset role against the
startup font instead of currentFonts, and use it in FontGridWidget::loadFonts
so the grid never shows the live preview as the theme's default.
- Reload the live theme in PaletteEditorDialog::onReset before re-reading the
grid, so Reset restores the saved fonts before loadFonts resolves fallbacks.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Fonts] Wire FontConfig into ThemeManager with appFont and preview
* [Fonts] Restore the startup font when a config has no InterfaceDefault
- Capture the untouched application font next to the default palette and use
it in previewFonts' else branch, so a config without InterfaceDefault
restores the startup font instead of leaving the last preview applied.
- Drop the unpolish/polish loop from previewFonts: QApplication::setFont
already notifies every widget, and the loop ran on every editor auto-apply.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Unticking 'Save password' only disabled auto-connect: the dialog still handed
the stored verifier to the connection, so a password typed at the same time was
ignored, and a server that still has the legacy hash refused the login before it
could upgrade the account.
Clear the verifier in effect on untick, restore it on re-tick unless a password
has been typed, and drop the stale 'Password saved' hint. The downgrade abort
can now point at unticking the box and logging in with the password, without the
extra reconnect round-trip.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* Fix deck analysis request lifetime, result URLs and form encoding [skip ci]
* Preserve unknown cards in deck analysis exports
* Share deck analysis URL validation and drop the unused request latch
Both analysis interfaces carried a copy of normalizeAnalysisUrl; move it to one
header next to them. They are constructed per analysis and analyzeDeck is called
once, so the analysisStarted latch was unreachable; remove it from both.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Keep Archidekt navigation tied to its active request [skip ci]
* [Client] Keep Archidekt listing pagination after a failed navigation
Only update list mode and the next-page link when a listing or deck
response is accepted, so a failed deck navigation leaves the existing
listing scrollable. Retry still re-issues the failed request.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Timeline seeks only expose whole timer intervals, so jumping to an event could leave that event unprocessed until the next tick. Add an event-time seek that also applies events at the target timestamp.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Keep Commander Spellbook requests bound to the active deck [skip ci]
* [Client] Restore the bracket info tooltip when idle
retranslateUi() used to install the static "Why this bracket?" tooltip, but it now calls displayEstimate(), which returned early while no estimate existed. That left the info button without its idle tooltip.
Set the fallback where displayEstimate() owns the button's tooltip. An existing estimate still wins, and a language change refreshes the text.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* 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
* Make visual database mouse click modifiers properly add/decrement different card zones
* Use event->modifiers(), refactor
* Removed QApplication include
* Added visual database display documentation page
* Minor fixes
* Added documentation for the mouse interaction of the table view
* [Client] Add option to load decks from ManaBox
* [Client] Fix PR comments
* [Client] Fix UI and transaltion requests
* [Client] Fix for UI and parser not doing what expected when loading maybe board
* Add ManaBox support to docs
As pointed out in review comments
* [Client] Fix final PR comments
---------
Co-authored-by: tooomm <tooomm@users.noreply.github.com>
* [PictureLoader] Keep the download log from going quadratic
A fast scrollbar sweep over the visual card database requests a picture for
every cell it paints, so one gesture can ask for thousands of downloads. The
status bar built a seven-widget log entry for each of them and answered every
completion with findChildren() over the whole, unbounded log, rebuilding
url.toString() per child: O(n) per completion, O(n^2) per burst.
Profiling a sweep on master put 47% of all cycles on the GUI thread, with
qt_qFindChildren_helper at 12.4% of the process (a quarter of that thread) and
QWidgetPrivate::reparentFocusChildren at 4.1%. LagMonitor reported stalls of
3-18 s, growing with the backlog.
Keep at most MAX_LOG_ENTRIES entries and index them by URL, so a completion
and the 1 Hz cleanup only visit the matching entries. The progress bar follows
the entries that remain: dropping a finished entry moves both counters back,
dropping a pending one only its maximum. The log is a diagnostic view, not a
queue - the downloads themselves are unaffected.
* fixup! [PictureLoader] Keep the download log from going quadratic
Make the URL index a QMultiHash and say why: a deferred retry re-queues the same URL while its first log entry is still pending, so one URL can back several live entries.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [VisualDatabase] Keep the card grid scrolling on macOS
The view ignored the wheel whenever the vertical scroll bar was not
visible. On macOS, scroll bars are transient: once the fade-out finishes,
QScrollbarStyleAnimation hides the scroll bar widget, so isVisible() then
reports false even though the grid has more rows to scroll. Scrolling was
dead until something else brought the bar back.
Gate on the scroll bar's range instead: it says whether there is anything
to scroll, while visibility is only about presentation. An event with no
range to scroll is still ignored, so an enclosing scroll area can take it.
* Update card_database_visual_view.cpp
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [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>
* [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>
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>
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>
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>
ZoneViewZone::initializeCards() connected PendingCommand::finished only after ZoneViewZoneLogic::requestZoneDump() had already sent the command. Local games process the dump synchronously, so finished was emitted before the view connected and the library/sideboard view stayed empty.
Split requestZoneDump() into prepareZoneDump() and sendZoneDump(), and connect before sending, matching the TabRoom prepare/send convention.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Build] Use versionless Qt CMake commands
Replace the versioned qt6_add_executable, qt6_add_resources, qt6_add_shaders,\nqt6_add_translations and qt6_wrap_cpp calls with their unversioned forms.\nAll five are documented as available at the project's 6.4 minimum and accept\nthe same arguments.\n\nVerification: full client, Oracle, server and test build passed.
* versionless qt_finalize_target and _project
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: tooomm <tooomm@users.noreply.github.com>