- 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.
- 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.
* [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>
* [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>
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>
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>
The changed-file matcher reused the `*.sh` glob as a regular expression,
where the leading `*` applies to the `(^|/)` group instead of the file
name. Every path ending in "sh" matched, so NSIS include files (.nsh) were
handed to shellcheck and failed the format leg.
Match the suffix explicitly and keep the glob for the find(1) branch.
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>
* [Servatrice] Use compile-time checked connections
Replace the remaining SIGNAL/SLOT string connects in servatrice.cpp,
smtpclient.cpp, serversocketinterface.cpp and isl_interface.cpp with
pointer-to-member or lambda connects, and convert the string invokeMethod
calls to the functor overload.
QxtSmtp has overloaded signals, so those need qOverload. Slots inherited as
protected members and the QWebSocket error connection owned by the ISL fix
are left on lambdas/strings as appropriate; the vendored qxtsmtp.cpp is
untouched.
* Fix remaining review threads on the connection syntax
- Connect the IslInterface signals straight to the inherited Server slots
instead of forwarding lambdas.
- Capture servatrice explicitly in the queued admin lambdas so they do not
read this->servatrice after teardown.
- Move the websocket newConnection connect out of the per-pool loop.
- Terminate the logging category and restore pendingKey's indentation.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
The root already set CMAKE_AUTOMOC; add CMAKE_AUTOUIC and CMAKE_AUTORCC
next to it and drop the per-directory copies from 18 CMakeLists files.
These variables are inherited by the subdirectories, so the duplicates
never changed anything.
qt_standard_project_setup is not used: besides the AUTO* variables it
sets CMAKE_RUNTIME_OUTPUT_DIRECTORY on Windows, which breaks the
install(DIRECTORY "$<TARGET_FILE_DIR:...>") copies and the per-app
symbol paths, and appends to CMAKE_INSTALL_RPATH on Linux.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [CI] Make the hdiutil retry wrapper report failure
CPack reads the wrapper's exit status to decide whether the disk image was
created, so a zero exit turns a failed hdiutil into a later "Problem copying
the package" error that hides hdiutil's own message.
- Propagate hdiutil's real status instead of collapsing every failure to 1.
- Name the failing subcommand (create/attach/detach/convert) so the message
says which CPack step gave up.
- Count attempts rather than failures in the retry line.
* Update .ci/compile.sh
Co-authored-by: tooomm <tooomm@users.noreply.github.com>
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: tooomm <tooomm@users.noreply.github.com>
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>
The temporary password was written as a legacy salt+hash row, which makes the
next login look like an authentication downgrade. A client holding a saved
verifier refuses that downgrade, so the reset locked the account out of every
modern client.
Store the verifier and, where security/legacy_backup_key is configured, keep a
sealed legacy hash of the same temporary password as the downgrade backup, so
pre-challenge clients keep the credential path the reset used to provide.
Without the key no row shape serves both: the verifier and the legacy hash
cannot share one column, so those clients have no path at all. Warn at reset
time that the new password has no pre-challenge path, and say so in
servatrice.ini.example instead of promising that a reset gets them back in.
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>
* [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>
* [Servatrice] Fix silently broken socket error connections
QAbstractSocket::error() was removed in Qt 6 in favour of errorOccurred(). The ISL interface connected to the old name through SIGNAL(), which compiles but fails at runtime, so catchSocketError never ran. The WebSocket interface used the same name on QWebSocket, where it still exists but is deprecated since 6.5.
Use errorOccurred for both. The WebSocket connect needs a lambda because catchSocketError is a protected base-class slot and a base pointer-to-member is not accessible from the derived class.
* [Servatrice] Guard the WebSocket error connect against Qt 6.4
QWebSocket gained errorOccurred in 6.5 and dropped the error signal with it, so
the typed connect cannot compile against the 6.4 floor that Debian 12 and
Ubuntu 24.04 still use. Keep the typed connect on 6.5+ and fall back to the
old-style connect below it, which is what the code did before.
Verified both branches compile: forcing the guard to the else path and
rebuilding serversocketinterface.cpp succeeds, so the 6.4 spelling is intact.
* [Servatrice] Initialize ISL serverId before authentication
catchSocketError() removes islInterfaces[serverId], but the incoming
connection constructor left serverId uninitialized until initServer()
set it after the SSL handshake and certificate check. A socket error
during waitForEncrypted() therefore read an indeterminate key.
* [Servatrice] Correct the QWebSocket error signal note
error() was deprecated in 6.5, not removed; errorOccurred() was added in
6.5.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [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>
* [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>
Add the deprecation cutoff after FindQtRuntime so the compiler rejects any
API deprecated before the project's minimum Qt version. Qt 6.5 renamed the
switch, so the fallback name is used on 6.4.
The full client, Oracle, server and test build passes with the cutoff
enabled.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
The cache action creates its directory only when a prefix matches an entry, and it reads that directory unguarded when it saves, so a scope nobody has written to yet fails the job. Create it up front on every outcome, and fall back to the workspace path when the cache step was skipped and set no path output.
Co-authored-by: opencode <opencode@local>
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>
* [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>
* [Refactor] Move card item creation out of game logic
* [Game] Rename signal to createCardRequested per review
* [Game] Move attachment bookkeeping onto CardState
CardState::setAttachedTo now maintains the attachedCards graph, resets the
grid point and emits the zone events itself, so game logic no longer needs
to reach into the CardItem graphics item. CardItem::setAttachedTo delegates
to the state and keeps only the view teardown (deleteLater).
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Refactor] Retype card containers and zone logic onto CardState
* [Game] Restore attach bookkeeping via CardItem and repaint on state change
* [Refactor] Give custom zones a hidden graphics owner
Custom PileZoneLogic zones had no CardZone, so moving a card into one
left the CardItem drawn at its old spot. Create a hidden PileZone in
onCustomZoneAdded so cards are reparented and hidden.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Refactor] Move the remaining zone commands out of game_graphics/zones/
* [Game] Move zone dump request onto ZoneViewZoneLogic per review
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Refactor] Move table row mapping from TableZone into TableZoneLogic
* [Game] Use TableZoneLogic::ROW_COUNT directly per review
* [Format] Match clang-format line breaking in table_zone
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Folds in the closed#7410 (Move AnnotationDialog from game/ logic to
game_graphics/) now that layers 1-2 are closed and this stack bottoms on
master directly:
- Add game_graphics/dialogs/dlg_annotation.{h,cpp} and move the
QInputDialog subclass there; game/ no longer declares any widget type.
- Add game/player/token_info.h and game/player/move_top_cards_until_options.h.
- Move CardMenuActionType to game/player/card_menu_action_type.h.
- Graphics dialog headers include the new game/ headers instead of
defining the structs themselves.
No behavior change.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [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>
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>
* [Installer] Request quit for hidden Cockatrice instances and record holders in abort telemetry
* [Installer] Fix compiler warnings in holder probe and installer quit handler
* [VisualDeckStorage] Order the includes the way clang-format wants them
* [Installer] Ask a hidden instance to close, then offer to end it once
The previous attempt had the client answer a registered window message, which
cannot work for the update it was reported on: the instance being replaced is a
client from before that message exists, so it never answers, and the user is
left with "Cockatrice is still running" exactly as reported. Every step that can
free the files has to be the installer's, so the client side is reverted.
The probe now posts WM_CLOSE to the instance's own window whether that window is
on screen or only in the notification area: the request does not need a visible
window, an instance in the tray is the one this update has no other way to
reach, and the confirmation Cockatrice answers with is a window Qt shows itself,
so it is painted - unlike one restored from outside. A visible window is brought
forward first, and Qt's bookkeeping windows stay excluded, because a close to the
notification-area one terminates the instance without its confirmation.
A question of the instance's own no longer suppresses the close. Withholding it
protected nobody: the installer asked the user to close a program it had never
asked, and a current client refuses the close while its confirmation is pending
anyway, leaving the question where it is. The launcher mode keeps the
visible-only rule, because a launcher without a window has already answered its
own close and the wait ends it after a grace instead of asking a user who is not
there.
The guard asked the same question until the user gave up: after the automatic
close failed it offered to end the programs, then re-ran the probe and showed
the same dialog word for word when the answer was No, which is how an abort was
reported three times over. It now asks once - Yes ends the Cockatrice, Oracle
and Servatrice programs the payload installs and says what that costs, No
cancels - and what survives an end is named in the abort instead of being put to
the user as the same question again. The uninstaller's guard and the recovery
path after a failed purge follow the same shape.
Kept: the abort telemetry that records the holders, the close result and the end
result as event data.
---------
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Installer] End a launcher that has no window to ask instead of prompting
The in-app updater starts this installer detached and then closes itself. A
client that accepts that close hides its window before it exits, and if it
then hangs in shutdown there is nothing left to ask: the holder probe
deliberately never posts WM_CLOSE to an instance it cannot see a window for,
so the "ask it to close again" answer only repeated itself until the user
picked Cancel and the installation aborted (Sentry COCKATRICE-DESKTOP-J).
The launcher wait now asks the launcher itself before asking the user. A new
probe mode, --close-launcher, answers whether that one process has a window a
close could reach at all; it is limited to a pid that still runs from the
directory being replaced, so a reused process id cannot be hit. When there is
no window, the installer gives the shutdown it already started one full grace
and then ends it through the handle it waited on, which is what the dialog's
Yes answer did. The prompt remains for the case it exists for: a window is up,
the instance may be waiting on its own question, and No now means only "keep
waiting".
The client is left alone: an already-installed version runs the installer
during its own update, so a change there cannot reach the run that aborts.
* [libcockatrice_rng] Include immintrin.h for AVX2 builds in SFMT
---------
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
* [Client] Stop reporting local login aborts as missing client features
Local aborts (unusable saved verifier, refused authentication downgrade,
failed scrypt derivation) reuse RespClientUpdateRequired with an empty
feature list, and the handler discarded the reason and advised a client
update. Report the abort with the cause instead, and report a strict
server's challenge_response_auth answer to a credential-less login as the
missing password it is.
* [Client] Keep the login abort messages in one named set
The local login aborts each carried their wording inline at the emit site, so
changing the tone or the remedy meant editing several functions. Collect them
as named constants next to RemoteClient instead, translated at the use sites
with the RemoteClient context so lupdate still picks them up.
* [Client] Reword the login abort explanations to match what happens
The first wording sent users to a server operator to 'migrate' an account,
which operators cannot do, and told them to 'save' a login the client saves by
itself. It also blamed the device for a derivation failure the server's
parameters can cause, and the dialog led with a line that said nothing while
the reason sat in the informative slot.
Say what the client actually does in each case, give the step that works, and
show the reason as the dialog's main text. The unreachable fallback sentence
about the login not reaching the server is gone.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Report password-reset rate limits instead of a generic failure
sigForgotPasswordError carried no response code, so rate limiting, a
disabled reset feature, an unknown user and a bad token all produced
"contact the server operator". Users who were only rate limited retried,
which kept them rate limited.
* [Client] Move the reset-password warning out of the response switch
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Installer] Recover leftover payload files when previous uninstaller leaves them behind
* [Installer] Fix leftover recovery on the 64-bit auto-uninstall path
- Record the 64-bit uninstaller exit code after ExecWait returns. It was
captured before the call (always empty), so WaitForOldPayloadToDisappear
always aborted before RemoveOldPayloadLeftovers could run.
- Restore the pre-uninstall user-data backup before aborting out of the wait,
including the guard's cancel and probe-failure prompts, so the only copy
cannot be stranded in %TEMP% for the next run to wipe.
- Report the leftover removal as done only when every payload executable is
gone and RetryDllPurge reports no stale DLLs, instead of checking
cockatrice.exe alone.
- Return directly after a successful recovery instead of reusing the poll
counter, so the log stops claiming the directory "was already clear" and
the normal settle delay is kept.
* [Installer] Delete the old uninstaller only after recovery succeeds
RemoveOldPayloadLeftovers cleared the attribute and deleted
$INSTDIR\uninstall.exe on every attempt, before the attempt was known to
work. On failure the installer aborts telling the user to run it again, but
UninstallString then points at a missing uninstaller: ExecWait leaves the
exit code empty, WaitForOldPayloadToDisappear takes the "cancelled or
failed" branch, and RemoveOldPayloadLeftovers is gated on exit code 0, so
no later run can ever get past it - a half-removed old install the previous
revision never produced.
Delete it in rop_done instead, after $OldPayloadRemoved is set.
WriteUninstaller recreates it on success anyway, and nothing in the
success verdict depends on it being gone.
---------
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Installer] Offer to end running Cockatrice immediately when auto-close fails
* [Installer] Keep the way out that costs nothing in the first prompt
The prompt now offers to end Cockatrice, so what the deleted close-again text
said has to survive in it: an instance the probe reports as waiting for an
answer to a question of its own has been brought forward, and answering that
window is the way out that ends nothing. Say so, and say where to go when Yes
leaves it listed - Task Manager - so an end that cannot take effect does not
loop on the same dialog.
* [Installer] Bring the comments back in line with the code
GuardCloseTried no longer counts closes, and the file header, CloseHolders and
GuardAppsNotRunning still described an escalation after a second close, as did
the probe's --end-holders note.
* [Installer] End Oracle and Servatrice too when the guard's end answer is confirmed
The guard's end answer only ever stopped cockatrice.exe, so an Oracle holding
the card database or a Servatrice service kept the payload locked and the
prompt came back with nothing left that could free it.
The probe now ends every program the payload installs, services included:
isPayloadProgram() - the list restart_manager_holders already used for its
parent walk - decides the set, so a scanner holding a DLL or a program the
user keeps in the install directory is still named and left to the user. A
service is terminated rather than sent a stop through the SCM, which notices
the process exit and marks it stopped afterwards.
The guard prompt now says what ending costs: an unsaved deck, an unwritten
setting, players on a running Servatrice, and an Oracle mid-way through the
card database. It also keeps the notification-area hint, which the probe's
"has no window to ask" answer depends on, and drops the claim that Oracle
and Servatrice had been brought forward.
* [Installer] Give every running-program prompt the same end-or-retry answers
Four prompts meet a running program and each offered a different set: the
guard ended it, the launcher wait only kept waiting, and the recovery prompt
after a failed purge could only ask Cockatrice to close. With Oracle or
Servatrice holding the files, that last one had no answer that could free
anything - Yes and No both retried the purge and Cancel aborted an install
whose old version was already gone.
All four now follow the guard's shape: one automatic attempt, then a prompt
whose Yes ends the programs it names and whose No retries, with Cancel to
give up.
- RecoverFromHolders closes once and purges before it asks anything, then
offers EndHolders - the same end answer as the guard - instead of a close
the user had to trigger himself.
- WaitForLauncherToExit can end the launcher by pid, the one process it
holds a pid for, rather than looping 60 s waits with Cancel as the only
way out.
- END_COST and AFTER_CLOSING_GUIDANCE are !defines, so the cost of ending
and the guidance for the answers that end nothing read the same in every
prompt.
* [Installer] End the launcher through the handle it was waited on
The "end it" answer used to look the launcher's pid up again after a dialog that
can stay open as long as the user takes, so a pid that had been reused in the
meantime - or a /PID= that was never the launcher - would be terminated instead.
The handle the wait already holds names that process and cannot be handed to
another, so the terminate answer now acts on it.
PROCESS_TERMINATE is requested along with PROCESS_SYNCHRONIZE; when only the
synchronize right is granted the launcher is still waited on and the answer falls
back to asking it to close, instead of reporting a live launcher as gone and
writing the payload over it.
* [Installer] Say what the end answer ends, and report a failed end
The prompt offered "end every program listed above", but EndHolders only ends the
programs the payload installs: a scanner holding a payload DLL, or a service the
Restart Manager names by its long name, was listed and then silently left alone, so
Yes looked like it did nothing and the same dialog came back unchanged.
The wording now says which programs Yes ends and that anything else has to be closed
by hand, and the round after an end that did not free the files says so, so the same
dialog no longer reappears word for word. The three prompts are one macro so they
cannot drift apart again.
The added sentences eat into the 1024 characters one NSIS string holds, so the
holder line's budget drops from 400 to 320 characters to keep the longest message
(cancel line included) inside it: 977 characters, measured against the worst case
the probe can produce.
---------
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Updater] Refuse a close that arrives while one is already asking
MainWindow::closeEvent() had a re-entrancy guard that returned without touching
the event. A QCloseEvent is accepted unless something ignores it, so a second
close request - an installer replacing our files posts one every 30 s - accepted
the close while the first one was still blocked on its own confirmation. close()
then hid this window, hid the confirmation dialog with it, and left the process
running inside the nested loop that dialog had opened: no window, no way to
answer it, and every later close request accepted into the same dead end. That
is what an update looked like when it could not close a running client, and it
is why the installer could name the process, bring it forward and ask it to
close for minutes without anything happening.
Track how far the shutdown has got instead. While a confirmation is up, a close
from anywhere else is refused, which leaves the question in front of the user;
once the shutdown has been accepted, a duplicate close (Qt delivers the event
twice on some platforms) is accepted again so it cannot cancel the quit.
* [Installer] Leave an instance that is asking a question alone
An instance that answered a close by hiding rather than quitting - the state the
client used to get into - has no visible window left, and the confirmation it is
waiting on is hidden with the window that owns it. The close probe only ever
looked at visible windows, so it restored the main window, posted WM_CLOSE into
the same dead end, and reported "still running" without saying why.
Show every window the instance left hidden before choosing the one to close, and
skip the close when the window it would reach is disabled: a modal dialog of the
instance's own is up, and posting the close then only hides the question again.
Name what is left of the window in the report as well - gone, still open, hidden
again, and whether the thread answers at all - so the log tells a refused close
from one that was never delivered.
* [Installer] Offer to end a Cockatrice that will not close
A close is a request, and an instance that does not answer it - waiting on a
question nobody is looking at, or stuck where a close cannot reach - leaves the
user with no way forward: the files cannot be replaced while it runs, and
clicking Yes again only repeats the request.
After a second close that changed nothing, offer the only thing that can free
the files: end it. The prompt says what it costs - an unsaved deck, a running
game or a setting it has not written yet - the default button is the safe one,
and it is a separate answer rather than a relabelled "Yes", so it is not
reached by clicking through the close prompt. Only cockatrice.exe is ever
ended: Oracle writes the card database in place and Servatrice is a service, so
both are named and left to the user, exactly as they are for the close.
The probe gains an --end-holders mode for it, and the uninstaller's guard gets
the same offer.
* [Installer] Never name the process that is doing the asking
The old uninstaller is started with _?= so it runs in place, from the very
directory it is emptying, and an installer can be downloaded into the folder it
installs to. Both are then processes running from the directory, so the probe
named them - and the guard asked the user to close the program that was asking.
That question cannot be answered: a close and the end offer both skip anything
that is not cockatrice.exe, so the guard repeated itself until the update or the
uninstall was cancelled.
The probe and the processes that started it are now never named, by either
lookup. The walk up the parent chain stops at the first program the payload
installs, because in an in-app update the installer's own parent is the
Cockatrice instance being replaced - the one the guard exists to name.
The Windows check held its locked file in the script that starts the probe, so
that holder is excluded by design now and the check read it as "nothing found".
It holds the file from a process of its own instead, and asserts the exclusion
itself: a file that only the asking process holds comes back as nothing.
* [Installer] Restore only the windows a user could answer
Showing every hidden window an instance owns put Qt's own bookkeeping windows on
screen: the tray icon keeps one to receive the TaskbarCreated message, styled
WS_OVERLAPPED and titled QTrayIconMessageWindow. Restored, it is an empty white
rectangle in the taskbar with no title bar to close it - it has no window
procedure that would hide it again, and the process is busy in the very loop the
restore is meant to break, so it stayed until the process was force-ended.
Only framed, non-empty windows are restored and considered for the close now.
That also keeps the close off Qt's tray window, which answers WM_CLOSE by asking
the application to terminate - a quit that skips the confirmation the close is
supposed to reach.
* [Installer] Uninstall the previous version before installing over it
A regular install over an existing one asked "a previous version must be
uninstalled", then ran the old uninstaller interactively. That uninstaller
put its confirmation page behind the installer window, where it could not be
answered, and it only removed the installation after the installer had
already written the new one over it.
Run the previous version's uninstaller silently instead. The installer has
already asked the question the user answered, so a second, hidden confirmation
is pure risk: with /S there is no window to be behind, and ExecWait is then
waiting for something that cannot outlive it. QuietUninstallString is used
when the old installation has one, otherwise /S is appended.
ExecWait only waits while the uninstaller runs in place: one that did not get
a usable _?= copies itself to %TEMP% and returns at once, while the copy keeps
deleting - which is how an uninstall can finish after the install it was
supposed to precede. Log the exit code and wait for the old cockatrice.exe to
disappear before restoring user data or writing anything, and abort with a
message instead of installing into a directory that is still being emptied.
Cancelling the "must be uninstalled" prompt now quits the installer; it used
to skip the page and install over the old version anyway.
* [Installer] Do not show windows the application has hidden
The guard restored every hidden window an instance owned before asking it to
close, so that a confirmation hidden together with its window would come back.
Windows does as it is told and shows them; the application does not, because Qt
still believes they are hidden and never paints them. What the user gets is a
blank white rectangle, on screen and on the taskbar, that answers nothing - and
the question it was meant to bring back is not answerable in it either.
An instance with no window a user could answer is named and left to the user
instead, and the prompt says what to do about it: open it from the notification
area. Ending it stays the last resort, offered once a close has been asked for
twice.
The abort after a failed uninstall now says what it saw - the old version is
still there after its uninstaller ran - instead of promising a removal that is
not happening, which is the state a cancelled old uninstaller leaves behind.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Installer] Check for running instances with the Restart Manager probe
The running-instance guard was built on powershell.exe started from the
32-bit NSIS stub, which is the 32-bit Windows PowerShell: its Get-Process
cannot read the path of a 64-bit process, so a running 64-bit Cockatrice
was reported as not running, the update went ahead, and the purge then
failed on the DLLs that instance still had mapped.
Use tools/installer-holder-probe instead - the same Restart Manager lookup
the abort path already had - before anything is replaced and in the
uninstaller as well. It is bitness-agnostic, so the Sysnative and WMI
fallbacks are gone.
The guard looks only at the payload, asks Cockatrice to close once by
itself (WM_CLOSE, the same request as the window's close button) and then
offers close again / retry after closing by hand / cancel for whatever the
close cannot reach - Oracle writes the card database in place, Servatrice
has no window - without force-stopping anything. A failed purge offers the
same close-and-retry, tries one last purge when nobody is named, and every
dialog shows in every mode: a silent reinstall stays silent until
something goes wrong.
* [Installer] Name processes running from the install directory
The running-instance guard answers from the Restart Manager, which reports
who has a file open. A running instance does not have to have one open: its
own image and the modules it loaded are mapped into it rather than opened by
it, and a mapping is not reliably reported. That is the state the probe was
added for - every DLL still on disk, an instance running from the directory,
and "nothing is holding them any more" as the answer.
A guard built on file handles alone is blind to exactly that instance, so it
passes, the purge fails, the holder lookup after it names nobody either, and
the install ends in a warning that no running program was found: no close is
attempted and no retry is offered. A manual install with another instance
open never asks it to close at all.
Name every process whose image lives in the directory being replaced as
well, with the same bitness-safe QueryFullProcessImageNameW the probe
already uses to label its holders. The image lookup is listed first - it is
the instance the user is asked to close - and the Restart Manager entries
follow with the pids already named dropped, so the close still reaches
everything that holds a file. A Restart Manager call that fails only makes
the probe unable to answer when the image lookup named nobody either.
Ask instead of dead-ending when the probe cannot answer at all: Retry checks
again, Ignore carries on, Abort cancels, in the installer and the uninstaller
alike, and the abort dialog says the check failed instead of claiming no
running program was found.
The Windows CI probe check covers the new lookup with a process started from
the directory that holds no file open.
* [Installer] Offer to wait again when the launcher has not exited
A client that is still closing after 60 s is usually not hung: it is showing
its own "quit anyway?" confirmation for an unsaved deck, an open game or a
card database update, and the user is the one who has to answer it. The
installer answered that with a plain warning and stopped, so the only way
forward was to close Cockatrice by hand and run the update again - which on
an in-app update means finding the downloaded installer.
Ask instead: Retry brings the client to the front, asks it to close again
(WM_CLOSE, nothing forced) and keeps waiting on its handle, Cancel stops as
before. There is no "install anyway" answer because a live process has its
image and Qt DLLs mapped, so writing the payload over them is the broken
install this wait exists to prevent - and the guard that runs right after
names the same process by its image path and stops there, so ignoring would
only move the same question a moment later.
* [Installer] Ask about a launcher that has not exited after ten seconds
A minute of silence is a long time to sit in front of an installer that has no
window yet, and the case it is waiting for is not a slow flush: a client that
started an update exits in well under a second, so a minute means it is almost
always showing its own "quit anyway?" confirmation with nobody looking at it.
Wait ten seconds for the first question instead, and the full grace for the
ones after it. The normal path is untouched - WaitForSingleObject returns as
soon as the handle signals, so a client that exits in 200 ms still costs 200
ms - and answering the first question once does not turn into a stream of
prompts while the user is in Cockatrice's window.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Installer] Offer to close blocking processes and retry purge on upgrade
* [Installer] Retry the purge after closing only our own applications
The first version of this fix closed every Restart Manager holder through
RmShutdown, forcing what refused to close. RmShutdown also stops services, and
the only API that brings them back is RmRestart, which would restart Cockatrice
itself into the files that are about to be replaced; the forced pass also
contradicts the running-instance guard, which never force-closes Cockatrice
because it may be holding an unsaved deck. The guard ran first anyway, so for
the case the fix exists for - a Cockatrice restarted during the update - the
probe never got to run: the guard waited 60 s and aborted.
Keep the prompt and the retry, but ask only Cockatrice, Oracle and Servatrice to
close, with WM_CLOSE and nothing else, and leave every other holder named by the
probe to the user. Repeat the prompt while the user keeps saying Yes, refresh
the names after a failed retry, and keep the /R abort message pointing at the
downloaded installer.
Also clear the read-only attribute before every purge attempt. Delete and RMDir
leave such a file in place, so the retry could never make progress, and the
diagnostics branch that named the cause had no action behind it.
The holder probe goes back to being diagnostic-only, so its Restart Manager
code and the --close argument are dropped.
* [Installer] Make an accepted close of the running client actually land
WM_CLOSE is a request, and Cockatrice answers it: while a card database
update is running, a game is open or a deck is unsaved it puts up its own
"are you sure?" confirmation. That dialog opened behind the installer's
prompt, so the user saw nothing happen; the next Yes was posted to a main
window the app's own modal dialog had disabled, so it was ignored as well,
and the identical yes/no prompt came back with no hint of why.
- Activate the matching window before posting WM_CLOSE, so the app's own
confirmation is in front where it can be answered.
- Wait up to 30 s for cockatrice.exe to exit instead of purging after a
fixed 2 s, so the purge measures the app gone rather than its dialog.
- On the second and later rounds ask with Retry/Cancel and say what is
needed, instead of repeating the same offer that just did nothing.
- Stop promising Oracle and Servatrice: they are console applications, so
CloseMainWindow() has nothing to send WM_CLOSE to. They are not
force-stopped either - Oracle writes the card database in place.
* [Installer] Close the client from the holder probe, not PowerShell
Accepting the offer to close did nothing on a real machine: the instance stayed
up, never came forward, and the same prompt came back.
The prompt names its holders from the Restart Manager probe, but the close itself
went through powershell.exe: Get-Process -Name, a strict "path starts with
$INSTDIR" filter, then Process.CloseMainWindow() and a WScript.Shell AppActivate.
Every step of that is silent. A process whose path cannot be read is filtered
out, a process .NET finds no main window for makes CloseMainWindow() return
false without posting anything, and AppActivate is refused for a background
caller - so an attempted close, a missed close and a close request the client
ignored all looked the same in the log: nothing.
Ask the probe instead. installer-holder-probe --close-holders closes exactly the
processes Restart Manager just named as holding a file of the version being
replaced, so no executable path has to be guessed and a third party application
that merely shares the image name is never in the set. Per instance it restores
and brings the top-level window forward (AttachThreadInput, so the client's own
"really quit?" confirmation is on screen rather than behind the installer), posts
WM_CLOSE to the main window, and waits up to 30 s for the process to exit. Its
report names every holder with what happened to it, including the ones left to
the user, and goes into the installer log as one line.
That also removes the separate PowerShell polling loop that used to wait for the
close: the probe holds a handle on the process it asked, so the purge that
follows measures a settled directory.
Oracle and Servatrice are still never closed, and nothing is force-stopped.
---------
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [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>
* [Installer] Retry locked DLL purge before aborting upgrade
* [Installer] Keep the purge verdict when the portable purge runs
$IsUpgrade was read from IsExistingCockatriceInstall after the portable purge,
which deletes the very markers that function looks for. A purge whose only
survivors were non-Qt DLLs (abseil_dll.dll, libssl-3-x64.dll) or content under
Plugins therefore reported "not an install", $IsUpgrade became 0 and the abort
was skipped - the mixed DLL set the retry exists to prevent. Measure the
directory before either purge block, where it still describes the directory as
it arrived, and let RetryDllPurge's own verdict decide the abort.
A portable install into a directory without portable.dat is neither purge
block, so $StaleRuntimeAfterPurge was never assigned there and the abort could
not fire either. Track whether a purge ran and measure that directory with
HasStaleRuntimeFiles instead, which is what every upgrade did before the retry.
---------
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
Co-authored-by: BruebachL <44814898+BruebachL@users.noreply.github.com>
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Network] Do not format discarded debug messages, parse without memmove
Protocol debug logging gets the full protobuf message rendered through
TextFormat and reflection. On the server this happened before every
session/room/game/moderator/admin/developer command, even though the
default Server_ProtocolHandler::logDebugMessage() is a no-op and even
when servatrice has no log file configured. On the clients the same
string was built as an argument to qCDebug(), which evaluates its
arguments regardless of whether the category is enabled.
Server_ProtocolHandler now exposes a virtual logsDebugMessages() hook.
The base implementation returns false, so handlers without a log
destination spend nothing. AbstractServerSocketInterface overrides it
with ServerLogger::isLoggingEnabled(), which checks the log file and the
server/writelog setting that logMessage() itself consults. All debug
message call sites check the hook before formatting. The client
send/receive paths check QLoggingCategory::isDebugEnabled() first, the
same pattern AbstractClient already uses for its slow-command warning.
ISL message decoding does the same.
Byte stream decoding called inputBuffer.remove(0, n) for the length
header and then again for every message. Each call shifts all remaining
bytes, so a read carrying k messages cost O(k * bytes remaining). The
three receive loops (remote client, TCP server interface, ISL interface)
now parse at a read offset and compact the consumed prefix once, when
they stop because a full message is not buffered yet. No buffering or
size-limit behavior changes.
Also in this batch:
- Server_ProtocolHandler::processGameCommandContainer() copied the whole
games map under gameListMutex just to look up one game id. A new
getGameInfo() performs the single lookup and returns the room/player
ids.
- The TCP receive loop's `ok` flag is initialized, so a ParseFromArray
exception can no longer leave it indeterminate before `if (ok)`.
- The exception log prints only the message being parsed instead of
hex-dumping the entire pending buffer.
Full build and ctest (628 tests) pass, including the server tests.
* [Network] Address review comments
- Move getGameInfo() out of the header into server_abstractuserinterface.cpp.
- Drop the consumed prefix once after the parse loop instead of on every
early return: the two "not enough data" exits now break out of the loop.
Applied to the remote client, TCP server and ISL receive loops.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* Retranslate all open tabs on language change
* Address review: deeper Visual DB retranslation, index-based Archidekt size logic
In Qt 6, setItemText() on the current item emits currentTextChanged but not
currentIndexChanged, so connecting the Archidekt combos to currentTextChanged
fired a search on every language change. Both combos now use currentIndexChanged,
and buildSearchUrl() compares the size-logic combo by index so translated labels
cannot drop sizeLogic.
* Derive display mode button text in one place
* [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>
The local picture loader walked the whole custom picture tree every ten
seconds on the GUI thread. Watch the root and every subdirectory with
QFileSystemWatcher, debounce bursts of events into a single 500 ms refresh,
and keep a five-minute fallback poll for filesystems where watching fails.
Add an explicit-path constructor and indexedPaths() so the indexing can be
tested without SettingsCache, and emit indexChanged() after a rebuild.
Verification: cockatrice built.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [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>
Read the avatar with setAutoTransform(true) so camera orientation is
applied, and ask QImageReader for a scaled decode at MAX_AVATAR_DIMENSION
instead of decoding a full-resolution photo and scaling it afterwards.
Verification: cockatrice built.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [QtUtils] Add parent lookup, layout clearing, scroll area and monospace helpers
* [QtUtils] Adopt shared Qt helpers across the client
* [QtUtils] Add layout creation and relayout helpers
* Swap parameter order (spacing before margin) and remove static from relayout()
* [QtUtils] Use relayout() after rebuilding dynamic layouts
* [QtUtils] Use makeLayout() for zero-margin layouts
* Consolidate setSpacing() calls into makeLayout() using new parameter order
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [QtUtils] Add layout creation and relayout helpers
* Swap parameter order (spacing before margin) and remove static from relayout()
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
A palette preview repaints widgets through applyStyleAndPalette without running
themeChangedSlot, so generator-owned pixmaps stayed at the previous palette
until the next full theme switch. Clear them where the palette is applied.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
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>
* Retranslate Home tab when the language changes
TabSupervisor::retranslateUi() never included the Home tab, and
HomeWidget set its button text only once, when the buttons were
created. Switching languages left the Home tab in the old language
until it was reopened.
Store the Home buttons as members, add HomeWidget::retranslateUi()
to set their text, and call it from TabHome and TabSupervisor. The
connect button text still only changes for the Connecting,
Disconnected and LoggedIn statuses, as before.
* Address review: plain ClientStatus, drop optional and placeholder
* Refactor display type toggler and add language change handling in VisualDeckDisplayOptionsWidget
* Address review: extract updateDisplayTypeButton(), document changeEvent
* Restore trailing newline in visual_deck_display_options_widget.cpp
* [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>
* [CardDB] Stream card properties from blobs
Parsed and cached cards used to keep a materialized QHash<QString,
QString> for their properties: LazyPropertiesHash only deferred building
that hash until the first query, and the cache writer re-serialized it on
save.
- LazyPropertiesHash answers value(), contains(), size(), isEmpty() and
keys() straight out of the serialized blob, so a property query no
longer builds a hash.
- serialize() is public, and the cache writer passes the stored blob
through instead of serializing a hash it just rebuilt.
- CardInfo and PrintingInfo expose getPropertiesBlob() and
getPropertyCount(); PrintingInfo::operator== compares key by key
without materializing either side.
- The XML parsers build cards via newInstanceFromPropertiesBlob(), which
derives the simple name and the alternate names that the parser used
to keep next to the hash.
The reload cycle fix stays out of this change; it depends on changing
the CardInfo self pointer and is reviewed separately.
* [CardDB] Address review comments on streaming properties
* [CardDB] Address final review nits on property streaming
Move BlobReader and the blob accessors into LazyPropertiesHash as private
members so the parsing helper is declared with the class instead of hiding
in the translation unit.
Add LazyPropertiesHash::serializedFrom() and use it where a PrintingInfo is
built from a fresh property hash, replacing the nested
LazyPropertiesHash(LazyPropertiesHash::serialize(props)) construction.
The pre-materialized QHash constructor stays: CardInfo's non-blob factory
still uses it for the oracle import path, and the tests cover it directly.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [CardDB] Avoid copying printing maps and property strings
Reading a card's sets was a measured hotspot: `SetToPrintingsMap setMap =
card->getSets();` copies the whole QMap, and then iterating it with a
non-const iterator detaches and deep-copies every PrintingInfo, including
its property hash, on first use. A micro-benchmark of that exact pattern
(copy plus non-const iteration) came out about 19x slower than a const
reference over the same data.
The querier, the cache writer, the XML writer, the picture loader and the
printing/set selection widgets now take `getSets()` by const reference and
only use const iterators. The tests that iterate generated data are
covered by the existing card database and picture loader suites.
String accessors were also returning by value where the value lives in a
hash that has already been materialized:
- CardInfo::getProperty() and the legacy getCardType()/getColors()/
getManaCost()/getCmc()/getPowTough()/getLoyalty() accessors,
- PrintingInfo::getProperty(), getUuid(), getFlavorName(), getArtist(),
and getSet(),
- ExactCard::getPrinting() and getCardPtr(),
- CardSet::getShortName(), getLongName() and getSetType().
getPrinting() in particular copied a PrintingInfo on every call, and a
copy of an unmaterialized PrintingInfo has to take the properties mutex.
Callers that need an owned copy still get one; the rest just read.
Other allocation removals in the same path:
- LazyPropertiesHash::ensureMaterialized() checks an atomic flag before
taking the mutex, so property reads after the first one do not lock.
The flag is release-stored after materialization and acquire-loaded by
readers/copies, preserving the existing locking for the first read.
- CardSet::getCorrectedShortName() no longer rebuilds its QSet of
reserved Windows file names on every call.
- CardSet::getIsKnownIgnored() no longer formats the release date into a
throwaway string just to check its length.
- The querier builds its card-type/format count caches without
`cards.values()` or a properties key list, reserves its result lists,
and picks a random card without materializing `cards.keys()`.
- The querier's pinned-printing path reuses the CardInfoPtr it already
has instead of looking the card up again by name.
- CardDatabaseCache/XML serialization copy the alt-name QSet by
reference.
- The picture loader compiles its !prop!/!set! regular expressions once
instead of per URL template, and walks the transform map with an
iterator instead of keys() plus operator[].
Full build and ctest (628 tests) pass.
* [CardDB] Address review comments
PrintingInfo is a value type: it is stored in QLists and returned by value
from the querier, so a const-ref getter can hand out a reference into a
temporary. Return the QStrings and the CardSetPtr by value again.
The constFind lookup stays, so a property read still never materializes the
lazy property hash - only the last shared-data refcount is paid.
* [CardDB] Undo the const-ref QString return changes
QString is implicitly shared, so returning by value costs only a refcount
and avoids handing out references into a lazily materialized hash. The
constFind lookups stay, so a property read still never materializes the
hash.
* [CardDB] Use QMap::asKeyValueRange() in transformUrl
The range is a view over the map's own iterators and yields references to
the stored key and value, so it keeps the no-allocation property of the
manual const iteration.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Filters] Precompute term data instead of redoing it per card
Card filters are evaluated once per card in the database, and several
attributes derived constant values from the filter term on every single
evaluation:
- acceptColor() ran up to twelve case-insensitive string replacements and
a space strip for each card, then called getColors() again for every
character of the term.
- acceptRarity() ran the rarity abbreviation expansion loop per card.
- acceptManaCost() uppercased and sorted the term per card, before
sorting the card's own cost.
- acceptLoyalty() and acceptFormat() trimmed/lowercased the term per card.
- relationCheck() re-parsed the numeric operator and operand per card,
for every cmc/power/toughness/loyalty comparison.
The term of a FilterItem never changes, so these values are now computed
once in the constructor and only the card-dependent part remains in the
accept path. For colors this also removes the repeat getColors() call,
which materializes the printing properties hash.
Additionally:
- CardInfoPtr is passed by const reference through FilterItemList,
FilterItem and FilterTree instead of by value, removing an atomic
refcount pair per card per filter node.
- Main/sub type filtering no longer allocates a QStringList via split();
it looks the " - " separator up directly and compares QStringViews.
- acceptSet() and acceptPowerToughness() fetch the printing/set or the
power/toughness string once instead of once per branch/accessor.
No filtering behavior is intended to change. Invalid numeric terms (for
example a bare "<") now return false instead of indexing trimmedTerm[1]
past the end.
Verified with filter_string_test (42 tests) and a full client build.
* [Filters] Address review comments
- Replace the main/sub type separator accessor function with a plain
namespace-scope constant.
- Take card type, colors, cmc, loyalty, power/toughness and the printing's
set by value; those getters already return by value.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Search] Stop allocating per card in card-name completion
CardSearchModel::updateSearchResults() runs on every textEdited()
signal, walks the whole card database and runs Levenshtein against every
matching name. Three costs dominated that path:
1. Every candidate built a QStringList of its searchable names, then
lowercased each name into another QString, only to break out of the
loop after the first hit.
2. The sort comparator called sortableName() for each comparison, which
re-fetched the name (QMap lookup plus QString construction) O(n log n)
times.
3. levenshteinDistance() allocated a full len1 x len2 vector of vectors.
The search model now lowercases the one name it tests directly, stores
the tie-breaking name length once per result, and the comparator only
compares integers. levenshteinDistance() uses the standard two-row
formulation: same result, O(len2) memory instead of O(len1*len2), with
early exits for empty and equal strings.
FilterString query closures get the same treatment:
- Set queries iterate the set map directly instead of materializing
keys()/values() lists for every card.
- Rarity queries iterate printings in place instead of copying them into
a QList<PrintingInfo> first.
- Power/toughness split the string with indexOf and compare QStringViews
instead of allocating a QStringList.
- Field queries do one hash lookup instead of contains() followed by
value().
- Color queries bind the colors string by reference instead of copying.
Verified with filter_string_test (42 tests) and a full client build.
* [Search] Address review comments
- Use std::any_of/std::none_of over the set map's key/value iterators
instead of hand-rolled loops.
- Extract the name matching into matchingLowercaseName() returning
std::optional<QString>.
- Reuse the matched name's length for the distance tie-break instead of
looking the name up again, removing one getLocalizedName() call per card.
- Take the color strings by value; the getters already return by value.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [CardDB] Sort power/toughness without splitting every comparison
When the card database is sorted by the P/T column, lessThan() runs
O(n log n) times over the whole view, and each call split both strings
into a QStringList ("power/toughness") and then read the two elements
back. For the roughly 36k cards in the database that is millions of
list and string allocations, all for a value that contains at most one
separator.
The comparator now locates the single '/' with indexOf and passes the
two halves as QStringView to lessThanNumerically(), which is changed
from QString to QStringView. The numeric parsing, the "1+*" fallback and
the locale-aware tie-break are unchanged, and strings with no separator
or more than one still fall through to the plain string comparison,
exactly as split()-based size checks did before.
lessThanNumerically() also initializes its two bool out-parameters; they
were declared uninitialized, which only worked because toFloat() always
assigns them.
Compared old and new comparator over a corpus of 2024 power/toughness
strings (4096576 ordered pairs, including malformed values): zero
differences. Full build and ctest (628 tests) pass.
* Extract well-formedness checks in P/T comparator
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [VisualDatabase] Use scoped QSignalBlockers for filter updates
updateFilterModel() blocked signals on the filter model and its filter tree,\nthen unblocked each of them twice. The second pair was a no-op only because\nthe first pair had already restored the default. Use two scoped\nQSignalBlockers instead, so the models are unblocked exactly once before the\nexplicit changed()/layoutChanged() notifications.\n\nVerification: cockatrice built.
* Drop stale typo note from QSignalBlocker comment
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [CardDB] Guard guessCard and findPrintingWithId against missing cards
guessCard() dereferenced a null CardInfoPtr through findPrintingWithId() when the name was unknown and a providerId was given. Return an empty ExactCard instead, and make findPrintingWithId() null-safe for every caller.
Add regression tests covering both paths.
* [CardDB] Rely on PrintingInfo's default null set in findPrintingWithId
The explicit nullptr was redundant: the constructor already defaults
the set to null.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Filter] Allow spaces around the colon in search queries
* [Filter] Keep whitespace out of the color-identity operator
Allowing `ws?` between `color` and the identity marker made `c i: g`
parse as identity green, changing the meaning of text that previously
read as a generic `c` plus a field query on `i`. Whitespace belongs
around the separator, not inside an operator token: `ci : g` and
`ci: g` stay valid, `c i: g` keeps its old parse.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [CrashReporting] Installer log send offer on abort (inc 4)
DSN parsed to CPACK_SENTRY_* (scheme-aware for http self-hosts) before
include(CPack); new SendInstallerLog.ps1.in builds a byte-accurate
Sentry envelope (128 KiB tail cap, X-Sentry-Auth, exit 0/1) with the
abort reason read from the log file itself, so the NSIS side needs no
string functions and quoting stays trivial. AbortInstall asks consent
(attended installs only, silent path untouched, abort never
swallowed). Empty/malformed DSN disables the offer; no secrets in repo.
* [CrashReporting] Embed installer-log sender via a relative NSIS path
The AbortInstall hook embedded SendInstallerLog.ps1 through the absolute
CPACK_SENTRY_PS1_PATH, which carries CMake's forward slashes (D:/a/...).
makensis does not treat a forward-slash drive path as absolute, so it
resolved it relative to project.nsi, found nothing and aborted packaging
on the Windows jobs of every branch in the stack.
Embed it with the same '..\..\..\' relative path that already resolves
NSIS.definitions.nsh: both are configure_file'd into the build root,
three levels up from where CPack places project.nsi. Drop the now-unused
CPACK_SENTRY_PS1_PATH cache variable.
* [CrashReporting] Drop the installer /CRASHTEST hook, tidy the log sender
Remove the /CRASHTEST switch and the matching COCKATRICE_SENTRY_DSN
environment override on the installer abort path. With no deliberate
in-installer trigger left, a package built without a baked DSN must never
offer to post a user's log to an endpoint chosen by whatever happens to be
in the environment, so the prompt is now gated on CPACK_SENTRY_ENABLED
alone. This also retires the comment that claimed /S /R /CRASHTEST reaches
the prompt: the switch was evaluated before /R set ReinstallMode, so that
form never showed it.
Alongside: correct the CPACK_SENTRY_RELEASE comment (PROJECT_VERSION keeps
its "-<label>" on Development tags where CPACK_PACKAGE_VERSION does not),
delete the unused CPACK_SENTRY_HOST/CPACK_SENTRY_PROJECT_ID cache vars,
document the DSN regex's path-prefix fallback, log a declined send, keep
the uploaded installer log free of helper-injected newlines, and bound
the post with -TimeoutSec/-NonInteractive.
* [CrashReporting] Derive the installer DSN vars as plain variables
Review nit: every CPACK_SENTRY_* value is recomputed from
COCKATRICE_SENTRY_DSN on each configure, so CACHE ... FORCE bought
nothing. Set empty defaults once, override them in the match branch, and
drop the duplicated else() copies. Still CPACK_-prefixed, so include(CPack)
still snapshots them into CPackConfig.cmake.
* [CrashReporting] Gate the installer log send at build time
Review nit: test CPACK_SENTRY_ENABLED with the NSIS preprocessor
instead of copying it into $SentryEnabled and branching at runtime, so a
package built without a DSN compiles the prompt out rather than carrying
dead code. The configure_file that produces the helper is gated the same
way, so a build without reporting neither references nor embeds it.
* [CrashReporting] Disclose the Windows user name in the send prompt
Review nit: say that the log can include the Windows user name (a real
part of what gets uploaded) and drop the "No account needed." clause,
which answered a question the prompt never raised.
* [CrashReporting] Show progress while the installer log is sent
Review nit: DetailPrint a line before nsExec runs, so the details pane
says something while the send is in flight instead of staying blank for
up to the 60 s /TIMEOUT.
* [CrashReporting] Rename the Sentry event local in the installer helper
Review nit: $Event is a PowerShell automatic variable, so name the local
$sentryEvent instead. Also drop $firstLine: Select-String returns the
matching line alone, so the reason is already one physical line and the
extra split added nothing.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [CrashReporting] Breakpad everywhere, session detection, symbol CI (inc 3)
Breakpad on all platforms (no crashpad packaging); remove the Windows
MiniDumpWithFullMemory filter and the installer WER LocalDumps full-dump
registration. QLockFile per-process session tracking replaces the
Windows-only .dmp scan, placed after parser.process and the instance
hand-off so help/version exits and handed-off secondaries never leak a
lock. CI: Release-only DSN bake from SENTRY_DSN secret, tag-only
sentry-cli symbol upload (org cockatrice, project native).
Tests: 533/533 ctest green, format clean, code gate PASS.
* [CrashReporting] Enable http_retry, harden symbol upload, fix NSIS context
Sets sentry_options_set_http_retry so the cache_keep-held crash
envelope in Ask mode is written with a retry filename and actually
flushes on giveConsent() instead of sitting in <db>/cache until
pruning.
Adds continue-on-error to both sentry-cli install steps so a Sentry
registry outage or broken Git Bash install cannot block release
assets.
Cleans the legacy WER CrashDumps folder under SetShellVarContext
current (under all it resolves to ProgramData, not the user's
AppData) in installer and uninstaller; registry delete works in
either context.
* [CrashReporting] Extract the startup crash-report flow from main() per review
* [CrashReporting] Break up the CrashReporter class doc per review
* [CrashReporting] Drop the invalid --release from the symbol upload
`debug-files upload` never accepted `--release` (checked against the pinned
sentry-cli 3.8.0: the subcommand declares --org/--project and a set of
type/id/scan options, and execute() reads no release; the only global flags are
--url/--header/--auth-token/--log-level/--quiet/--allow-failure). Clap rejects
the unknown flag before any upload happens, so every tag build has been shipping
without symbols, with continue-on-error keeping the release pipeline green.
Release association is also the wrong mechanism: debug information files are
matched to events by debug ID via the event's debug_meta. The Sentry release of
a native event comes from the event itself (COCKATRICE_SENTRY_RELEASE), not from
the DIF upload. Remove the flag and the now-unused SENTRY_RELEASE env from both
invocations (Linux, and Windows/macOS).
The symbol upload stays continue-on-error (a Sentry outage must not block the
release assets); revisit once it has proven itself on a tag.
* [CrashReporting] Split debug information out of packaged release binaries
-g is required so the CI symbol upload has DWARF to send, but it was left in
the shipped binaries too: the Linux Release client is 563 MiB, 552 MiB of it
debug info, and every distribution channel pays for that.
Keep the build tree unchanged (CI uploads symbols from build/) and strip only
the packaged copy. A CPack pre-build script extracts each ELF's debug info to
COCKATRICE_DEBUG_SYMBOL_DIR and then runs `strip --strip-debug --strip-unneeded`.
.eh_frame is allocatable and survives, so unwinding is unaffected, and the build
ID is identical in the stripped binary and the sidecar.
Measured on a local Release build (GCC -O2 -g, WITH_CRASH_REPORTING=ON),
bytes:
binary build tree packaged before packaged after .debug sidecar
cockatrice 590,722,560 590,722,560 28,086,480 579,623,576
oracle 53,432,608 53,432,608 1,649,888 52,075,344
tar.gz of both trees: 289,964,521 -> 19,018,058 (~15x smaller).
Only ELF (Linux/BSD) is handled: Windows already links /DEBUG with separate
PDBs, and macOS extracts a dSYM from the bundle. Opt-out via
COCKATRICE_SPLIT_DEBUG_SYMBOLS; skipped with a warning when objcopy/strip are
missing. CPACK_PRE_BUILD_SCRIPTS (not CPACK_INSTALL_SCRIPTS, which runs before
staging) invokes it, and the script refuses to run unless CPACK_TEMPORARY_DIRECTORY
is a real CPack staging tree so a missing variable can never turn its glob into
a filesystem-wide walk.
* [CrashReporting] Upload Windows PDBs from where Ninja actually puts them
The Windows branch of the symbol upload looked in build/<target>/Release, which
is the Visual Studio generator's layout. The Windows leg uses Ninja, which is
single-config: linker PDBs land in build/<target>/, next to the binaries — the
same paths the "[Windows] Upload PDBs" artifact step below globs with
if-no-files-found: error.
So paths was always empty on Windows, the step printed "No symbol paths found,
skipping symbol upload" and exited 0: a silent no-op behind continue-on-error.
Every Windows build would then report "A required debug information file was
missing" for cockatrice.exe with a PDB-style debug id (GUID-age), exactly as if
no upload step existed at all.
Use the artifact step's globs. Also turn the empty-path case into a failure in
both symbol steps: a silent skip is indistinguishable from a successful upload
(which is how the wrong path went unnoticed), and continue-on-error already
keeps a broken upload from failing the release.
Verified by extracting both steps' run blocks and executing the Windows branch
against a Ninja-style tree: it now passes the three linker PDBs to sentry-cli
(and not the CMakeFiles/*.pdb intermediates), and exits non-zero with an
::error:: annotation when there is nothing to upload.
* [CrashReporting] Archive PDBs on tag builds so a failed upload is recoverable
The "[Windows] Upload PDBs" artifact step skipped tag builds
(github.ref_type != 'tag'), so for a release the symbols existed only inside
the Sentry upload — which is continue-on-error. A failed upload therefore lost
the ability to symbolicate that release permanently: no rebuild produces the
same PDB, since the debug id is a per-build GUID+age. That is how every
Windows crash reported so far became unreadable.
Archive the PDBs for tag builds as well, and order the step after
"Upload to release" so a missing PDB can never block a release from being
published. Retention stays at the repository default; nothing consumes the
artifact automatically, it is the manual recovery path
(gh run download <run> -n <name>-PDBs, then sentry-cli debug-files upload).
Verified by re-parsing the workflow and asserting the step order
(release -> PDBs -> attest) and the broadened condition.
* [CrashReporting] Upload the Windows executables with their PDBs
Sentry needs two things per image on Windows: symbolication and stack
unwinding. The PDB covers symbolication, but for a 64-bit PE the Call Frame
Information (.pdata/.xdata) lives in the executable and is not copied into the
PDB, so uploading PDBs alone leaves the image at "Stack Unwinding: Missing".
That is exactly what the event's Images panel showed for cockatrice.exe, while
Sentry's own Microsoft symbol server resolved KERNELBASE.dll and ucrtbase.dll
from the same dump.
sentry-cli also needs the pair in the same invocation: only then does it
associate the PE's code identifier with the PDB's debug identifier, otherwise
"the identifiers diverge and Sentry might not be able to resolve the correct
file for symbolication" (docs: Debug Identifiers).
Do the same for macOS, where the dSYM carries the debug information but the
Mach-O carries the unwind information, and include the executables in the
symbol artifact so a re-upload still has the pair to offer.
Verified by extracting the step and running both branches against stub
harnesses: Windows passes the three exe/pdb pairs in a single sentry-cli
invocation and still fails loudly on an empty tree; macOS passes the dSYM and
the Mach-O together.
* [CrashReporting] Upload the shipped Qt runtime DLLs too
Qt ships no PDBs with its MSVC SDK, so every Qt frame in a crash report
resolved to nothing: an image with neither PDB nor PE has no unwind
information (on 64-bit Windows the Call Frame Information lives in the PE
and is not copied into the PDB) and no symbol source to fall back on.
The list is required-runtime.txt, the manifest .ci/compile.sh already
asserts the installer against, so what gets uploaded is exactly what an
install carries. Qt frames now get unwinding plus export-table names; line
numbers still need Qt's debug information files, which the SDK omits.
The guards are not cosmetic: the step runs under `bash -eo pipefail`, where
a sed over a missing file or an unmatched ls glob aborts the whole step, and
continue-on-error makes that look identical to a successful upload.
* [CrashReporting] Upload only the binaries, archive symbols only on tags
The Linux leg handed sentry-cli whole build directories: 471 candidate files,
of which 7 were object files that can never match a stack frame. Only the
three linked binaries carry the build-ids Sentry looks up; the packaged copy
is split and stripped but keeps that same build-id.
The Windows symbol archive ran on every packaging build even though only tag
builds upload to Sentry, so a PR build paid for an artifact nothing could
ever consume. Tags keep it as the durable copy for a failed upload.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [CrashReporting] Real send path with consent gating (DSN still placeholder)
require_user_consent + give/revoke around the Ask dialog, message event
with in-memory crash-log.txt bytes attachment, before_send scrubs
message/logentry/breadcrumbs/data of cockatrice:// secrets. Failure box
offers Save (shows path) / Discard (revokes + rejects) / Cancel (back
to dialog); reject() override so X/Escape cannot bypass revocation;
silent AlwaysSend path falls back to a local file. Empty DSN keeps the
previous mock behavior byte-identical.
Tests: 528/528 ctest green, format clean, code + UI gates PASS.
* [CrashReporting] Attach crash log and user note to the pending crash event
The redacted log used to ride a separate message event from the native
crash (minidump + stack), forcing manual correlation in Sentry. Fold it
into the pending crash envelope(s) instead by appending well-formed
attachment items before consent releases them: sentry-native replays
cached envelopes byte-for-byte (raw read, raw serialize in 0.17.3), so
the appended crash-log.txt and user-note.txt items upload with the very
event that already carries the dump and stack.
Covers both storage locations: Ask mode's consent-held files in
<database>/cache and AlwaysSend's transport-dumped files in
<database>/<run>.run, which must be attached before sentry_init replays
them. When no pending crash event exists (unclean exit without a native
crash) the standalone message event remains as fallback, now also
carrying the optional note.
The dialog gains an optional free-text field ('Here's what I was
doing...') that travels as user-note.txt; the mock JSON records it too.
* [CrashReporting] Send PROJECT_VERSION as the Sentry release
main() reported VERSION_STRING (PROJECT_VERSION_FRIENDLY) as the Sentry
release — that carries a build date, so every build of the same version
landed in its own release, and it never matched the installer, which
reports CPACK/PROJECT_VERSION. Bake COCKATRICE_SENTRY_RELEASE=
PROJECT_VERSION instead: date-free, identical between client and
installer, and derived from the git tag on tag builds, so events,
symbol uploads and the installer log all group under one release.
* [CrashReporting] Friendlier crash dialog copy
Reword the report dialog along a single friendly arc: headline ('Oops,
we crashed — and now we need your help to make Cockatrice better!'),
why helping is easy, exactly what gets collected (crash dump with stack
trace, app version, OS info, redacted log excerpt), the value of each
report ('better and less crashy for everyone'), and the consent choice
('Send and remember makes this automatic without interruption in the
future'). The checkbox itself stays direction-neutral ('Remember my
choice') because it also persists a 'Don't send' decision.
* [CrashReporting] Log where crash reports go and warn when they cannot
The client resolved COCKATRICE_SENTRY_DSN (environment over the baked-in
value) without ever saying which one won, and sentry-native refuses to build
a request for a DSN it considers invalid without reporting anything, so a
tester pointing the client at a scratch project had nothing to go on when a
crash never appeared.
Say which DSN source won, and log the destination (host and project id, never
the key) once the SDK is armed. Warn instead of staying quiet when no DSN is
configured at all, when a DSN is malformed - disabling reporting rather than
silently falling back to the project baked into the build, which is the rule
the installer already follows in cmake/CrashReporting.cmake - and when the
build was compiled without Sentry support. A deliberate Never-send stays at
info level: that is a choice, not a misconfiguration.
Also warn when a pending crash envelope was captured for a different
destination than the session that will upload it. sentry-native posts every
envelope with the DSN held in the transport state of the session doing the
sending, and envelopes restored from disk are raw, so the "dsn" header of a
cached envelope is never read: a crash captured for one project can be
uploaded to another with nothing in the log to show it.
Tests cover the DSN shape check (same unanchored pattern the build matches, so
a DSN this build accepted is never rejected here), destination description
without the key, and the mismatch detection, which stays quiet for a different
key on the same project, unreadable files, and headers without a dsn.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [CrashReporting] Mock dialog, sentry skeleton and CI prerequisites
Mock: DlgCrashReport preview dialog (no account needed, redacted log
preview), CrashReportingMode Ask/Always/Never setting in DebugSettings,
--simulate-crash flag and Help-menu preview entry. Send writes a local
mock JSON only.
Vendor sentry-native 0.17.3 via FetchContent behind
WITH_CRASH_REPORTING (breakpad everywhere, backend none under
sanitizers) plus a CrashReporter facade with early init and post-loop
shutdown. DSN is a placeholder: empty CMake default with
COCKATRICE_SENTRY_DSN env override, so SDK init is skipped and mock
behavior is unchanged until a real DSN is configured.
CI prerequisites riding here so every layer below builds green:
libcurl dev plus CA bundles in the Linux images and CodeQL env, sentry
Qt integration off (its own find_package cannot see the macOS thin Qt
layout), and the StrFunc Using construction makensis requires.
Tests: ctest green, format clean, code plus UI gates PASS.
* [CrashReporting] Forward mode changes to SDK and fix Linux CI opt-in
Connects PersonalSettings::crashReportingModeChanged to
giveConsent()/revokeConsent() so switching from AlwaysSend to
NeverSend (or Ask) mid-session reaches the SDK instead of leaving
consent given until restart.
Forwards WITH_CRASH_REPORTING into the docker container the way
CCACHE_DIR is, so the Linux package leg actually builds with crash
reporting ON instead of silently building OFF.
* [CrashReporting] Give crash-report logging its own category per review
* [CrashReporting] Drop unused DebugSettings constructor visibility per review
* [CrashReporting] Document reporting modes on the enum per review
* [CrashReporting] Match the dump scan to the writer's temp fallback
Review nit: when the handler's SHGetKnownFolderPath lookup fails it writes to
<temp>/../.., but the startup scan looked in QDir::tempPath(), so a dump
produced through the fallback would never be found and its crash would never
offer to be reported. Walk up the same two levels in the scan.
The path is Windows-only (#ifdef Q_OS_WIN); the statements were compiled and
run standalone against Qt 6 to confirm the API use.
* [CrashReporting] Complete the dump-scan fallback path
The previous commit walked the scan's fallback up two levels but dropped the
CrashDumps/Cockatrice suffix that the writer appends unconditionally, so a dump
written through the fallback was still invisible to the scan.
Evaluate the writer's expression verbatim rather than mirroring it with QDir:
cdUp() is existence-checked and silently leaves the path alone when the
directory is missing, parent_path() is not, and QDir::absoluteFilePath() joins
the shared suffix without risking a doubled separator when the walk lands on a
root. Verified by compiling the block verbatim against Qt 6 with -Wextra
-Werror and comparing it with the writer's join for both the normal and the
fallback case.
Also record that %LOCALAPPDATA% is the scan's stand-in for
FOLDERID_LocalAppData and why the two normally agree.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
X-Forwarded-For carries a comma-separated list, and constructing a
QHostAddress from the raw header value returns a null address for it, so a
configured X-Forwarded-For or a multi-hop header never resolved and websocket
clients stayed on the proxy's address.
- Add parseForwardedAddress, returning the rightmost usable entry of a
forwarded header list
- Use it for the websocket handshake instead of parsing the raw value
- Exempt loopback from per-address auth rate limits so a same-host proxy with a
customized trusted_sources list cannot lock every user out
- Add forwarded_address_test
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
trusted_sources was checked with QString::contains. A dual-stack listener
reports a local proxy as ::ffff:127.0.0.1, which never matches the default
"127.0.0.1,::1". Behind nginx the forwarded client IP header was then
ignored, every websocket user shared the proxy's address, and logins
failed with "too many concurrent connections" once max_users_per_address
was reached.
Add isTrustedSource(), which parses each entry as a QHostAddress and
compares exact addresses with IPv4-mapped addresses normalized, and use it
at all four call sites. The ban command also read server/trusted_sources
instead of the documented security/trusted_sources; it now uses the same
getter. Entries that are not IP addresses are logged at startup since they
no longer match anything.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* [Client] Settings: isolate the selected section when drilling down
Show only the selected section's group box. Restore the overview on
page rows. Filter search results to the control's containing section.
Reset the scroll position on every change.
Reworked to stay in the enlarged modal instead of the settings tab.
* [Client] Settings: re-clicking an isolated section restores the overview
* [Settings] Keep section row selected when drilling down per review
* [Settings] Stop stealing nav selection mid-gesture per review
* [Settings] Sample nav selection pre-press so re-click detection works per review
* Don't flash the section on drill-down
* [Client] Settings: clear the section filter when selecting the page row per review
onTabClicked() returned early for the already-current page before switchPageContent() could call applySectionFilter(index, nullptr), so clicking the page row while a section was isolated moved the selection but left the page filtered. Only skip the restore when no section is isolated.
* [Settings] Select the search result's section in the sidebar per review
Reaching a setting through the search isolated its section and flashed the
widget, but left the navigation selection on the page row, so the sidebar
looked like it had only jumped to the section's page. Extract the section-row
selection shared with drill-down into selectSectionRow() and use it for search
results too, keeping the gesture/re-click behavior untouched.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Add AbstractSettingsPage::getSections built from each page's group boxes.
Show each multi-group page's sections as child rows in the navigation
tree. Section rows show the parent page and scroll to and highlight the
group box. Start pages collapsed. Keep single-group pages flat.
Reworked to stay in the enlarged modal instead of the settings tab.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Settings: add top-right cogwheel shortcut
Add a subtle flat settings button to the tab bar corner that opens the
settings dialog via the existing menu action slot. Icon and tooltip
follow the menu action in retranslateUi.
* [Client] Settings: flat black-cogwheel corner entry per review
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Settings: grouped left navigation for settings pages
Replace the top icon-button bar in SettingsView with a grouped left
QTreeWidget: Application, Cards and Data, Chat and Sound, Advanced. The
active page is the selected row (palette highlight); search mode hides
the tree; group headers expand but never select. Page tr() contexts
stay DlgSettings; new group names are SettingsView strings.
Reworked to stay in the enlarged modal instead of the settings tab.
* [Settings] Make groupedPages static const per review
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Settings: enlarge the settings dialog
Keep settings modal per review: size to ~90% of the main window
(5% margins each side) with screen fallback, size grip and
750x700 minimum bounded to the screen. Drops the tab approach
while giving dense pages room without the tab-switch friction.
* [Client] Settings: clamp dialog width to 1280 per review
* [Client] Settings: extract dialog size calculation per review
Move the host/screen target math out of the DlgSettings constructor into a calculateDialogSize helper. Drop the leftover 'per review' wording from the clamp comment. No behavior change.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Settings: extract SettingsView from DlgSettings
Move the search bar, tab bar, stacked pages and search results out of
DlgSettings into a reusable SettingsView widget. The dialog becomes a
thin frame (title, OK button, close validation) delegating to the view.
No visual or behavior change; tr() contexts preserved as DlgSettings.
* [Client] Settings: match CI clang-format-18 line breaking
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* Create transifex.yml
* Update transifex.yml
* Update and rename transifex.yml to transifex_github_config.yml
* Update and rename transifex_github_config.yml to github_integration_config.yml
CMAKE_GENERATOR_PLATFORM is Visual-Studio-only and stays empty under the
Ninja generator that Windows CI switched to in #7380, so TRICE_IS_64_BIT
was always 0 and every Windows artifact was mislabeled 32-bit. That fired
the installer's 'installing a 32-bit version on a 64-bit system' warning
and routed a genuinely 64-bit build into Program Files (x86) with the
32-bit registry view.
Use CMAKE_SIZEOF_VOID_P, which reflects the actual target architecture
for every generator (same test FindVCredistRuntime.cmake already uses).
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* Translate oracle_en@source.ts in ru [Manual Sync]
71% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'ru'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in ru [Manual Sync]
48% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'ru'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in yue [Manual Sync]
82% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'yue'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in yue [Manual Sync]
43% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'yue'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate oracle_en@source.ts in yue [Manual Sync]
82% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'yue'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
35% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'zh-Hant'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
16% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'sv'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in sr [Manual Sync]
23% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'sr'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in sr [Manual Sync]
7% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'sr'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in pl [Manual Sync]
82% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'pl'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in pl [Manual Sync]
41% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'pl'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in en@pirate [Manual Sync]
7% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'en@pirate'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in en@pirate [Manual Sync]
5% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'en@pirate'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
13% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'cs'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in en_US [Manual Sync]
93% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'en_US'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in en_US [Manual Sync]
67% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'en_US'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in pt_BR [Manual Sync]
92% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'pt_BR'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in pt_BR [Manual Sync]
52% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'pt_BR'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in pt [Manual Sync]
50% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'pt'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in pt [Manual Sync]
21% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'pt'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in el [Manual Sync]
52% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'el'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in el [Manual Sync]
10% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'el'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in et [Manual Sync]
80% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'et'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in et [Manual Sync]
6% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'et'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in de [Manual Sync]
93% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'de'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in de [Manual Sync]
67% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'de'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in es [Manual Sync]
93% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'es'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in es [Manual Sync]
62% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'es'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in it [Manual Sync]
93% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'it'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in it [Manual Sync]
67% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'it'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in ja [Manual Sync]
81% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'ja'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in ja [Manual Sync]
41% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'ja'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in fi [Manual Sync]
82% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'fi'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in fi [Manual Sync]
33% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'fi'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in zh-Hans [Manual Sync]
92% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'zh-Hans'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in zh-Hans [Manual Sync]
42% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'zh-Hans'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
28% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'tr'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in nl [Manual Sync]
85% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'nl'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in nl [Manual Sync]
61% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'nl'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in ko [Manual Sync]
50% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'ko'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in ko [Manual Sync]
20% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'ko'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* Translate oracle_en@source.ts in fr [Manual Sync]
93% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'fr'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
* Translate cockatrice_en@source.ts in fr [Manual Sync]
68% of minimum 5% translated source file: 'cockatrice_en@source.ts'
on 'fr'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
---------
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
50% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'nb'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
85% of minimum 5% translated source file: 'oracle_en@source.ts'
on 'ca'.
Sync of partially translated files:
untranslated content is included with an empty translation
or source language content depending on file format
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
* [Security] Add challenge-response auth with scrypt verifiers and stop storing plaintext passwords
Challenge-response authentication: the client derives a scrypt verifier
(RFC 7914, EVP_PBE_scrypt, N=32768, r=8, p=1) and authenticates with
HMAC-SHA256(key, nonce), so neither the password nor its hash is
transmitted. The stored format becomes
"$scrypt$<n>$<r>$<p>$<salt>$<verifier>" and Response_PasswordSalt
now carries the cost parameters. Strict servers only accept scrypt
verifiers; legacy accounts are migrated after a successful login.
Fix#344 for challenge-response servers: a saved profile stores the
derived verifier under the password key instead of the plaintext password.
The connect dialog loads it without revealing it, autoconnect passes it
through, the change-password dialog no longer prefills the old password
field with it, and the client only persists the verifier when
"Save password" is checked.
Took 3 minutes
Took 1 minute
Took 10 seconds
Took 7 minutes
* [Security] Harden credential format acceptance and challenge nonce validation
* [Security] Refuse KDF downgrade; trim profile name and warn on silent credential drop
* [Security] Add SupportsAll ServerOptions enumerator to fix enum validation assert
The server advertised both SupportsPasswordHash (1) and
SupportsChallengeResponseAuth (2) via a bitwise-OR that produced the
value 3, which no enumerator declared. Protobuf's proto2 enum
validation rejects the value in _internal_set_server_options, crashing
servatrice on the debug assertion. Add a SupportsAll = 3 enumerator with
an identical wire value so the bitmask reads unchanged while the value
is now a documented, valid member of the enum.
* [Security] Normalize username when issuing challenge nonce
cmdRequestPasswordSalt bound the challenge nonce to the raw, un-simplified
username while cmdLogin verifies it against the simplified name. A user
who typed leading or trailing whitespace in the username box would be
silently rejected during challenge-response login even with the correct
password. Simplify at the nonce-issuing site so both commands agree on
the normalized name.
Also drive-by formatting: collapse the now single-expression
serverOptions initializer onto one line.
* [Security] Keep an encrypted legacy password backup for old-client logins
Migrating an account to scrypt stranded pre-challenge clients, which can
only present the legacy salt+hash: after migration the server served them
a scrypt salt they cannot use, so the only recovery was a password reset.
When security/legacy_backup_key is configured (64 hex chars,
openssl rand -hex 32), submitPasswordVerifier now seals the legacy
credential with AES-256-GCM under that key before overwriting the row,
storing it in password_legacy_backup with the time it was last used. Old
clients keep working: cmdRequestPasswordSalt serves the backup's 16-char
salt in password_salt so they can derive the legacy hash they were
always able to compute, while new clients derive the verifier from the
new scrypt_salt field, which always carries the verifier salt. The
non-challenge login path accepts the plaintext or legacy hash against
the decrypted backup and refreshes its window; a migrated row with no
usable backup fails closed.
Backups age out so a database dump taken after the window closes never
carries a crackable legacy hash: getUsableLegacyBackup purges on first
use past security/legacy_backup_ttl_days (default 90, clamped 1..3650),
and a six-hourly sweep drops backups idle accounts never touch.
changeUserPassword clears the backup since a new credential invalidates
it. Without a configured key nothing is sealed or purged, so a
temporarily missing key cannot destroy existing backups.
The AAD is the simplified user name on both sides, sealing the blob to
its row so it cannot be moved between accounts. Sealing failures
degrade to the previous lockout behaviour rather than blocking
migration. The columns extend the unmerged 0036->0037 migration, so the
schema version stays 37.
* [Security] Address review: proto compat, plaintext verify, AAD case, strict scope
Fix the two blockers and follow-ups from the 2026-10-16 review:
- Advertise challenge-response in a new supports_challenge_response
field instead of the ServerOptions enum and drop SupportsAll:
proto2 enums are closed, so already-released clients parsed
server_options = 3 as NoOptions and fell back to plaintext
passwords. server_options stays exactly SupportsPasswordHash
on the wire; old clients ignore the new field.
- Verify a plaintext password directly against a migrated scrypt
row with verifyPassword() so email changes (cmdAccountEdit)
and old clients sending password keep working without a
legacy backup key. Pre-hashed values are still checked
against the backup, and its TTL window is refreshed only
when the backup is what actually matched.
- Use the stored row name as the AES-GCM AAD on both seal
and open so a login whose case differs from the stored
name (utf8mb4_unicode_ci) still opens its backup.
- Require challenge-response in strict mode only for
registered SQL accounts (userExists gate), leaving
guest/unregistered logins and none/password auth
methods unaffected.
- Document in servatrice.ini.example that a captured
legacy hash can take over a not-yet-migrated account
and that strict only constrains registered SQL
accounts. Schema version is 38.
* [Security] Raise legacy backup TTL default to 180 days
Gives operators and slow-to-upgrade users a longer downgrade
window for pre-challenge clients before unused encrypted
legacy password backups age out.
* [Security] Fix servatrice.cpp formatting per clang-format-18
* [Security] Add missing forward declaration for Command_SubmitPasswordVerifier
The cmdSubmitPasswordVerifier declaration references the proto-generated
type, which otherwise does not name a type in this header and breaks
every C++ build.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: BruebachL <bruebachl@cockatrice.example>
* [Servatrice] Relay room channel messages over ISL
* [Servatrice] Skip audit log for unknown ISL channel per review
- externalRoomSay drops messages for channels not in the local
catalog, matching Server_Room::say, instead of logging what was
never delivered
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Servatrice] Log room channel messages and bump schema to 38
* Fixup: show room_channel audit rows and keep migration nullable
The log history query returns room_channel rows, but the log tab
dropped them. Render them in the room table with an extra Channel
column. Also drop the NOT NULL on target_type from the 37->38
migration so migrated schemas match the nullable servatrice.sql
definition (and strict-mode ALTERs cannot fail on legacy NULL rows).
Update the channel tests to the flag-based privilege model: the admin
fixture now carries IsModerator alongside IsAdmin, as the database
layer assigns.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Add room channel selector with per-channel chat views
* [Client] Tabbed room channels with unread states, collapse, read-only, banners
Reworks the room chat channel display: channels move out of the combobox
into a tab bar below the message input showing every channel available
to the user.
- Each tab shows its unread message count and is tinted when it holds
unreads or a mention; both colors are user-configurable chat settings
(channelUnreadColor, channelMentionColor) with a settings-page section.
- Clicking the active tab collapses the chat: message view and input
hide and the splitter section minimizes to the tab strip; expanding
restores the previous splitter sizes. Viewing a channel clears its
unread state.
- Channels the user may read but not write (per write_access_level)
show with the message input disabled and a read-only placeholder;
sends are guarded locally, the server still enforces.
- Channel banner_message strings render at the top of their channel
view on join, like the room join message on Main.
- Replayed channel history no longer inflates unread counts; moderation
redaction now applies across all channel views.
* Fixup: keep chat tab bar visible when chat is collapsed
QSplitter children are collapsible by default, so setSizes({1, 0})
hid the whole chat group box including the channel tab bar.
Mark the chat widget non-collapsible so collapsing clamps to its
minimum size (title + tab bar) instead.
* [Room] Explicitly initialize MAIN_CHANNEL per review
* [Room] Address channel tab review follow-ups
- gate @all mentions on moderator/admin sender to match ChatView,
whole-word matching for own name and highlight words
- hide single-channel tab bar so old servers do not show a lone Main
tab that collapses chat on misclick
- capture target ChatView in sendMessage so errors land in the
channel the message was sent from
- share ignore/unregistered filtering between RoomSay and
RoomChannelSay via isRoomMessageIgnored
* [Client] Fix tab_room.cpp formatting per clang-format-18
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: BruebachL <bruebachl@cockatrice.example>
* [Server] Gate room channels on privilege levels
* [Server] Filter channel catalog per user, split read/write gating, add banners
- Server_Room::getInfo() now takes the requesting user and only lists
channels that user may read. Previously every room member received the
full catalog (e.g. the moderators channel) while message delivery was
already gated, so restricted channels were visible but silent.
- Split channel access into mayReadChannel/mayWriteChannel: history
replay, catalog listing, and event fan-out use the read level
(access_level); Command_RoomChannelSay uses the write level
(write_access_level, falling back to the read level when unset).
- Plumb write_access_level and banner_message through both room sources
(SQL rooms_channels table and config-file channels via writeaccess
and banner keys).
- Schema 38 -> 39: add write_access_level (NULL means inherit the read
level) and banner_message columns; also widen access_level to the
full six-value privilege enum to match the 37 -> 38 migration.
* Fixup: evaluate channel access per user flag instead of ladder
User flags are independent bits: IsDeveloper does not imply
IsModerator/IsAdmin, so developer-only users must not reach
moderator/admin channels. Replace the userPrivilegeLevel ladder with
hasChannelPrivilege: moderator channels need IsModerator (admins hold
it alongside IsAdmin), admin channels need IsAdmin, developer channels
need IsDeveloper or IsAdmin, judge channels admit IsJudge/IsModerator.
Covers catalog filtering, history replay, write gating and fan-out,
and corrects the ini example and proto docs.
* [Server] Make channel privilege helpers static per review
* [Server] Fail closed on unknown channel access levels per review
- roomChannelAccessFromKey returns nullopt for unknown keywords
instead of silently falling back to Public
- skip channels with unknown read/write levels with qWarning, empty
read defaults to Public, empty write means no override
- mayWriteChannel now also requires read access so write-below-read
configs cannot post into invisible channels
* [Server] Remove stray conflict marker in channel replay comment
* [Server] Fix servatrice.cpp formatting per clang-format-18
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: BruebachL <bruebachl@cockatrice.example>
* [Server] Add room chat channels with per-channel fan-out and history
* Fixup: redact all channel histories on message removal
removeSaidMessages only redacted the main history, so banned/warned
content stayed visible to late joiners on every other channel.
Drop the unused channelId parameter and redact up to amount messages
per channel across the whole channelChatHistory map.
* [Server] Validate room channel ids and fix catalog schema per review
- servatrice.sql: expand rooms_channels access_level enum to full set
to match 0037->0038 migration
- servatrice.cpp: skip empty, reserved (__main__) and duplicate channel
ids when loading from ini and DB with qWarning
- server_protocolhandler.cpp: clarify catalog replay comment (filtering
happens in moderator stack)
- server_room: drop duplicate channelIndex, linear search preserves
catalog order
* [Server] Fix server_room.cpp formatting per clang-format-18
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: BruebachL <bruebachl@cockatrice.example>
* [Protocol] Add room chat channel extensions and catalog payload
* [Protocol] Add per-channel write level and banner to room channel catalog
Adds two optional fields to ServerInfo_RoomChannel:
- write_access_level: minimum privilege level required to send to the
channel. When unset, writing requires the same level as reading
(access_level), preserving the previous single-level behavior. This
allows read-mostly channels such as a developer news feed: readable
by everyone, writable only by developers.
- banner_message: static notice shown at the top of the channel,
analogous to the room join message on Main. Empty means none.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Installer][Client] Back up user data before old uninstallers can delete it
* [Installer][Client] Fix stale backup resurrection, batch session backups, extend coverage
NSIS: clear stale %TEMP% backup and only arm restore when something was copied, directory-existence checks, back up loose .dec decks, drop downloaded pics but keep pics\CUSTOM.
Helper: single session dir per run (ms timestamps), same-basename disambiguation, partial copies still report their location.
* [Installer][Client] Fix review findings: throttle backups, NSIS old dir, portable messaging
* [Installer][Client] Fix review findings: backup markers, NSIS relocate, size cap
* [Installer][Client] Fix review findings: sounds, session coverage, NSIS wording
Client: back up data/sounds in portable mode and mention it in the
warning; reuse a recent session only when complete and covering the
current sources (source manifest), estimate size only for fresh copies,
normalize dedup via isInsideDir.
Helper: source manifest per session with sessionCoversSources(), clear
out-params on all failure paths.
NSIS: document defensive portable branch, merge backup over surviving
data/ to repair partial deletes, copy (not move) wording in relocate,
note .txt exclusion in deck notice.
* [Installer][Client] Fix review findings: stale backup, empty reuse, relocate, portable warning
NSIS: wipe TEMP backup once per run to avoid resurrecting deleted files,
check data dir existence in relocate so dir-only trees are moved.
Client: never reuse a recent session for an empty source set, only fall
back to a covering session on copy failure, warn fresh portable installs
instead of staying silent.
* [Installer][Client] Fix review findings: usable session reuse, loose decks, NSIS pics, symlinks
* [Installer][Client] Fix portable backup exclusions in offender merge
* [Client] Split at-risk data warning into focused helpers
Ricky's nit on the 200+ line warnAboutDataPathsInProgramDir: extract
collection (portable/installed), backup, message building and dialog
into named helpers. No behavior change, all tr() strings untouched.
* [Client] Read path settings inside isPortableBackupExcluded
* [Client] Make stateless at-risk helpers static
* [Client] Extract at-risk data warning out of MainWindow
* [Installer][Client] Fix review findings: subdir backup, portable warning, backup API
NSIS:
- Back up unknown top-level install subdirs (e.g. <installdir>\decks),
not just loose deck files, so the 3.1.1 -> new upgrade no longer loses
data folders the old RMDir /r deletes before the client ever runs.
Payload dirs (plugins/translations/sounds/themes) stay in place to avoid
restoring stale DLLs; restore + relocate handle the new backup set and
the one-time notice covers both decks and folders.
- Restart the %TEMP% backup when the 32/64-bit passes point at different
dirs instead of merging (which restored one dir's files into the other);
document that only the last pass relocates and the %TEMP% copy is not
a durable second copy.
Client:
- Drop the portable at-risk warning: portable installs never shipped an
uninstaller and portable updates preserve data/, so every portable user
got a spurious dialog plus daily host-machine copies.
- AtRiskDataWarning is now a plain class with Q_DECLARE_TR_FUNCTIONS
instead of QObject+Q_OBJECT; logging category renamed to
at_risk_data_warning; MainWindow wrapper inlined into startupConfigCheck.
- Lower the synchronous startup backup cap 500 -> 200 MB.
Utility/tests:
- Remove test-only backupAtRiskPath()/findRecentBackupSession() and the
no-marker/no-manifest legacy paths; isBackupSessionComplete() now
requires the complete marker; tests updated to the plural API.
* [Client] Restore collectInstalledAtRiskPaths per review
a23e02b5 removed the definition along with the portable helpers,
breaking the build (undefined reference at link time, and
isInsideAnyDir unused under -Werror on Arch/Debian 13). Restores
the 202c0d65 version (pathsInsideDir plus loose top-level deck
files) as suggested.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Tell the user when their playmat art is blocked
A server DENY rule silently drops banned playmat art: SetPlaymat fails
with no handler and DeckSelect strips the mat while answering RespOk.
Surface both cases with the same critical message box the banner card
art flow uses. DeckSelect strips are detected by comparing the
requested mat, passed via PendingCommand extraData, against the
server-echoed deck. Notices dedupe per card and reset on success.
* [Client] Gate playmat blocked notice on RespFunctionNotAllowed per review
Only SetPlaymat with RespFunctionNotAllowed signals a card art DENY
rule. Older servers answer RespInvalidCommand and strip the mat from
DeckSelect, so drop the DeckSelect comparison to avoid false
blacklisted popups on pre-playmat servers.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: BruebachL <bruebachl@users.noreply.github.com>
* [Server] Apply card art rules to playmats
Card art DENY rules were only enforced for user card art
(SetCardArtParams) while playmats using the same art bypassed them.
Move the rule lookup to Servatrice_DatabaseInterface::isCardArtAllowed
with a default-allow base on Server_DatabaseInterface, delegate the
session check to it, reject DENY-listed Command_SetPlaymat with
RespFunctionNotAllowed, and strip DENY-listed deck playmats on
DeckSelect so banned art is never broadcast.
* [Server] Address review nits on playmat card art rules
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Completer] Highlight popup rows without rewriting the line edit
Selecting a popup row with ClearAndSelect emits selectionChanged, which
QCompleter turns into highlighted() and QLineEdit turns into setText().
Since be2c16eca5 (#7466) that rewrites the typed text on every completion
reset / arrow-key press (e.g. typing "A" becomes "Axe" and cannot be
edited), regressing the 514014dc00 (#7447) fix that used NoUpdate.
Add selectWithoutRewrite(), which blocks the selection-model signals while
applying ClearAndSelect so the highlight stays visible without touching
the line edit, then syncs the preview manually. Route both the
above-popup auto-select and Up/Down navigation through it, and handle
Up/Down for below-popups too instead of falling through to Qt's
rewriting default. Enter/click acceptance via activated() is unchanged.
* [Completer] Repaint popup highlight after silent selection change
Blocking the selection model's signals also suppresses the view's own
selectionChanged/currentChanged repaints, so force a viewport update
after restoring signals. Use EnsureVisible to match Qt's normal scroll
behaviour.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-16 19:01:38 +02:00
585 changed files with 155968 additions and 52190 deletions
throw "expected exit 0 for a program the payload does not install, got $($ignored.Code) : $($ignored.Output)"
}
Write-Host "holder lookup named pid $($holder.Id) for a file another process holds, named nothing for a file only the asking process holds, reported a payload executable it could not name as in use instead of free, named nothing for an empty directory, named pid $($running.Id) for a payload program running from the directory, and named nothing for a payload program in a folder below it or for a program the payload does not install"
<b>To get started with Cockatrice ⇢ [view our webpage](https://cockatrice.github.io/)</b><br>
<b>To get support, or suggest changes to the app ⇢ [file an issue](https://github.com/Cockatrice/Cockatrice/issues) ([How?](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice))</b>
<b>To help with development ⇢ learn [how to contribute](#contribute-)</b>
<b>To get started with Cockatrice ⇢ [view our webpage](https://cockatrice.github.io/)</b>
<b>To get support, or suggest changes to the app ⇢ [file an issue](https://github.com/Cockatrice/Cockatrice/issues) ([How?](https://github.com/Cockatrice/Cockatrice/wiki/How-to-Create-a-GitHub-Ticket-Regarding-Cockatrice))</b><br>
<b>To learn about the app and its code ⇢ [read our code documentation](https://cockatrice.github.io/docs/)</b>
<b>To help with development ⇢ learn [how to contribute](#contribute)</b>
</pre><p><br>
@ -83,7 +84,7 @@ Join our [Discord community](https://discord.gg/3Z9yzmA) to connect with other p
### Code
To contribute code to the project, please review our [guidelines](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md) first.<br>
To commit code to the project, please review our [Contribution Guidelines](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md) first.<br>
We maintain two tags for contributors to easier find issues to potentially work on:
- [](https://github.com/Cockatrice/Cockatrice/issues?utf8=%E2%9C%93&q=is%3Aopen%20is%3Aissue%20label%3A%22Good%20first%20issue%22%20)<br>
Issues tagged in this way provide a simple way to get started. They don't require much experience to be worked on.
@ -95,7 +96,7 @@ We'll happily advice on how best to implement a feature, or we can show you wher
You can also have a look at our `Todo List` in our [Code Documentation](https://cockatrice.github.io/docs) or search the repo for [`\todo` comments](https://github.com/search?q=repo%3ACockatrice%2FCockatrice%20%5Ctodo&type=code).
Cockatrice tries to use the [Google Developer Documentation Style Guide](https://developers.google.com/style/) to ensure consistent documentation. We encourage you to improve the documentation by suggesting edits based on this guide.
Cockatrice tries to use the [Google Developer Documentation Style Guide](https://developers.google.com/style/) to ensure consistent documentation.
We encourage everybody to improve the documentation by suggesting edits based on this guide.